Gliner/Neural/GlinerAttentionParity.cs
using System;
using System.Collections.Generic;
using GlinerPoc.Preprocessing;
using System.Diagnostics;
using Sandbox;
namespace GlinerPoc.Neural;
/// <summary>
/// Phase 5.37 — attention/layer parity harness. For each oracle case (short +
/// S=40) runs native preprocessing → embeddings → layer 0, comparing every
/// exported stage boundary against the pinned Python reference. Integer index
/// fixtures are compared exactly; float stages use the 1e-5/1e-4 policy.
/// </summary>
[Title( "GLiNER Layer Parity" )]
[Category( "GLiNER" )]
public sealed class GlinerAttentionParity : Component
{
[Property]
public bool RunOnStart { get; set; } = true;
[Property]
public GlinerPoc.Packaging.GlinerModelResource ModelResource { get; set; }
private int _passed;
private int _failed;
protected override void OnStart()
{
if ( RunOnStart )
{
_ = RunAsync();
}
}
private async Task RunAsync()
{
var sw = Stopwatch.StartNew();
Log.Info( "[GLI:L05] layer parity harness start" );
try
{
if ( ModelResource is null )
{
throw new InvalidOperationException( "[GLI:ERROR] No GlinerModelResource assigned." );
}
GlinerTokenizer tokenizer = null;
GlinerModelWeights weights = null;
double tokMs = 0.0;
await Task.RunInThreadAsync( () =>
{
tokenizer = GlinerTokenizer.Load( ModelResource.TokenizerData.Bytes, out tokMs );
weights = GlinerModelWeights.FromResource( ModelResource );
} );
Log.Info( $"[GLI:L05] ready tokenizer_ms={tokMs:N1}" );
var processor = new GlinerProcessor( tokenizer );
var layer = new GlinerDebertaLayer();
var layerWeights = layer.LoadLayer0( weights );
float[] normRel = GlinerMath.LayerNorm(
weights.GetTensor( "encoder.encoder.rel_embeddings.weight" ),
512, 384,
weights.GetTensor( "encoder.encoder.LayerNorm.weight" ),
weights.GetTensor( "encoder.encoder.LayerNorm.bias" ), 1e-7f );
float[] embLnW = weights.GetTensor( "encoder.embeddings.LayerNorm.weight" );
float[] embLnB = weights.GetTensor( "encoder.embeddings.LayerNorm.bias" );
foreach ( var fixture in NeuralP5FixturesV2.All )
{
RunCase( processor, layer, layerWeights, weights, embLnW, embLnB, normRel, fixture );
}
Log.Info( $"[GLI:L05] parity complete passed={_passed} failed={_failed} " +
$"total_ms={sw.ElapsedMilliseconds}" );
Log.Info( _failed == 0 ? "[GLI:L05] ALL PASS" : $"[GLI:L05] FAILURES PRESENT ({_failed})" );
}
catch ( Exception error )
{
Log.Error( $"[GLI:L05] harness failure: {error.Message}" );
Log.Error( $"[GLI:L05] parity complete passed={_passed} failed={_failed}" );
}
}
private void RunCase(
GlinerProcessor processor,
GlinerDebertaLayer layer,
GlinerDebertaLayer.LayerWeights w,
GlinerModelWeights weights,
float[] embLnW, float[] embLnB, float[] normRel,
NeuralP5FixturesV2.Case f )
{
string name = f.Name;
try
{
int S = f.SeqLen;
// ---- native preprocessing must reproduce oracle IDs ----
var encoded = processor.Encode( BuildRequest( name ), collectDiagnostics: false );
bool idsOk = encoded.InputIds.Length == S;
if ( idsOk )
{
for ( int i = 0; i < S; i++ )
{
idsOk &= encoded.InputIds[i] == f.Ids[i];
}
}
Gate( name, "preprocessing_ids", idsOk, "exact" );
// ---- embeddings: gather + LN ----
float[] gathered = new float[S * 384];
for ( int i = 0; i < S; i++ )
{
float[] row = weights.GetRows( "encoder.embeddings.word_embeddings.weight", encoded.InputIds[i], 1 );
for ( int j = 0; j < 384; j++ )
{
gathered[i * 384 + j] = row[j];
}
}
float[] layerInput = GlinerMath.LayerNorm( gathered, S, 384, embLnW, embLnB, 1e-7f );
CompareF( name, "layer_input", layerInput, f.T( "layer_input" ) );
// ---- relative positions (exact ints) ----
int[,] rel = GlinerRelativePositions.BuildRelativePositions( S, S, 256, -1 );
bool relOk = true;
for ( int q = 0; q < S && relOk; q++ )
{
for ( int k = 0; k < S; k++ )
{
if ( rel[q, k] != f.RelIdx[q * S + k] )
{
relOk = false;
Fail( name, $"rel_idx[{q},{k}] expected {f.RelIdx[q * S + k]} found {rel[q, k]}" );
}
if ( Math.Clamp( rel[q, k] + 256, 0, 511 ) != f.C2pIdx[q * S + k] )
{
relOk = false;
Fail( name, $"c2p_idx[{q},{k}] mismatch" );
}
// Fixture documents the SOURCE's index matrix before the
// transpose: p2c_pos[0,q,k] = clamp(-(q-k)+256) = k-q+256.
if ( Math.Clamp( rel[q, k] * -1 + 256, 0, 511 ) != f.P2cIdx[q * S + k] )
{
relOk = false;
Fail( name, $"p2c_idx[{q},{k}] mismatch" );
}
}
}
Gate( name, "rel_indices", relOk, "exact (rel + c2p + p2c)" );
// ---- full layer forward with stage trace ----
var trace = new Dictionary<string, float[]>();
var sw = Stopwatch.StartNew();
float[] layerOut = layer.Forward( w, layerInput, S, encoded.AttentionMask, rel, normRel, trace );
double layerMs = sw.Elapsed.TotalMilliseconds;
// ---- stage gates ----
CompareF( name, "q_proj", trace["q_proj"], f.T( "q_proj" ) );
CompareF( name, "k_proj", trace["k_proj"], f.T( "k_proj" ) );
CompareF( name, "v_proj", trace["v_proj"], f.T( "v_proj" ) );
CompareF( name, "q_layer", trace["q_layer"], f.T( "q_layer" ) );
CompareF( name, "k_layer", trace["k_layer"], f.T( "k_layer" ) );
CompareF( name, "v_layer", trace["v_layer"], f.T( "v_layer" ) );
CompareF( name, "pos_q_rows", trace["pos_q_head_rows"], f.T( "pos_q_head_rows" ) );
CompareF( name, "pos_k_rows", trace["pos_k_head_rows"], f.T( "pos_k_head_rows" ) );
CompareF( name, "c2c", trace["c2c"], f.T( "c2c" ) );
CompareF( name, "c2p_contrib", trace["c2p_contrib"], f.T( "c2p_contrib" ) );
CompareF( name, "p2c_contrib", trace["p2c_contrib"], f.T( "p2c_contrib" ) );
CompareF( name, "scores_premask", trace["scores_premask"], f.T( "scores_premask" ) );
CompareF( name, "probs", trace["probs"], f.T( "probs" ) );
CompareF( name, "context", trace["context"], f.T( "context" ) );
CompareF( name, "attn_dense", trace["attn_dense"], f.T( "attn_dense" ) );
CompareF( name, "attn_ln", trace["attn_ln"], f.T( "attn_ln" ) );
CompareF( name, "ffn_pre", trace["ffn_pre"], f.T( "ffn_pre" ) );
CompareF( name, "gelu", trace["gelu"], f.T( "gelu" ) );
CompareF( name, "ffn_out", trace["ffn_out"], f.T( "ffn_out" ) );
CompareF( name, "layer_output", layerOut, f.T( "layer_out" ) );
Log.Info( $"[GLI:L05] PERF {name} layer_ms={layerMs:N2} seq={S}" );
}
catch ( Exception error )
{
_failed++;
Log.Error( $"[GLI:L05] FAIL {name}: {error.Message}" );
}
}
private static GlinerClassificationRequest BuildRequest( string name )
{
if ( name == "short" )
{
return new GlinerClassificationRequest( "Boss close. Ammo low. Cover near.",
"Choose the best action.",
new[] { new GlinerCandidate( "attack" ), new GlinerCandidate( "retreat" ) } );
}
return new GlinerClassificationRequest(
"Health: 25%. Ammo: 2/10. Player visible: yes. Heal available: yes.",
"Choose the best action.",
new[] { new GlinerCandidate( "attack" ), new GlinerCandidate( "heal" ),
new GlinerCandidate( "reload" ), new GlinerCandidate( "retreat" ) } );
}
private void CompareF( string caseName, string stage, float[] actual, float[] expected )
{
if ( actual is null )
{
throw new InvalidOperationException( $"{stage}: no trace recorded" );
}
if ( actual.Length != expected.Length )
{
throw new InvalidOperationException(
$"{stage}: length {actual.Length} vs expected {expected.Length}" );
}
var m = GlinerMath.Compare( actual, expected );
if ( !m.Pass )
{
throw new InvalidOperationException(
$"{stage}: maxAbs={m.MaxAbs:0.###e-00} meanAbs={m.MeanAbs:0.###e-00} " +
$"worst={m.WorstIndex} expected={expected[m.WorstIndex]} actual={actual[m.WorstIndex]}" );
}
Gate( caseName, stage, true,
$"maxAbs={m.MaxAbs:0.###e-00} meanAbs={m.MeanAbs:0.###e-00} maxRel={m.MaxRel:0.###e-00}" );
}
private void Gate( string caseName, string gate, bool pass, string detail )
{
if ( pass )
{
_passed++;
Log.Info( $"[GLI:L05] PASS {caseName}/{gate} {detail}" );
}
else
{
_failed++;
Log.Error( $"[GLI:L05] FAIL {caseName}/{gate} {detail}" );
}
}
private void Fail( string caseName, string detail )
{
Log.Error( $"[GLI:L05] detail FAIL {caseName}: {detail}" );
}
}