Llm/GreedyGenerationParity.cs

Validation harness for greedy LLM generation parity. It runs a greedy-generation using a SboxLlmModel and compares tokens, top-5 logits, full-logit checkpoint, and other metadata against an authoritative reference, throwing on mismatches and logging diagnostics.

File AccessNetworking
using Sandbox.Diagnostics;

namespace LlmPoc.Llm;

public sealed class GreedyGenerationParityResult
{
	public GreedyGenerationResult Generation { get; init; }
	public NumericComparison LaterStepLogitComparison { get; init; }
	public int LaterStepLogitIndex { get; init; }
	public float MinimumMargin { get; init; }
	public int MinimumMarginStep { get; init; }
	public double ValidationHarnessMilliseconds { get; init; }
}

public static class GreedyGenerationParity
{
	private const double LogitAbsoluteTolerance = 5.0e-5;
	private const double LogitRelativeTolerance = 1.0e-5;
	private const double TopLogitAbsoluteTolerance = 5.0e-5;

	public static GreedyGenerationParityResult Validate(
		SboxLlmModel model,
		TinyStoriesConfig config,
		Gpt2ByteBpeTokenizer tokenizer,
		LlmReferenceData historicalReference,
		GreedyGenerationReferenceDocument reference )
	{
		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 ( historicalReference is null )
			throw new ArgumentNullException( nameof( historicalReference ) );
		if ( reference is null ) throw new ArgumentNullException( nameof( reference ) );

		ValidateReference( config, tokenizer, historicalReference, reference );
		float[] laterExpected = ReferenceFloatData.LoadFromMounted(
			LlmPaths.ReferenceStage( reference.LaterFullLogitReference.File ),
			reference.LaterFullLogitReference.Elements );
		NumericComparison laterComparison = null;
		FastTimer validationTimer = FastTimer.StartNew();

		GreedyGenerationResult generation = TinyStoriesGreedyGenerator.Generate(
			model,
			config,
			tokenizer,
			reference.InputTokenIds,
			reference.MaxNewTokens,
			observation =>
			{
				GreedyGenerationStepResult actual = observation.Step;
				if ( actual.Step < 0 || actual.Step >= reference.Steps.Length )
				{
					throw new InvalidOperationException(
						$"[LLM:ERROR] Generation produced unexpected step {actual.Step}." );
				}
				GreedyGenerationReferenceStep expected = reference.Steps[actual.Step];
				ValidateStepContext( expected, observation.InputTokenIds );
				bool tokenPassed = actual.TokenId == expected.ExpectedNextTokenId;
				if ( !tokenPassed )
				{
					LogMismatch( tokenizer, expected, observation );
					throw new InvalidOperationException(
						$"[LLM:ERROR] Greedy generation first mismatch at step {actual.Step}: " +
						$"expected token {expected.ExpectedNextTokenId}, actual {actual.TokenId}. " +
						"No mismatching token was appended." );
				}

				if ( actual.DecodedToken != expected.DecodedToken )
				{
					throw new InvalidOperationException(
						$"[LLM:ERROR] Generation step {actual.Step} token {actual.TokenId} decoded " +
						$"as '{EscapeVisible( actual.DecodedToken )}', Python expected " +
						$"'{EscapeVisible( expected.DecodedToken )}'." );
				}
				ValidateTopFive( tokenizer, expected, actual );
				if ( actual.EosReached != expected.EosReached )
				{
					throw new InvalidOperationException(
						$"[LLM:ERROR] Generation step {actual.Step} EOS state expected " +
						$"{expected.EosReached}, actual {actual.EosReached}." );
				}

				if ( actual.Step == reference.LaterFullLogitReference.Step )
				{
					laterComparison = TensorDiagnostics.Compare(
						laterExpected,
						observation.Forward.Logits.Data,
						LogitAbsoluteTolerance,
						LogitRelativeTolerance );
					TensorSummary summary = TensorDiagnostics.Summarize(
						observation.Forward.Logits );
					LlmLog.Info(
						"PARITY",
						$"generation.step{actual.Step}.logits count={summary.ElementCount:N0} " +
						$"finite={summary.FiniteCount:N0}/{summary.ElementCount:N0} " +
						$"maxAbs={laterComparison.MaximumAbsoluteError:G12} " +
						$"meanAbs={laterComparison.MeanAbsoluteError:G12} " +
						$"maxRel={laterComparison.MaximumRelativeError:G12} " +
						$"worst_vocab={laterComparison.MaximumErrorIndex} " +
						$"expected={laterComparison.ExpectedAtMaximumError:G9} " +
						$"actual={laterComparison.ActualAtMaximumError:G9} " +
						$"absTol={LogitAbsoluteTolerance:G1} relTol={LogitRelativeTolerance:G1} " +
						$"{(laterComparison.Passed ? "PASS" : "FAIL")}" );
					if ( !laterComparison.Passed )
					{
						throw new InvalidOperationException(
							$"[LLM:ERROR] Later generation step {actual.Step} full-logit parity " +
							$"failed: {laterComparison}." );
					}
				}

				LlmLog.Info(
					"GEN",
					$"step={actual.Step} context={actual.InputSequenceLength} " +
					$"position={actual.NewTokenPosition} expected={expected.ExpectedNextTokenId} " +
					$"actual={actual.TokenId} token='{EscapeVisible( actual.DecodedToken )}' " +
					$"margin={actual.Top1Top2Margin:G9} forward_ms={actual.ForwardMilliseconds:N4} " +
					$"lm_head_ms={actual.LmHeadMilliseconds:N4} PASS" );
			} );

		RequireExactArray(
			"generated token IDs", reference.GeneratedTokenIds, generation.GeneratedTokenIds );
		RequireExactArray(
			"full generated sequence", reference.FullSequenceTokenIds,
			generation.FullSequenceTokenIds );
		if ( generation.GeneratedText != reference.GeneratedText )
		{
			throw new InvalidOperationException(
				$"[LLM:ERROR] Generated text expected '{EscapeVisible( reference.GeneratedText )}', " +
				$"actual '{EscapeVisible( generation.GeneratedText )}'." );
		}
		if ( generation.StopReason != reference.StopReason ||
			generation.EosReached != reference.EosReached )
		{
			throw new InvalidOperationException(
				$"[LLM:ERROR] Generation stop expected reason={reference.StopReason} " +
				$"eos={reference.EosReached}, actual reason={generation.StopReason} " +
				$"eos={generation.EosReached}." );
		}
		if ( laterComparison is null )
		{
			throw new InvalidOperationException(
				$"[LLM:ERROR] Later-step full-logit checkpoint " +
				$"{reference.LaterFullLogitReference.Step} did not execute." );
		}

		GreedyGenerationStepResult minimum = generation.Steps[0];
		for ( int index = 1; index < generation.Steps.Length; index++ )
		{
			if ( generation.Steps[index].Top1Top2Margin < minimum.Top1Top2Margin )
			{
				minimum = generation.Steps[index];
			}
		}
		if ( minimum.Step != reference.MinimumMarginStep ||
			Math.Abs( minimum.Top1Top2Margin - reference.MinimumTop1Top2Margin ) >
				TopLogitAbsoluteTolerance )
		{
			throw new InvalidOperationException(
				$"[LLM:ERROR] Minimum generation margin expected step=" +
				$"{reference.MinimumMarginStep} value={reference.MinimumTop1Top2Margin:G9}, " +
				$"actual step={minimum.Step} value={minimum.Top1Top2Margin:G9}." );
		}

		LlmLog.Info(
			"PARITY",
			$"greedy_sequence generated={generation.GeneratedTokenIds.Length} " +
			$"matched={generation.GeneratedTokenIds.Length} ids=" +
			$"[{string.Join( ",", generation.GeneratedTokenIds )}] " +
			$"text='{EscapeVisible( generation.GeneratedText )}' stop={generation.StopReason} PASS" );
		LlmLog.Info(
			"GEN",
			$"minimum_margin={minimum.Top1Top2Margin:G9} step={minimum.Step} " +
			$"first_token_ms={generation.Steps[0].StepMilliseconds:N4} " +
			$"last_token_ms={generation.Steps[^1].StepMilliseconds:N4} " +
			$"total_forward_ms={generation.TotalForwardMilliseconds:N4} " +
			$"total_generation_ms={generation.TotalGenerationMilliseconds:N4} " +
			$"average_ms_per_token=" +
			$"{generation.TotalGenerationMilliseconds / generation.Steps.Length:N4} " +
			$"tokens_per_second=" +
			$"{generation.Steps.Length * 1000.0 / generation.TotalGenerationMilliseconds:N4} " +
			"kv_cache=false PASS" );

		return new GreedyGenerationParityResult
		{
			Generation = generation,
			LaterStepLogitComparison = laterComparison,
			LaterStepLogitIndex = reference.LaterFullLogitReference.Step,
			MinimumMargin = minimum.Top1Top2Margin,
			MinimumMarginStep = minimum.Step,
			ValidationHarnessMilliseconds = validationTimer.ElapsedMilliSeconds
		};
	}

