Gliner/Neural/GlinerModelWeights.cs
using System;
using System.Collections.Generic;
namespace GlinerPoc.Neural;
/// <summary>
/// Phase 4.4/4.5/4.19 — FP32 weight access over the packaged GLiNER resource.
///
/// Runtime source of truth: the normal packaged s&box model resource
/// (root manifest + eagerly-materialized shard payloads). No Hugging Face or
/// development files are read.
///
/// Ownership strategy (Phase 2/4 measured + documented):
/// - Shard payload byte[]s are engine-owned via the resource BlobData and are
/// NOT duplicated here (references only).
/// - Small tensors may be decoded+cached on demand (documented per call site).
/// - The 196.6 MB word embedding table is NEVER fully decoded: row-range reads
/// go through the SBGLI1 chunk metadata and decode only the requested rows
/// (per row: 384 BitConverter.ToSingle calls straight from the shard bytes).
/// </summary>
public sealed class GlinerModelWeights
{
private readonly GlinerPoc.Packaging.Sbgli1Manifest _manifest;
private readonly byte[][] _shardData;
private readonly Dictionary<string, float[]> _decodedCache = new( StringComparer.Ordinal );
public GlinerModelWeights( GlinerPoc.Packaging.Sbgli1Manifest manifest, byte[][] shardData )
{
_manifest = manifest ?? throw new ArgumentNullException( nameof( manifest ) );
_shardData = shardData ?? throw new ArgumentNullException( nameof( shardData ) );
}
public GlinerPoc.Packaging.Sbgli1Manifest Manifest => _manifest;
public static GlinerModelWeights FromResource( GlinerPoc.Packaging.GlinerModelResource root )
{
var manifest = GlinerPoc.Packaging.Sbgli1Manifest.Parse( root.MetadataData.Bytes );
if ( root.Shards.Count != manifest.ShardCount )
{
throw new InvalidOperationException( "[GLI:ERROR] Shard reference count != manifest shard count." );
}
var shardData = new byte[manifest.ShardCount][];
for ( int i = 0; i < root.Shards.Count; i++ )
{
var bytes = root.Shards[i].Shard?.Payload?.Bytes;
if ( bytes is not { Length: > 0 } )
{
throw new InvalidOperationException( $"[GLI:ERROR] Shard {i} payload not materialized." );
}
shardData[i] = bytes;
}
return new GlinerModelWeights( manifest, shardData );
}
/// <summary>Decode a FULL tensor (use only for small tensors; cached).</summary>
public float[] GetTensor( string name )
{
if ( _decodedCache.TryGetValue( name, out var cached ) )
{
return cached;
}
var record = _manifest.GetRequiredTensor( name );
if ( record.ByteCount > 8 * 1024 * 1024 )
{
throw new InvalidOperationException(
$"[GLI:ERROR] Refusing full decode of large tensor '{name}' ({record.ByteCount} B); " +
"use row/range access." );
}
byte[] bytes = _manifest.ReadTensorBytes( name, _shardData );
var result = GlinerMath.DecodeF32( bytes, 0, (int)record.ElementCount );
_decodedCache[name] = result;
return result;
}
/// <summary>Tensor metadata lookup.</summary>
public GlinerPoc.Packaging.Sbgli1Manifest.TensorRecord GetRecord( string name ) =>
_manifest.GetRequiredTensor( name );
/// <summary>
/// Row-range read for a rank-2 tensor stored row-major: copies only the
/// requested rows, correctly splicing across SBGLI1 row-range chunk
/// boundaries. Returns rows×cols floats. Bit-exact source values.
/// </summary>
public float[] GetRows( string name, int rowStart, int rowCount )
{
var record = _manifest.GetRequiredTensor( name );
if ( record.Rank != 2 )
{
throw new InvalidOperationException( $"[GLI:ERROR] GetRows requires rank 2, '{name}' has rank {record.Rank}." );
}
int cols = record.Shape[1];
int totalRows = record.Shape[0];
if ( rowStart < 0 || rowCount <= 0 || rowStart + rowCount > totalRows )
{
throw new InvalidOperationException(
$"[GLI:ERROR] Row range [{rowStart}, {rowStart + rowCount}) outside '{name}' ({totalRows} rows)." );
}
var result = new float[rowCount * cols];
int row = rowStart;
int done = 0;
while ( done < rowCount )
{
var chunk = FindChunkCoveringRow( record, row );
int chunkRowStart = chunk.RowStart;
int chunkRowCount = chunk.RowCount;
int take = Math.Min( chunkRowStart + chunkRowCount - row, rowCount - done );
byte[] shard = _shardData[chunk.ShardIndex];
long rowBytes = cols * 4;
long srcByte = chunk.ByteOffsetInShard + (row - chunkRowStart) * rowBytes;
int dstBase = done * cols;
for ( int r = 0; r < take; r++ )
{
long rb = srcByte + r * rowBytes;
int db = dstBase + r * cols;
for ( int c = 0; c < cols; c++ )
{
result[db + c] = BitConverter.ToSingle( shard, (int)(rb + c * 4) );
}
}
row += take;
done += take;
}
return result;
}
private GlinerPoc.Packaging.Sbgli1Manifest.ChunkRecord FindChunkCoveringRow(
GlinerPoc.Packaging.Sbgli1Manifest.TensorRecord record, int row )
{
for ( int ci = record.FirstChunkIndex; ci < record.FirstChunkIndex + record.ChunkCount; ci++ )
{
var c = _manifest.Chunks[ci];
if ( row >= c.RowStart && row < c.RowStart + c.RowCount )
{
return c;
}
}
throw new InvalidOperationException(
$"[GLI:ERROR] No chunk covers row {row} of '{record.Name}'." );
}
}