Llm/ModelMetadata.cs

Defines DTOs and validators for an on-device LLM model. Contains configuration for a TinyStories model, manifest types for tensors and reference data, and a validator that checks consistency between a loaded binary model and a JSON tensor manifest.

File Access
using System.Text.Json.Serialization;

namespace LlmPoc.Llm;

public sealed class TinyStoriesConfig
{
	[JsonPropertyName( "vocab_size" )]
	public int VocabularySize { get; set; }

	[JsonPropertyName( "max_position_embeddings" )]
	public int MaximumPositions { get; set; }

	[JsonPropertyName( "hidden_size" )]
	public int HiddenSize { get; set; }

	[JsonPropertyName( "num_layers" )]
	public int LayerCount { get; set; }

	[JsonPropertyName( "num_heads" )]
	public int AttentionHeadCount { get; set; }

	[JsonPropertyName( "window_size" )]
	public int AttentionWindowSize { get; set; }

	[JsonPropertyName( "activation_function" )]
	public string ActivationFunction { get; set; }

	[JsonPropertyName( "layer_norm_epsilon" )]
	public float LayerNormEpsilon { get; set; }

	[JsonPropertyName( "embed_dropout" )]
	public float EmbeddingDropout { get; set; }

	[JsonPropertyName( "bos_token_id" )]
	public int BosTokenId { get; set; }

	[JsonPropertyName( "eos_token_id" )]
	public int EosTokenId { get; set; }

	[JsonPropertyName( "tie_word_embeddings" )]
	public bool TieWordEmbeddings { get; set; }

	[JsonPropertyName( "attention_layers" )]
	public string[] AttentionLayers { get; set; }

	public void Validate()
	{
		if ( VocabularySize <= 0 || HiddenSize <= 0 || LayerCount <= 0 || AttentionHeadCount <= 0 )
		{
			throw new InvalidOperationException(
				$"[LLM:ERROR] Invalid model config: vocab={VocabularySize}, hidden={HiddenSize}, " +
				$"layers={LayerCount}, heads={AttentionHeadCount}." );
		}
		if ( HiddenSize % AttentionHeadCount != 0 )
		{
			throw new InvalidOperationException(
				$"[LLM:ERROR] Hidden size {HiddenSize} is not divisible by " +
				$"attention head count {AttentionHeadCount}." );
		}
		if ( AttentionLayers is null || AttentionLayers.Length != LayerCount )
		{
			throw new InvalidOperationException(
				$"[LLM:ERROR] Expected {LayerCount} attention layer modes, " +
				$"found {AttentionLayers?.Length ?? 0}." );
		}
	}
}

public sealed class TensorManifestDocument
{
	[JsonPropertyName( "format" )]
	public string Format { get; set; }

	[JsonPropertyName( "version" )]
	public int Version { get; set; }

	[JsonPropertyName( "dtype" )]
	public string Dtype { get; set; }

	[JsonPropertyName( "tensor_count" )]
	public int TensorCount { get; set; }

	[JsonPropertyName( "tensors" )]
	public List<TensorManifestEntry> Tensors { get; set; }
}

public sealed class TensorManifestEntry
{
	[JsonPropertyName( "name" )]
	public string Name { get; set; }

	[JsonPropertyName( "shape" )]
	public int[] Shape { get; set; }

	[JsonPropertyName( "dtype" )]
	public string Dtype { get; set; }

	[JsonPropertyName( "elements" )]
	public long Elements { get; set; }

	[JsonPropertyName( "bytes" )]
	public long Bytes { get; set; }
}

public sealed class LlmReferenceData
{
	[JsonPropertyName( "prompt" )]
	public string Prompt { get; set; }

	[JsonPropertyName( "input_token_ids" )]
	public int[] InputTokenIds { get; set; }

	[JsonPropertyName( "greedy_next_token_id" )]
	public int GreedyNextTokenId { get; set; }

	[JsonPropertyName( "greedy_next_token_text" )]
	public string GreedyNextTokenText { get; set; }

	[JsonPropertyName( "generated_token_ids" )]
	public int[] GeneratedTokenIds { get; set; }

	[JsonPropertyName( "generated_text" )]
	public string GeneratedText { get; set; }

	[JsonPropertyName( "full_sequence_token_ids" )]
	public int[] FullSequenceTokenIds { get; set; }

	[JsonPropertyName( "eos_token_id" )]
	public int EosTokenId { get; set; }

	[JsonPropertyName( "bos_token_id" )]
	public int BosTokenId { get; set; }
}

public static class ModelManifestValidator
{
	public static void Validate( SboxLlmModel model, TensorManifestDocument manifest )
	{
		if ( model is null )
		{
			throw new ArgumentNullException( nameof( model ) );
		}
		if ( manifest is null )
		{
			throw new ArgumentNullException( nameof( manifest ) );
		}
		if ( manifest.Format != "SBOXLLM" || manifest.Version != SboxLlmModelLoader.SupportedVersion )
		{
			throw new InvalidOperationException(
				$"[LLM:ERROR] Manifest format/version expected SBOXLLM/" +
				$"{SboxLlmModelLoader.SupportedVersion}, found {manifest.Format}/{manifest.Version}." );
		}
		if ( manifest.Dtype != "float32" )
		{
			throw new InvalidOperationException(
				$"[LLM:ERROR] Manifest dtype expected float32, found '{manifest.Dtype}'." );
		}
		if ( manifest.Tensors is null )
		{
			throw new InvalidOperationException( "[LLM:ERROR] Manifest tensors array is missing." );
		}
		if ( manifest.TensorCount != manifest.Tensors.Count || model.TensorCount != manifest.TensorCount )
		{
			throw new InvalidOperationException(
				$"[LLM:ERROR] Tensor count mismatch: binary={model.TensorCount}, " +
				$"manifest field={manifest.TensorCount}, manifest entries={manifest.Tensors.Count}." );
		}

		for ( int index = 0; index < manifest.Tensors.Count; index++ )
		{
			TensorManifestEntry expected = manifest.Tensors[index];
			Tensor actual = model.GetRequiredTensor( expected.Name );
			if ( expected.Dtype != "float32" )
			{
				throw new InvalidOperationException(
					$"[LLM:ERROR] {expected.Name} manifest dtype expected float32, found {expected.Dtype}." );
			}
			actual.RequireShape( expected.Shape );
			if ( expected.Elements < 0 || expected.Bytes < 0 ||
				actual.ElementCount != expected.Elements ||
				expected.Bytes / sizeof( float ) != expected.Elements ||
				expected.Bytes % sizeof( float ) != 0 )
			{
				throw new InvalidOperationException(
					$"[LLM:ERROR] {expected.Name} manifest counts disagree: " +
					$"binary elements={actual.ElementCount}, manifest elements={expected.Elements}, " +
					$"manifest bytes={expected.Bytes}." );
			}
		}
	}
}