	private static void ValidateReference(
		TinyStoriesConfig config,
		Gpt2ByteBpeTokenizer tokenizer,
		LlmReferenceData historical,
		GreedyGenerationReferenceDocument reference )
	{
		RequireExactArray( "reference prompt IDs", historical.InputTokenIds, reference.InputTokenIds );
		RequireExactArray(
			"historical generated IDs", historical.GeneratedTokenIds,
			reference.GeneratedTokenIds );
		if ( historical.Prompt != reference.Prompt ||
			historical.GeneratedText != reference.GeneratedText )
		{
			throw new InvalidOperationException(
				"[LLM:ERROR] Compact generation reference differs from historical reference.json." );
		}
		if ( reference.EosTokenId != config.EosTokenId ||
			reference.BosTokenId != config.BosTokenId )
		{
			throw new InvalidOperationException(
				$"[LLM:ERROR] Generation special IDs expected BOS/EOS " +
				$"{config.BosTokenId}/{config.EosTokenId}, found " +
				$"{reference.BosTokenId}/{reference.EosTokenId}." );
		}
		if ( reference.DoSample || reference.BosAutomaticallyAdded ||
			reference.EosAutomaticallyAdded || !reference.RawLogitsUsedForArgmax ||
			!reference.CachedAndUncachedSequencesMatch || reference.CsharpBaselineUseCache ||
			reference.LogitsProcessors is null || reference.LogitsProcessors.Length != 0 ||
			reference.ArgmaxTieBreak != "first (lowest) vocabulary index" )
		{
			throw new InvalidOperationException(
				"[LLM:ERROR] Generation reference does not describe raw, uncached, " +
				"deterministic first-index argmax semantics." );
		}
		if ( reference.MaxNewTokens <= 0 || reference.Steps is null ||
			reference.Steps.Length != reference.GeneratedTokenIds.Length ||
			reference.Steps.Length > reference.MaxNewTokens )
		{
			throw new InvalidOperationException(
				"[LLM:ERROR] Generation reference step/token counts are inconsistent." );
		}
		if ( tokenizer.Decode( reference.GeneratedTokenIds ) != reference.GeneratedText )
		{
			throw new InvalidOperationException(
				"[LLM:ERROR] C# tokenizer cannot reproduce Python generated text from " +
				"the authoritative token sequence." );
		}
		if ( reference.LaterFullLogitReference is null ||
			reference.LaterFullLogitReference.Step < 1 ||
			reference.LaterFullLogitReference.Step >= reference.Steps.Length ||
			reference.LaterFullLogitReference.Elements != config.VocabularySize ||
			reference.LaterFullLogitReference.Bytes != config.VocabularySize * sizeof( float ) )
		{
			throw new InvalidOperationException(
				"[LLM:ERROR] Later generation full-logit reference metadata is invalid." );
		}
	}

