Llm/TinyStoriesGreedyGenerator.cs

Greedy generator for the TinyStories model. It deterministically generates up to maxNewTokens by recomputing the full model logits each step, selects the argmax token, records timing and top-k logits, and returns the generated tokens and diagnostics.

NetworkingFile Access
using Sandbox.Diagnostics;

namespace LlmPoc.Llm;

public sealed class GreedyGenerationStepResult
{
	public int Step { get; init; }
	public int InputSequenceLength { get; init; }
	public int NewTokenPosition { get; init; }
	public int TokenId { get; init; }
	public string DecodedToken { get; init; }
	public float Top1Logit { get; init; }
	public int Top2TokenId { get; init; }
	public float Top2Logit { get; init; }
	public float Top1Top2Margin { get; init; }
	public LogitRank[] TopFive { get; init; }
	public bool EosReached { get; init; }
	public double ForwardMilliseconds { get; init; }
	public double LmHeadMilliseconds { get; init; }
	public double StepMilliseconds { get; init; }
	public double[] LayerMilliseconds { get; init; }
}

public sealed class GreedyGenerationStepObservation
{
	public GreedyGenerationStepResult Step { get; init; }
	public int[] InputTokenIds { get; init; }
	public TinyStoriesModelForwardResult Forward { get; init; }
}

public sealed class GreedyGenerationResult
{
	public int[] PromptTokenIds { get; init; }
	public int[] GeneratedTokenIds { get; init; }
	public int[] FullSequenceTokenIds { get; init; }
	public string GeneratedText { get; init; }
	public string StopReason { get; init; }
	public bool EosReached { get; init; }
	public GreedyGenerationStepResult[] Steps { get; init; }
	public double TotalForwardMilliseconds { get; init; }
	public double TotalGenerationMilliseconds { get; init; }
}

