Gliner/Gpu/GlinerGpuParity.cs
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Sandbox;
namespace GlinerPoc.Gpu;
/// <summary>
/// Phase 8A.11–8A.21 — isolated GPU numerical parity harness. Runs FP32 GPU
/// compute kernels (naïve, correctness-first) against the validated scalar
/// CPU backend (GlinerMath — the permanent oracle) inside normal s&box play
/// mode, using the established tolerance policy (atol 1e-5, rtol 1e-4):
///
/// vec add / ReLU • GEMM Y = XWᵀ(+b): synthetic (non-square, non-group
/// dims), real GLiNER dimensions (S=40/128 × 384/1536), one REAL packaged
/// layer-0 tensor chain • write→read buffer chaining with no intermediate
/// readback • LayerNorm (synthetic + full real rel_embeddings 512×384) •
/// erf GELU (synthetic + real FFN pre-activation) • layer-0 multi-tensor
/// weight residency across rounds • upload/dispatch/readback timing split
/// (fresh vs resident) • GPU FP32 weight-memory arithmetic from the manifest.
///
/// Also reports secondary drift vs the embedded Python fixtures where they
/// exist (Layer0QueryProjOutput, NormalizedRelEmbeddings, Layer0GeluOutput).
/// No production neural class is modified by this harness.
/// </summary>
[Title( "GLiNER GPU Parity" )]
[Category( "GLiNER" )]
public sealed class GlinerGpuParity : Component
{
[Property]
public bool RunOnStart { get; set; } = true;
[Property]
public GlinerPoc.Packaging.GlinerModelResource ModelResource { get; set; }
private int _passed;
private int _failed;
// deterministic LCG so every synthetic tensor is reproducible run-to-run
private static float Lcg( ref uint state )
{
state = state * 1664525u + 1013904223u;
return ((state >> 8) & 0xFFFF) / 65535f - 0.5f;
}
private static float[] LcgArray( int count, uint seed )
{
uint s = seed;
var a = new float[count];
for ( int i = 0; i < count; i++ )
{
a[i] = Lcg( ref s );
}
return a;
}
protected override void OnStart()
{
if ( RunOnStart )
{
_ = RunAsync();
}
}
private async Task RunAsync()
{
var sw = Stopwatch.StartNew();
GlinerGpuShaderLoader.Reset(); // hotload-preserved statics must not poison this run
Log.Info( "[GLI:GPUP] gpu parity harness start (FP32, tolerance atol=1e-5 rtol=1e-4, CPU oracle = GlinerMath)" );
try
{
if ( ModelResource is null )
{
throw new InvalidOperationException( "[GLI:ERROR] No GlinerModelResource assigned." );
}
GateVecAdd();
GateRelu();
GateGemmSynthetic();
await GateGemmRealDims();
await GateGemmRealWeights();
await GateChainNoReadback();
await GateLayerNormSynthetic();
await GateLayerNormReal();
await GateGelu();
await GateLayer0Residency();
GateMemoryEstimate();
Log.Info( $"[GLI:GPUP] parity complete passed={_passed} failed={_failed} total_ms={sw.ElapsedMilliseconds}" );
Log.Info( _failed == 0
? "[GLI:GPUP] ALL PASS"
: $"[GLI:GPUP] FAILURES PRESENT ({_failed})" );
}
catch ( Exception error )
{
Log.Error( $"[GLI:GPUP] harness failure: {error}" );
}
}
// ---- 8A.11: vector primitives ---------------------------------------------
private void GateVecAdd()
{
var a = LcgArray( 4096, 1001 );
float[] cpu = GlinerPoc.Neural.GlinerMath.Add( a, Fill( 4096, 0.25f ) );
var shader = GlinerGpuShaderLoader.Create( "gliner_vec_add.shader" );
float[] gpu = RunVecAdd( shader, a, 0.25f, out _, out _ );
var m = GlinerPoc.Neural.GlinerMath.Compare( gpu, cpu );
Gate( "gpu_vec_add", m.Pass,
$"4096 vals maxAbs={m.MaxAbs:0.###e-00} meanAbs={m.MeanAbs:0.###e-00} maxRel={m.MaxRel:0.###e-00} worst={m.WorstIndex}", "" );
}
private static float[] Fill( int count, float value )
{
var a = new float[count];
for ( int i = 0; i < count; i++ )
{
a[i] = value;
}
return a;
}
private static float[] RunVecAdd( ComputeShader shader, float[] input, float constant,
out double uploadMs, out double dispatchReadbackMs )
{
int n = input.Length;
using var inBuf = new GpuBuffer<float>( n );
using var outBuf = new GpuBuffer<float>( n );
var swUp = Stopwatch.StartNew();
inBuf.SetData( input );
swUp.Stop();
shader.Attributes.Set( "InputValues", inBuf );
shader.Attributes.Set( "OutputValues", outBuf );
shader.Attributes.Set( "ValueCount", n );
shader.Attributes.Set( "AddConstant", constant );
var swCycle = Stopwatch.StartNew();
shader.Dispatch( n, 1, 1 );
var result = new float[n];
outBuf.GetData( result, 0, n );
swCycle.Stop();
uploadMs = swUp.Elapsed.TotalMilliseconds;
dispatchReadbackMs = swCycle.Elapsed.TotalMilliseconds;
return result;
}
private void GateRelu()
{
var x = LcgArray( 15367, 2001 ); // non-multiple-of-64 length on purpose
float[] cpu = GlinerPoc.Neural.GlinerMath.Relu( x );
var shader = GlinerGpuShaderLoader.Create( "gliner_relu.shader" );
int n = x.Length;
using var inBuf = new GpuBuffer<float>( n );
using var outBuf = new GpuBuffer<float>( n );
inBuf.SetData( x );
shader.Attributes.Set( "InputValues", inBuf );
shader.Attributes.Set( "OutputValues", outBuf );
shader.Attributes.Set( "ValueCount", n );
shader.Dispatch( n, 1, 1 );
var result = new float[n];
outBuf.GetData( result, 0, n );
int exact = 0;
for ( int i = 0; i < n; i++ )
{
if ( BitConverter.SingleToInt32Bits( result[i] ) == BitConverter.SingleToInt32Bits( cpu[i] ) )
{
exact++;
}
}
Gate( "gpu_relu", exact == n, $"{n} vals bit-exact vs CPU ({exact}/{n})", "" );
}
// ---- 8A.12/8A.13: synthetic GEMM (bounds handling, odd shapes) --------------
private void GateGemmSynthetic()
{
var shader = GlinerGpuShaderLoader.Create( "gliner_gemm.shader" );
// case 1: 4x3 · 2x3ᵀ with bias (the P4 synthetic linear shape)
bool c1 = GemmCase( shader, 4, 3, 2, true, 3001, "gemm_syn_4x3_2_bias" );
// case 2: same, no bias
bool c2 = GemmCase( shader, 4, 3, 2, false, 3002, "gemm_syn_4x3_2_nobias" );
// case 3: 5x7 · 3x7ᵀ — non-square, dims not multiples of the 8x8 group
bool c3 = GemmCase( shader, 5, 7, 3, true, 3003, "gemm_syn_5x7_3_bias" );
// case 4: 1x384 · 384x384ᵀ — single row, real width
bool c4 = GemmCase( shader, 1, 384, 384, true, 3004, "gemm_syn_1x384_384" );
Gate( "gpu_gemm_synthetic", c1 && c2 && c3 && c4, "4 synthetic shapes incl. odd/group-remainder dims", "" );
}
/// <summary>Runs one GEMM case on GPU + CPU and gates the comparison.</summary>
private bool GemmCase( ComputeShader shader, int rows, int inDim, int outDim, bool withBias, uint seed, string name )
{
float[] x = LcgArray( rows * inDim, seed );
float[] w = LcgArray( outDim * inDim, seed + 1 );
float[] b = LcgArray( outDim, seed + 2 );
float[] cpu = GlinerPoc.Neural.GlinerMath.Linear( x, rows, w, withBias ? b : null, inDim, outDim );
float[] gpu = RunGemm( shader, x, w, withBias ? b : null, rows, inDim, outDim, out _, out _, out _ );
var m = GlinerPoc.Neural.GlinerMath.Compare( gpu, cpu );
Gate( name, m.Pass,
$"[{rows},{inDim}]x[{inDim},{outDim}] bias={withBias} maxAbs={m.MaxAbs:0.###e-00} meanAbs={m.MeanAbs:0.###e-00} " +
$"maxRel={m.MaxRel:0.###e-00} worst={m.WorstIndex}", "" );
return m.Pass;
}
/// <summary>Upload → dispatch → readback for one GEMM; timings in ms.</summary>
private static float[] RunGemm( ComputeShader shader, float[] x, float[] w, float[] bias,
int rows, int inDim, int outDim,
out double uploadMs, out double dispatchMs, out double readbackMs )
{
using var xBuf = new GpuBuffer<float>( x.Length );
using var wBuf = new GpuBuffer<float>( w.Length );
using var bBuf = new GpuBuffer<float>( bias is null ? 1 : bias.Length );
using var yBuf = new GpuBuffer<float>( rows * outDim );
var swUp = Stopwatch.StartNew();
xBuf.SetData( x );
wBuf.SetData( w );
if ( bias is not null )
{
bBuf.SetData( bias );
}
swUp.Stop();
shader.Attributes.Set( "X", xBuf );
shader.Attributes.Set( "W", wBuf );
shader.Attributes.Set( "Bias", bBuf );
shader.Attributes.Set( "Y", yBuf );
shader.Attributes.Set( "RowCount", rows );
shader.Attributes.Set( "InDim", inDim );
shader.Attributes.Set( "OutDim", outDim );
shader.Attributes.Set( "HasBias", bias is not null ? 1 : 0 );
var swDisp = Stopwatch.StartNew();
shader.Dispatch( rows, outDim, 1 );
swDisp.Stop();
var y = new float[rows * outDim];
var swRb = Stopwatch.StartNew();
yBuf.GetData( y, 0, y.Length );
swRb.Stop();
uploadMs = swUp.Elapsed.TotalMilliseconds;
dispatchMs = swDisp.Elapsed.TotalMilliseconds;
readbackMs = swRb.Elapsed.TotalMilliseconds;
return y;
}
// ---- 8A.14/8A.15/8A.16: real GLiNER dimensions + timing split ----------------
private async Task GateGemmRealDims()
{
var shader = GlinerGpuShaderLoader.Create( "gliner_gemm.shader" );
await GemmRealDimCase( shader, 40, 384, 384, true, 4001, "gemm_s40_384_384" );
await GemmRealDimCase( shader, 40, 384, 1536, true, 4002, "gemm_s40_384_1536" );
await GemmRealDimCase( shader, 40, 1536, 384, true, 4003, "gemm_s40_1536_384" );
await GemmRealDimCase( shader, 128, 384, 384, true, 4004, "gemm_s128_384_384" );
}
private async Task GemmRealDimCase( ComputeShader shader, int rows, int inDim, int outDim, bool withBias, uint seed, string name )
{
float[] x = LcgArray( rows * inDim, seed );
float[] w = LcgArray( outDim * inDim, seed + 1 );
float[] b = LcgArray( outDim, seed + 2 );
// CPU oracle on the worker thread (comparable to the P4 measurements)
var cpuRun = await Task.RunInThreadAsync( () =>
{
var sw = Stopwatch.StartNew();
float[] y = GlinerPoc.Neural.GlinerMath.Linear( x, rows, w, withBias ? b : null, inDim, outDim );
return (y, sw.Elapsed.TotalMilliseconds);
} );
// persistent buffers so the resident-buffer timing measures reuse
using var xBuf = new GpuBuffer<float>( x.Length );
using var wBuf = new GpuBuffer<float>( w.Length );
using var bBuf = new GpuBuffer<float>( b.Length );
using var yBuf = new GpuBuffer<float>( rows * outDim );
var swUp = Stopwatch.StartNew();
xBuf.SetData( x );
wBuf.SetData( w );
bBuf.SetData( b );
swUp.Stop();
shader.Attributes.Set( "X", xBuf );
shader.Attributes.Set( "W", wBuf );
shader.Attributes.Set( "Bias", bBuf );
shader.Attributes.Set( "Y", yBuf );
shader.Attributes.Set( "RowCount", rows );
shader.Attributes.Set( "InDim", inDim );
shader.Attributes.Set( "OutDim", outDim );
shader.Attributes.Set( "HasBias", withBias ? 1 : 0 );
var swDisp = Stopwatch.StartNew();
shader.Dispatch( rows, outDim, 1 );
swDisp.Stop();
var gpu = new float[rows * outDim];
var swRb = Stopwatch.StartNew();
yBuf.GetData( gpu, 0, gpu.Length );
swRb.Stop();
var m = GlinerPoc.Neural.GlinerMath.Compare( gpu, cpuRun.y );
// resident pass: buffers already on the GPU; measure dispatch+readback
// cycles with NO re-upload, plus enqueue-only dispatches
var sink = new float[rows * outDim];
var swResident = Stopwatch.StartNew();
const int Iter = 10;
for ( int i = 0; i < Iter; i++ )
{
shader.Dispatch( rows, outDim, 1 );
yBuf.GetData( sink, 0, sink.Length );
}
swResident.Stop();
var swEnq = Stopwatch.StartNew();
for ( int i = 0; i < Iter; i++ )
{
shader.Dispatch( rows, outDim, 1 );
}
yBuf.GetData( sink, 0, sink.Length );
swEnq.Stop();
Log.Info( $"[GLI:GPUP] TIMING {name} cpuMs={cpuRun.Item2:N2} uploadMs={swUp.Elapsed.TotalMilliseconds:N2} " +
$"dispatchMs={swDisp.Elapsed.TotalMilliseconds:N3} readbackMs={swRb.Elapsed.TotalMilliseconds:N2} " +
$"residentCycleMs={swResident.Elapsed.TotalMilliseconds / Iter:N2} residentEnqueueMs={swEnq.Elapsed.TotalMilliseconds / Iter:N3}" );
Gate( name, m.Pass,
$"maxAbs={m.MaxAbs:0.###e-00} meanAbs={m.MeanAbs:0.###e-00} maxRel={m.MaxRel:0.###e-00} worst={m.WorstIndex} | " +
$"cpuMs={cpuRun.Item2:N2} upMs={swUp.Elapsed.TotalMilliseconds:N2} dispMs={swDisp.Elapsed.TotalMilliseconds:N3} rbMs={swRb.Elapsed.TotalMilliseconds:N2} " +
$"residentCycleMs={swResident.Elapsed.TotalMilliseconds / Iter:N2}", "" );
}
// ---- 8A.17/8A.18: real packaged tensor through GPU GEMM -----------------------
private async Task GateGemmRealWeights()
{
GlinerPoc.Neural.GlinerModelWeights weights =
await Task.RunInThreadAsync( () => GlinerPoc.Neural.GlinerModelWeights.FromResource( ModelResource ) );
int seq = GlinerPoc.Neural.NeuralP4FixturesV2.EmbeddingCaseSeqLen; // 40
float[] embOut = DecodeB64( GlinerPoc.Neural.NeuralP4FixturesV2.EmbeddingStageOutput );
float[] wq = weights.GetTensor( "encoder.encoder.layer.0.attention.self.query_proj.weight" );
float[] bq = weights.GetTensor( "encoder.encoder.layer.0.attention.self.query_proj.bias" );
var cpuRun = await Task.RunInThreadAsync( () =>
{
var sw = Stopwatch.StartNew();
float[] y = GlinerPoc.Neural.GlinerMath.Linear( embOut, seq, wq, bq, 384, 384 );
return (y, sw.Elapsed.TotalMilliseconds);
} );
var shader = GlinerGpuShaderLoader.Create( "gliner_gemm.shader" );
float[] gpu = RunGemm( shader, embOut, wq, bq, seq, 384, 384,
out double upMs, out double dispMs, out double rbMs );
var mGpuVsCpu = GlinerPoc.Neural.GlinerMath.Compare( gpu, cpuRun.y );
// secondary: GPU vs the Python oracle fixture directly
float[] pythonRef = DecodeB64( GlinerPoc.Neural.NeuralP4FixturesV2.Layer0QueryProjOutput );
var mGpuVsPython = GlinerPoc.Neural.GlinerMath.Compare( gpu, pythonRef );
Gate( "gemm_real_layer0_query", mGpuVsCpu.Pass && mGpuVsPython.Pass,
$"packaged query_proj W[384,384]+b, X=real embedding output [{seq},384] | " +
$"GPUvsCPU maxAbs={mGpuVsCpu.MaxAbs:0.###e-00} meanAbs={mGpuVsCpu.MeanAbs:0.###e-00} | " +
$"GPUvsPython maxAbs={mGpuVsPython.MaxAbs:0.###e-00} | cpuMs={cpuRun.Item2:N2} upMs={upMs:N2} dispMs={dispMs:N3} rbMs={rbMs:N2}", "" );
}
// ---- 8A.28: intermediates stay on GPU across dispatches ------------------------
private async Task GateChainNoReadback()
{
GlinerPoc.Neural.GlinerModelWeights weights =
await Task.RunInThreadAsync( () => GlinerPoc.Neural.GlinerModelWeights.FromResource( ModelResource ) );
int seq = GlinerPoc.Neural.NeuralP4FixturesV2.EmbeddingCaseSeqLen;
float[] embOut = DecodeB64( GlinerPoc.Neural.NeuralP4FixturesV2.EmbeddingStageOutput );
float[] wq = weights.GetTensor( "encoder.encoder.layer.0.attention.self.query_proj.weight" );
float[] bq = weights.GetTensor( "encoder.encoder.layer.0.attention.self.query_proj.bias" );
float[] wUp = weights.GetTensor( "encoder.encoder.layer.0.intermediate.dense.weight" );
float[] bUp = weights.GetTensor( "encoder.encoder.layer.0.intermediate.dense.bias" );
var cpuRun = await Task.RunInThreadAsync( () =>
{
// CPU reference of the exact chained computation
float[] q = GlinerPoc.Neural.GlinerMath.Linear( embOut, seq, wq, bq, 384, 384 );
return GlinerPoc.Neural.GlinerMath.Linear( q, seq, wUp, bUp, 384, 1536 );
} );
var shader = GlinerGpuShaderLoader.Create( "gliner_gemm.shader" );
using var xBuf = new GpuBuffer<float>( seq * 384 );
using var wqBuf = new GpuBuffer<float>( wq.Length );
using var bqBuf = new GpuBuffer<float>( bq.Length );
using var qBuf = new GpuBuffer<float>( seq * 384 );
using var wUpBuf = new GpuBuffer<float>( wUp.Length );
using var bUpBuf = new GpuBuffer<float>( bUp.Length );
using var yBuf = new GpuBuffer<float>( seq * 1536 );
xBuf.SetData( embOut );
wqBuf.SetData( wq );
bqBuf.SetData( bq );
wUpBuf.SetData( wUp );
bUpBuf.SetData( bUp );
// kernel 1: Q = X·Wqᵀ+b, output into qBuf
shader.Attributes.Set( "X", xBuf );
shader.Attributes.Set( "W", wqBuf );
shader.Attributes.Set( "Bias", bqBuf );
shader.Attributes.Set( "Y", qBuf );
shader.Attributes.Set( "RowCount", seq );
shader.Attributes.Set( "InDim", 384 );
shader.Attributes.Set( "OutDim", 384 );
shader.Attributes.Set( "HasBias", 1 );
shader.Dispatch( seq, 384, 1 );
// kernel 2: Y = qBuf·Wupᵀ+b — qBuf (written by kernel 1) read as input,
// no CPU round trip between the two dispatches
shader.Attributes.Set( "X", qBuf );
shader.Attributes.Set( "W", wUpBuf );
shader.Attributes.Set( "Bias", bUpBuf );
shader.Attributes.Set( "Y", yBuf );
shader.Attributes.Set( "RowCount", seq );
shader.Attributes.Set( "InDim", 384 );
shader.Attributes.Set( "OutDim", 1536 );
shader.Attributes.Set( "HasBias", 1 );
shader.Dispatch( seq, 1536, 1 );
var gpu = new float[seq * 1536];
yBuf.GetData( gpu, 0, gpu.Length );
var m = GlinerPoc.Neural.GlinerMath.Compare( gpu, cpuRun );
Gate( "gpu_chain_no_readback", m.Pass,
$"GEMM→GEMM chained on GPU (Q buffer written then read), single final readback | " +
$"maxAbs={m.MaxAbs:0.###e-00} meanAbs={m.MeanAbs:0.###e-00} worst={m.WorstIndex}", "" );
}
// ---- 8A.19: LayerNorm ------------------------------------------------------------
private async Task GateLayerNormSynthetic()
{
var shader = GlinerGpuShaderLoader.Create( "gliner_layernorm.shader" );
float[] lnW = DecodeB64( GlinerPoc.Neural.NeuralP4FixturesV2.LayerNormWeightB64 );
float[] lnB = DecodeB64( GlinerPoc.Neural.NeuralP4FixturesV2.LayerNormBiasB64 );
bool all = true;
foreach ( var kc in GlinerPoc.Neural.NeuralP4FixturesV2.KernelCases )
{
if ( !kc.Name.StartsWith( "layernorm" ) )
{
continue;
}
int rows = kc.Shape[0];
int dim = kc.Shape[1];
float[] cpu = GlinerPoc.Neural.GlinerMath.LayerNorm( kc.Input, rows, dim, lnW, lnB, 1e-7f );
float[] gpu = RunLayerNorm( shader, kc.Input, rows, dim, lnW, lnB, 1e-7f );
var m = GlinerPoc.Neural.GlinerMath.Compare( gpu, cpu );
bool pass = m.Pass;
if ( kc.Name.Contains( "near_constant" ) )
{
// the numerically sharp case: require tight absolute closeness
pass &= m.MaxAbs < 1e-5;
}
Gate( $"gpu_{kc.Name}", pass,
$"[{rows},{dim}] maxAbs={m.MaxAbs:0.###e-00} meanAbs={m.MeanAbs:0.###e-00} worst={m.WorstIndex}", "" );
all &= pass;
}
Gate( "gpu_layernorm_synthetic", all, "all synthetic LN cases (normal/tiny/large/near-constant) vs CPU", "" );
await Task.Yield();
}
private static float[] RunLayerNorm( ComputeShader shader, float[] x, int rows, int dim,
float[] weight, float[] bias, float eps )
{
using var xBuf = new GpuBuffer<float>( x.Length );
using var wBuf = new GpuBuffer<float>( weight.Length );
using var bBuf = new GpuBuffer<float>( bias.Length );
using var yBuf = new GpuBuffer<float>( x.Length );
xBuf.SetData( x );
wBuf.SetData( weight );
bBuf.SetData( bias );
shader.Attributes.Set( "X", xBuf );
shader.Attributes.Set( "LnWeight", wBuf );
shader.Attributes.Set( "LnBias", bBuf );
shader.Attributes.Set( "Y", yBuf );
shader.Attributes.Set( "RowCount", rows );
shader.Attributes.Set( "Dim", dim );
shader.Attributes.Set( "Eps", eps );
shader.Dispatch( rows, 1, 1 );
var y = new float[x.Length];
yBuf.GetData( y, 0, y.Length );
return y;
}
private async Task GateLayerNormReal()
{
GlinerPoc.Neural.GlinerModelWeights weights =
await Task.RunInThreadAsync( () => GlinerPoc.Neural.GlinerModelWeights.FromResource( ModelResource ) );
float[] raw = weights.GetTensor( "encoder.encoder.rel_embeddings.weight" );
float[] w = weights.GetTensor( "encoder.encoder.LayerNorm.weight" );
float[] b = weights.GetTensor( "encoder.encoder.LayerNorm.bias" );
var cpuRun = await Task.RunInThreadAsync( () =>
{
var sw = Stopwatch.StartNew();
float[] y = GlinerPoc.Neural.GlinerMath.LayerNorm( raw, 512, 384, w, b, 1e-7f );
return (y, sw.Elapsed.TotalMilliseconds);
} );
var shader = GlinerGpuShaderLoader.Create( "gliner_layernorm.shader" );
var swGpu = Stopwatch.StartNew();
float[] gpu = RunLayerNorm( shader, raw, 512, 384, w, b, 1e-7f );
swGpu.Stop();
var mGpuVsCpu = GlinerPoc.Neural.GlinerMath.Compare( gpu, cpuRun.y );
float[] pythonRef = DecodeB64( GlinerPoc.Neural.NeuralP4FixturesV2.NormalizedRelEmbeddings );
var mGpuVsPython = GlinerPoc.Neural.GlinerMath.Compare( gpu, pythonRef );
Gate( "gpu_layernorm_real_rel", mGpuVsCpu.Pass && mGpuVsPython.Pass,
$"real rel_embeddings 512x384 | GPUvsCPU maxAbs={mGpuVsCpu.MaxAbs:0.###e-00} | " +
$"GPUvsPython maxAbs={mGpuVsPython.MaxAbs:0.###e-00} | cpuMs={cpuRun.Item2:N2} gpuCycleMs={swGpu.Elapsed.TotalMilliseconds:N2}", "" );
}
// ---- 8A.21: GELU ------------------------------------------------------------------
private async Task GateGelu()
{
var shader = GlinerGpuShaderLoader.Create( "gliner_gelu.shader" );
// synthetic: P4 fixture input
var geluCase = Array.Find( GlinerPoc.Neural.NeuralP4FixturesV2.KernelCases, k => k.Name == "gelu_erf" );
bool synPass = false;
if ( geluCase is not null )
{
float[] cpu = GlinerPoc.Neural.GlinerMath.GeluErf( geluCase.Input );
float[] gpu = RunGelu( shader, geluCase.Input );
var m = GlinerPoc.Neural.GlinerMath.Compare( gpu, cpu );
synPass = m.Pass;
Gate( "gpu_gelu_synthetic", synPass,
$"n={geluCase.Input.Length} maxAbs={m.MaxAbs:0.###e-00} meanAbs={m.MeanAbs:0.###e-00} worst={m.WorstIndex}", "" );
}
// real: layer-0 FFN pre-activation [40,1536]
GlinerPoc.Neural.GlinerModelWeights weights =
await Task.RunInThreadAsync( () => GlinerPoc.Neural.GlinerModelWeights.FromResource( ModelResource ) );
_ = weights;
float[] pre = DecodeB64( GlinerPoc.Neural.NeuralP4FixturesV2.Layer0IntermediatePreGelu );
var cpuRun = await Task.RunInThreadAsync( () => GlinerPoc.Neural.GlinerMath.GeluErf( pre ) );
float[] gpuReal = RunGelu( shader, pre );
var mReal = GlinerPoc.Neural.GlinerMath.Compare( gpuReal, cpuRun );
float[] pythonRef = DecodeB64( GlinerPoc.Neural.NeuralP4FixturesV2.Layer0GeluOutput );
var mGpuVsPython = GlinerPoc.Neural.GlinerMath.Compare( gpuReal, pythonRef );
Gate( "gpu_gelu_real_ffn", mReal.Pass && mGpuVsPython.Pass,
$"[40,1536] | GPUvsCPU maxAbs={mReal.MaxAbs:0.###e-00} | GPUvsPython maxAbs={mGpuVsPython.MaxAbs:0.###e-00}", "" );
}
private static float[] RunGelu( ComputeShader shader, float[] x )
{
int n = x.Length;
using var inBuf = new GpuBuffer<float>( n );
using var outBuf = new GpuBuffer<float>( n );
inBuf.SetData( x );
shader.Attributes.Set( "InputValues", inBuf );
shader.Attributes.Set( "OutputValues", outBuf );
shader.Attributes.Set( "ValueCount", n );
shader.Dispatch( n, 1, 1 );
var y = new float[n];
outBuf.GetData( y, 0, n );
return y;
}
// ---- 8A.25: layer-0 multi-tensor residency -----------------------------------------
private async Task GateLayer0Residency()
{
GlinerPoc.Neural.GlinerModelWeights weights =
await Task.RunInThreadAsync( () => GlinerPoc.Neural.GlinerModelWeights.FromResource( ModelResource ) );
const string p = "encoder.encoder.layer.0.";
float[] wq = weights.GetTensor( p + "attention.self.query_proj.weight" );
float[] bq = weights.GetTensor( p + "attention.self.query_proj.bias" );
float[] wUp = weights.GetTensor( p + "intermediate.dense.weight" );
float[] bUp = weights.GetTensor( p + "intermediate.dense.bias" );
float[] wDown = weights.GetTensor( p + "output.dense.weight" );
float[] bDown = weights.GetTensor( p + "output.dense.bias" );
int seq = 40;
var shader = GlinerGpuShaderLoader.Create( "gliner_gemm.shader" );
using var xBuf = new GpuBuffer<float>( seq * 384 );
using var wqBuf = new GpuBuffer<float>( wq.Length );
using var bqBuf = new GpuBuffer<float>( bq.Length );
using var qBuf = new GpuBuffer<float>( seq * 384 );
using var wUpBuf = new GpuBuffer<float>( wUp.Length );
using var bUpBuf = new GpuBuffer<float>( bUp.Length );
using var upBuf = new GpuBuffer<float>( seq * 1536 );
using var wDownBuf = new GpuBuffer<float>( wDown.Length );
using var bDownBuf = new GpuBuffer<float>( bDown.Length );
using var downBuf = new GpuBuffer<float>( seq * 384 );
// ONE upload of every weight tensor; they stay resident for all rounds
var swUpload = Stopwatch.StartNew();
wqBuf.SetData( wq );
bqBuf.SetData( bq );
wUpBuf.SetData( wUp );
bUpBuf.SetData( bUp );
wDownBuf.SetData( wDown );
bDownBuf.SetData( bDown );
swUpload.Stop();
bool allPass = true;
int rounds = 5;
for ( int round = 0; round < rounds; round++ )
{
float[] x = LcgArray( seq * 384, (uint)(5000 + round) );
var cpuRun = await Task.RunInThreadAsync( () =>
{
float[] q = GlinerPoc.Neural.GlinerMath.Linear( x, seq, wq, bq, 384, 384 );
float[] up = GlinerPoc.Neural.GlinerMath.Linear( q, seq, wUp, bUp, 384, 1536 );
float[] down = GlinerPoc.Neural.GlinerMath.Linear( up, seq, wDown, bDown, 1536, 384 );
return down;
} );
xBuf.SetData( x ); // only the activation input is re-uploaded
shader.Attributes.Set( "X", xBuf );
shader.Attributes.Set( "W", wqBuf );
shader.Attributes.Set( "Bias", bqBuf );
shader.Attributes.Set( "Y", qBuf );
shader.Attributes.Set( "RowCount", seq );
shader.Attributes.Set( "InDim", 384 );
shader.Attributes.Set( "OutDim", 384 );
shader.Attributes.Set( "HasBias", 1 );
shader.Dispatch( seq, 384, 1 );
shader.Attributes.Set( "X", qBuf );
shader.Attributes.Set( "W", wUpBuf );
shader.Attributes.Set( "Bias", bUpBuf );
shader.Attributes.Set( "Y", upBuf );
shader.Attributes.Set( "InDim", 384 );
shader.Attributes.Set( "OutDim", 1536 );
shader.Dispatch( seq, 1536, 1 );
shader.Attributes.Set( "X", upBuf );
shader.Attributes.Set( "W", wDownBuf );
shader.Attributes.Set( "Bias", bDownBuf );
shader.Attributes.Set( "Y", downBuf );
shader.Attributes.Set( "InDim", 1536 );
shader.Attributes.Set( "OutDim", 384 );
shader.Dispatch( seq, 384, 1 );
var gpu = new float[seq * 384];
downBuf.GetData( gpu, 0, gpu.Length );
var m = GlinerPoc.Neural.GlinerMath.Compare( gpu, cpuRun );
if ( !m.Pass )
{
allPass = false;
Log.Error( $"[GLI:GPUP] residency round {round} FAILED maxAbs={m.MaxAbs:0.###e-00} worst={m.WorstIndex}" );
}
await Task.Delay( 16 ); // across frames
}
Gate( "gpu_layer0_residency", allPass,
$"6 real weight buffers (~{((wq.Length + wUp.Length + wDown.Length) * 4 + (bq.Length + bUp.Length + bDown.Length) * 4) / 1024:N0} KiB) " +
$"uploaded once, {rounds} rounds × 3 chained GEMMs, only X re-uploaded; weightUpload={swUpload.Elapsed.TotalMilliseconds:N2}ms", "" );
}
// ---- 8A.23: GPU FP32 memory arithmetic from the manifest ---------------------------
private void GateMemoryEstimate()
{
var manifest = GlinerPoc.Packaging.Sbgli1Manifest.Parse( ModelResource.MetadataData.Bytes );
long weightBytes = 0;
foreach ( var t in manifest.Tensors )
{
weightBytes += t.ByteCount;
}
// worst-case activation/scratch arithmetic at S=256 (V1 budget), FP32:
// hidden/x/q/k/v/out ~ 6 × [256,384] = 2.25 MiB
// attention score families ~ 4 × [6,256,256] = 6.0 MiB
// rel pos layers 2 × [512,384] = 1.5 MiB
// FFN intermediate 2 × [256,1536] = 3.0 MiB
// + readback staging [256,384] = 0.375 MiB
long activationsS256 = (6L * 256 * 384 + 4L * 6 * 256 * 256 + 2L * 512 * 384 + 2L * 256 * 1536 + 256 * 384) * 4;
long total = weightBytes + activationsS256;
Log.Info( $"[GLI:GPUP] VRAM arithmetic: weights={weightBytes / (1024.0 * 1024):N1} MiB " +
$"activations(≤S=256 est)={activationsS256 / (1024.0 * 1024):N1} MiB total={total / (1024.0 * 1024):N1} MiB" );
try
{
ulong budget = Graphics.VideoMemoryBudget;
if ( budget > 0 )
{
Log.Info( $"[GLI:GPUP] VRAM budget={budget / (1024 * 1024):N0} MiB -> model would use {100.0 * total / budget:0.0}% of budget" );
}
else
{
Log.Info( "[GLI:GPUP] VRAM budget reports 0 (not exposed in this context)" );
}
}
catch ( Exception e )
{
Log.Info( $"[GLI:GPUP] VRAM budget unavailable: {e.GetType().Name}" );
}
Gate( "gpu_memory_estimate", weightBytes == 283_777_540L,
$"manifest classification weights {weightBytes:N0} B matches P02 packaged payload", "" );
}
// ---- helpers ------------------------------------------------------------------------
private static float[] DecodeB64( string b64 )
{
byte[] bytes = Convert.FromBase64String( b64 );
return GlinerPoc.Neural.GlinerMath.DecodeF32( bytes, 0, bytes.Length / 4 );
}
private void Gate( string name, bool pass, string detail, string extra )
{
if ( pass )
{
_passed++;
Log.Info( $"[GLI:GPUP] PASS {name} {detail} {extra}" );
}
else
{
_failed++;
Log.Error( $"[GLI:GPUP] FAIL {name} {detail} {extra}" );
}
}
}