	private static void ValidateStepContext(
		GreedyGenerationReferenceStep expected,
		int[] actualInput )
	{
		if ( expected.Step < 0 || expected.InputTokenIds is null ||
			expected.InputSequenceLength != expected.InputTokenIds.Length ||
			expected.NewTokenPosition != expected.InputSequenceLength )
		{
			throw new InvalidOperationException(
				$"[LLM:ERROR] Generation reference step {expected.Step} context metadata is invalid." );
		}
		RequireExactArray( $"generation step {expected.Step} input", expected.InputTokenIds, actualInput );
	}

	private static void ValidateTopFive(
		Gpt2ByteBpeTokenizer tokenizer,
		GreedyGenerationReferenceStep expected,
		GreedyGenerationStepResult actual )
	{
		if ( expected.TopFive is null || expected.TopFive.Length != 5 ||
			actual.TopFive is null || actual.TopFive.Length != 5 )
		{
			throw new InvalidOperationException(
				$"[LLM:ERROR] Generation step {actual.Step} requires five top-logit entries." );
		}
		for ( int rank = 0; rank < 5; rank++ )
		{
			ForwardReferenceTopLogit expectedRank = expected.TopFive[rank];
			LogitRank actualRank = actual.TopFive[rank];
			string decoded = tokenizer.Decode( new[] { actualRank.TokenId } );
			if ( expectedRank.Rank != actualRank.Rank ||
				expectedRank.TokenId != actualRank.TokenId ||
				expectedRank.DecodedToken != decoded ||
				Math.Abs( expectedRank.Logit - actualRank.Logit ) > TopLogitAbsoluteTolerance )
			{
				throw new InvalidOperationException(
					$"[LLM:ERROR] Generation step {actual.Step} top-5 rank {rank + 1} " +
					$"expected token={expectedRank.TokenId} logit={expectedRank.Logit:G9} " +
					$"decoded='{EscapeVisible( expectedRank.DecodedToken )}', actual " +
					$"token={actualRank.TokenId} logit={actualRank.Logit:G9} " +
					$"decoded='{EscapeVisible( decoded )}'." );
			}
		}
	}