/// <summary>
/// Deterministic correctness baseline: every new token recomputes the complete
/// model over the complete current context. No K/V state is retained.
/// </summary>
public static class TinyStoriesGreedyGenerator
{
	public static GreedyGenerationResult Generate(
		SboxLlmModel model,
		TinyStoriesConfig config,
		Gpt2ByteBpeTokenizer tokenizer,
		IReadOnlyList<int> promptTokenIds,
		int maxNewTokens,
		Action<GreedyGenerationStepObservation> observer = null,
		bool logLifecycle = true )
	{
		if ( model is null ) throw new ArgumentNullException( nameof( model ) );
		if ( config is null ) throw new ArgumentNullException( nameof( config ) );
		if ( tokenizer is null ) throw new ArgumentNullException( nameof( tokenizer ) );
		if ( promptTokenIds is null ) throw new ArgumentNullException( nameof( promptTokenIds ) );
		if ( promptTokenIds.Count == 0 )
		{
			throw new ArgumentException(
				"[LLM:ERROR] Greedy generation requires at least one prompt token.",
				nameof( promptTokenIds ) );
		}
		if ( maxNewTokens <= 0 )
		{
			throw new ArgumentOutOfRangeException(
				nameof( maxNewTokens ), maxNewTokens,
				"[LLM:ERROR] Greedy generation maxNewTokens must be positive." );
		}
		if ( promptTokenIds.Count > config.MaximumPositions - maxNewTokens )
		{
			throw new InvalidOperationException(
				$"[LLM:ERROR] Greedy generation prompt length {promptTokenIds.Count} plus " +
				$"maxNewTokens {maxNewTokens} would exceed maximum context length " +
				$"{config.MaximumPositions}; context truncation is disabled." );
		}

		int[] promptCopy = promptTokenIds.ToArray();
		List<int> context = new( promptCopy.Length + maxNewTokens );
		context.AddRange( promptCopy );
		List<int> generated = new( maxNewTokens );
		List<GreedyGenerationStepResult> steps = new( maxNewTokens );
		double totalForwardMilliseconds = 0;
		double totalGenerationMilliseconds = 0;
		bool eosReached = false;

		if ( logLifecycle )
		{
			LlmLog.Info(
				"GEN",
				$"starting prompt_tokens={promptCopy.Length} max_new_tokens={maxNewTokens} " +
				$"maximum_context={config.MaximumPositions} strategy=greedy " +
				"full_recompute=true kv_cache=false" );
		}

		for ( int stepIndex = 0; stepIndex < maxNewTokens; stepIndex++ )
		{
			FastTimer stepTimer = FastTimer.StartNew();
			TinyStoriesModelForwardResult forward =
				TinyStoriesModelForward.ForwardLastTokenLogits( model, config, context );
			int tokenId = TinyStoriesModelHead.ArgmaxFinite(
				forward.Logits.Data, $"generation.step{stepIndex}.logits" );
			LogitRank[] topFive = TinyStoriesModelHead.TopKFinite(
				forward.Logits.Data, 5, $"generation.step{stepIndex}.logits" );
			string decodedToken = tokenizer.Decode( new[] { tokenId } );
			float margin = topFive[0].Logit - topFive[1].Logit;
			bool stepEos = tokenId == config.EosTokenId;
			GreedyGenerationStepResult step = new()
			{
				Step = stepIndex,
				InputSequenceLength = context.Count,
				NewTokenPosition = context.Count,
				TokenId = tokenId,
				DecodedToken = decodedToken,
				Top1Logit = topFive[0].Logit,
				Top2TokenId = topFive[1].TokenId,
				Top2Logit = topFive[1].Logit,
				Top1Top2Margin = margin,
				TopFive = topFive,
				EosReached = stepEos,
				ForwardMilliseconds = forward.TotalMilliseconds,
				LmHeadMilliseconds = forward.LmHeadMilliseconds,
				StepMilliseconds = stepTimer.ElapsedMilliSeconds,
				LayerMilliseconds = forward.LayerMilliseconds
			};

			GreedyGenerationStepObservation observation = new()
			{
				Step = step,
				InputTokenIds = context.ToArray(),
				Forward = forward
			};
			observer?.Invoke( observation );
			if ( observer is null && logLifecycle )
			{
				LlmLog.Info(
					"GEN",
					$"step={stepIndex} context={context.Count} token={tokenId} " +
					$"piece='{EscapeVisible( decodedToken )}' margin={margin:G9} " +
					$"forward_ms={forward.TotalMilliseconds:N4}" );
			}
			LlmLog.Trace(
				"GEN",
				$"step={stepIndex} sequence={context.Count} new_token_position={context.Count} " +
				$"top5=[{string.Join( ",", topFive.Select( item => $"{item.TokenId}:{item.Logit:G9}" ) )}]" );

			context.Add( tokenId );
			generated.Add( tokenId );
			steps.Add( step );
			totalForwardMilliseconds += forward.TotalMilliseconds;
			totalGenerationMilliseconds += step.StepMilliseconds;
			if ( stepEos )
			{
				eosReached = true;
				break;
			}
		}

		for ( int index = 0; index < promptCopy.Length; index++ )
		{
			if ( promptCopy[index] != promptTokenIds[index] )
			{
				throw new InvalidOperationException(
					$"[LLM:ERROR] Greedy generation mutated caller prompt token {index}." );
			}
		}

		return new GreedyGenerationResult
		{
			PromptTokenIds = promptCopy,
			GeneratedTokenIds = generated.ToArray(),
			FullSequenceTokenIds = context.ToArray(),
			GeneratedText = tokenizer.Decode( generated ),
			StopReason = eosReached ? "eos" : "max_new_tokens",
			EosReached = eosReached,
			Steps = steps.ToArray(),
			TotalForwardMilliseconds = totalForwardMilliseconds,
			TotalGenerationMilliseconds = totalGenerationMilliseconds
		};
	}

	private static string EscapeVisible( string value )
	{
		return value
			.Replace( "\\", "\\\\" )
			.Replace( "\r", "\\r" )
			.Replace( "\n", "\\n" )
			.Replace( "\t", "\\t" )
			.Replace( "'", "\\'" );
	}
}