	private static void LogMismatch(
		Gpt2ByteBpeTokenizer tokenizer,
		GreedyGenerationReferenceStep expected,
		GreedyGenerationStepObservation observation )
	{
		GreedyGenerationStepResult actual = observation.Step;
		LlmLog.Error(
			$"Greedy mismatch step={actual.Step} context={actual.InputSequenceLength} " +
			$"input=[{string.Join( ",", observation.InputTokenIds )}] " +
			$"expected={expected.ExpectedNextTokenId} " +
			$"expected_piece='{EscapeVisible( expected.DecodedToken )}' actual={actual.TokenId} " +
			$"actual_piece='{EscapeVisible( tokenizer.Decode( new[] { actual.TokenId } ) )}' " +
			$"expected_margin={expected.Top1Top2Margin:G9} " +
			$"actual_margin={actual.Top1Top2Margin:G9}." );
		LlmLog.Error(
			$"Python top5=[{string.Join( ",", expected.TopFive.Select( item => $"{item.TokenId}:{item.Logit:G9}" ) )}] " +
			$"C# top5=[{string.Join( ",", actual.TopFive.Select( item => $"{item.TokenId}:{item.Logit:G9}" ) )}]. " +
			"Generate a full Python logit reference for this step only before changing tolerances." );
	}

	private static void RequireExactArray( string label, int[] expected, int[] actual )
	{
		if ( expected is null || actual is null || expected.Length != actual.Length )
		{
			throw new InvalidOperationException(
				$"[LLM:ERROR] {label} expected length {expected?.Length ?? -1}, " +
				$"actual {actual?.Length ?? -1}." );
		}
		for ( int index = 0; index < expected.Length; index++ )
		{
			if ( expected[index] != actual[index] )
			{
				throw new InvalidOperationException(
					$"[LLM:ERROR] {label} mismatch at index {index}: " +
					$"expected={expected[index]}, actual={actual[index]}." );
			}
		}
	}

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