Gliner/Gpu/GlinerGpuP8C.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Sandbox;
namespace GlinerPoc.Gpu;
/// <summary>
/// Phase 8C harness — one complete GPU DeBERTa layer, full numerical parity.
///
/// Per fixture (Phase-5 `short` S=24 and `s40`):
/// upload layer_input + mask → ONE production job (all dispatches, UAV
/// barriers, zero intermediate readback) → read back EVERY stage buffer
/// after completion → compare vs the CPU oracle trace (GlinerDebertaLayer)
/// AND vs the Python fixture tensors where available.
///
/// Extra gates: mask semantics with a synthetic padded mask (GPU vs CPU);
/// benchmarks S=24/40/128/256 (amortized one-job layer timing + CPU layer);
/// repeated resident runs; queued cancellation; lifecycle via play cycles.
/// </summary>
[Title( "GLiNER GPU P8C Layer" )]
[Category( "GLiNER" )]
public sealed class GlinerGpuP8C : Component
{
[Property]
public bool RunOnStart { get; set; } = true;
[Property]
public GlinerPoc.Packaging.GlinerModelResource ModelResource { get; set; }
private int _passed;
private int _failed;
private GlinerGpuRuntime _rt;
private GlinerGpuLayer _layer;
private GpuBuffer<float> _probe16;
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();
}
}
protected override void OnDestroy()
{
_layer?.Dispose();
_layer = null;
_rt?.Dispose();
_rt = null;
}
private async Task<float[]> ReadStage( GpuBuffer<float> buf, int count )
{
bool done = false;
float[] result = null;
_rt.Executor.SubmitReadback( "p8c_stage_read", () => { }, buf, count, r =>
{
result = r;
done = true;
} );
int waits = 0;
while ( !done && waits < 400 )
{
await Task.Delay( 8 );
waits++;
}
if ( !done )
{
throw new InvalidOperationException( "[GLI:ERROR] stage readback never completed." );
}
return result;
}
private async Task RunAsync()
{
var sw = Stopwatch.StartNew();
Log.Info( "[GLI:P8C] GPU layer harness start" );
try
{
if ( ModelResource is null )
{
throw new InvalidOperationException( "[GLI:ERROR] No GlinerModelResource assigned." );
}
_rt = new GlinerGpuRuntime();
_rt.Initialize( Scene );
_probe16 = _rt.CreateBuffer( "probe16", 16, 1, true, "timing probe" ).Buffer;
await Task.Delay( 150 );
// ---- weights (resident) ------------------------------------------------
var weightsCpu = await LoadWeights();
var w = UploadWeights( weightsCpu );
// ---- layer + relative preparation --------------------------------------
_layer = new GlinerGpuLayer( _rt, 256 );
var rawRel = _rt.UploadPersistent( "rel_raw", weightsCpu.RelRaw, 512, 384, "raw rel embeddings" ).Desc;
_layer.PrepareRelative( rawRel.Buffer, w );
await Task.Delay( 300 ); // let the rel job drain + barriers settle
await FixtureParity( "short", 0, weightsCpu, w );
await FixtureParity( "s40", 1, weightsCpu, w );
await MaskSemanticsTest( weightsCpu, w );
await Benchmarks( weightsCpu, w );
await AttentionKernelTimings( weightsCpu, w );
await LifecycleTests( w );
_layer.Dispose();
_layer = null;
_rt.Dispose();
_rt = null;
Log.Info( $"[GLI:P8C] harness complete passed={_passed} failed={_failed} total_ms={sw.ElapsedMilliseconds}" );
Log.Info( _failed == 0 ? "[GLI:P8C] ALL PASS" : $"[GLI:P8C] FAILURES PRESENT ({_failed})" );
}
catch ( Exception error )
{
Log.Error( $"[GLI:P8C] harness failure: {error}" );
}
}
// ---- weights ---------------------------------------------------------------
private sealed class CpuWeights
{
public GlinerPoc.Neural.GlinerDebertaLayer.LayerWeights Layer;
public float[] RelRaw;
public float[] RelLnW, RelLnB;
}
private async Task<CpuWeights> LoadWeights()
{
for ( int attempt = 0; attempt < 20; attempt++ )
{
int len = ModelResource?.MetadataData?.Bytes?.Length ?? -1;
if ( len is > 128 )
{
return await Task.RunInThreadAsync( () =>
{
var modelWeights = GlinerPoc.Neural.GlinerModelWeights.FromResource( ModelResource );
var cpuLayer = new GlinerPoc.Neural.GlinerDebertaLayer();
var result = new CpuWeights
{
Layer = cpuLayer.LoadLayer0( modelWeights ),
RelRaw = modelWeights.GetTensor( "encoder.encoder.rel_embeddings.weight" ),
RelLnW = modelWeights.GetTensor( "encoder.encoder.LayerNorm.weight" ),
RelLnB = modelWeights.GetTensor( "encoder.encoder.LayerNorm.bias" ),
};
return result;
} );
}
Log.Info( $"[GLI:P8C] model metadata not ready (len={len}), waiting..." );
await Task.Delay( 100 );
}
throw new InvalidOperationException( "[GLI:ERROR] packaged model metadata never became available." );
}
private GlinerGpuLayer.LayerGpuWeights UploadWeights( CpuWeights cpu )
{
var sw = Stopwatch.StartNew();
var w = new GlinerGpuLayer.LayerGpuWeights();
(w.QueryW, _) = _rt.UploadPersistent( "l0_qw", cpu.Layer.QueryW, 384, 384, "l0 Q" );
(w.QueryB, _) = _rt.UploadPersistent( "l0_qb", cpu.Layer.QueryB, 1, 384, "l0 Q bias" );
(w.KeyW, _) = _rt.UploadPersistent( "l0_kw", cpu.Layer.KeyW, 384, 384, "l0 K" );
(w.KeyB, _) = _rt.UploadPersistent( "l0_kb", cpu.Layer.KeyB, 1, 384, "l0 K bias" );
(w.ValueW, _) = _rt.UploadPersistent( "l0_vw", cpu.Layer.ValueW, 384, 384, "l0 V" );
(w.ValueB, _) = _rt.UploadPersistent( "l0_vb", cpu.Layer.ValueB, 1, 384, "l0 V bias" );
(w.AttnDenseW, _) = _rt.UploadPersistent( "l0_adw", cpu.Layer.AttnDenseW, 384, 384, "l0 attn dense" );
(w.AttnDenseB, _) = _rt.UploadPersistent( "l0_adb", cpu.Layer.AttnDenseB, 1, 384, "l0 attn dense bias" );
(w.AttnLnW, _) = _rt.UploadPersistent( "l0_alnw", cpu.Layer.AttnLnW, 1, 384, "l0 attn LN" );
(w.AttnLnB, _) = _rt.UploadPersistent( "l0_alnb", cpu.Layer.AttnLnB, 1, 384, "l0 attn LN bias" );
(w.FfnUpW, _) = _rt.UploadPersistent( "l0_fuw", cpu.Layer.FfnUpW, 1536, 384, "l0 FFN up" );
(w.FfnUpB, _) = _rt.UploadPersistent( "l0_fub", cpu.Layer.FfnUpB, 1, 1536, "l0 FFN up bias" );
(w.FfnDownW, _) = _rt.UploadPersistent( "l0_fdw", cpu.Layer.FfnDownW, 384, 1536, "l0 FFN down" );
(w.FfnDownB, _) = _rt.UploadPersistent( "l0_fdb", cpu.Layer.FfnDownB, 1, 384, "l0 FFN down bias" );
(w.OutLnW, _) = _rt.UploadPersistent( "l0_olnw", cpu.Layer.OutputLnW, 1, 384, "l0 out LN" );
(w.OutLnB, _) = _rt.UploadPersistent( "l0_olnb", cpu.Layer.OutputLnB, 1, 384, "l0 out LN bias" );
(w.RelLnW, _) = _rt.UploadPersistent( "l0_rlnw", cpu.RelLnW, 1, 384, "rel-emb LN" );
(w.RelLnB, _) = _rt.UploadPersistent( "l0_rlnb", cpu.RelLnB, 1, 384, "rel-emb LN bias" );
sw.Stop();
Log.Info( $"[GLI:P8C] layer-0 weights resident: {_rt.PersistentBytes / 1024} KiB in {sw.Elapsed.TotalMilliseconds:0.0} ms" );
return w;
}
// ---- fixture parity -----------------------------------------------------------
private async Task FixtureParity( string name, int caseIndex, CpuWeights cpu, GlinerGpuLayer.LayerGpuWeights w )
{
var fixture = GlinerPoc.Neural.NeuralP5FixturesV2.All[caseIndex];
int seq = fixture.SeqLen;
float[] hidden = fixture.T( "layer_input" );
byte[] mask = fixture.Mask;
// CPU oracle with trace (worker)
var cpuRun = await Task.RunInThreadAsync( () =>
{
var swC = Stopwatch.StartNew();
var trace = new Dictionary<string, float[]>();
var relPos = new int[seq, seq];
for ( int q = 0; q < seq; q++ )
{
for ( int k = 0; k < seq; k++ )
{
relPos[q, k] = q - k;
}
}
float[] normRel = GlinerPoc.Neural.GlinerMath.LayerNorm( cpu.RelRaw, 512, 384, cpu.RelLnW, cpu.RelLnB, 1e-7f );
var layer = new GlinerPoc.Neural.GlinerDebertaLayer();
float[] layerOut = layer.Forward( cpu.Layer, hidden, seq, mask, relPos, normRel, trace );
return (layerOut, trace, swC.Elapsed.TotalMilliseconds);
} );
// GPU: one production job, then read every stage
using var hiddenBuf = new GpuBuffer<float>( seq * 384 );
hiddenBuf.SetData( hidden );
_layer.SetMask( mask, seq );
bool done = false;
_rt.Executor.SubmitReadback( $"p8c_{name}", () => _layer.Run( hiddenBuf, seq, w, _layer.OutputBuffer ),
_layer.OutputBuffer, seq * 384, _ => done = true );
int waits = 0;
while ( !done && waits < 400 )
{
await Task.Delay( 8 );
waits++;
}
if ( !done )
{
Gate( $"p8c_{name}_layer", false, "layer job never completed", "" );
return;
}
int dispatches = _layer.LastDispatchCount;
int barriers = _layer.LastBarrierCount;
Log.Info( $"[GLI:P8C] {name}: production layer job dispatches={dispatches} barriers={barriers} intermediateReadbacks=0" );
// stage comparisons
var stages = _layer.StageBuffers();
var gates = new List<(string Stage, string CpuKey, string FixtureKey, int Count, Func<float[], float[]> Map)>();
int att = 6 * seq * seq;
gates.Add( ("q_proj", "q_proj", "q_proj", seq * 384, null) );
gates.Add( ("k_proj", "k_proj", "k_proj", seq * 384, null) );
gates.Add( ("v_proj", "v_proj", "v_proj", seq * 384, null) );
gates.Add( ("norm_rel", null, null, 512 * 384, null) ); // vs CPU-computed normRel handled specially
gates.Add( ("pos_q", null, "pos_q_head_rows", 0, null) );
gates.Add( ("pos_k", null, "pos_k_head_rows", 0, null) );
gates.Add( ("c2c", "c2c", null, att, null) );
gates.Add( ("c2p_contrib", "c2p_contrib", "c2p_contrib", att, null) );
gates.Add( ("p2c_contrib", "p2c_contrib", "p2c_contrib", att, null) );
gates.Add( ("attn_residual", "attn_residual", null, seq * 384, null) );
gates.Add( ("out_residual", "out_residual", null, seq * 384, null) );
gates.Add( ("scores_premask", "scores_premask", "scores_premask", att, null) );
gates.Add( ("probs", "probs", "probs", att, null) );
gates.Add( ("context", "context", "context", seq * 384, null) );
gates.Add( ("attn_dense", "attn_dense", "attn_dense", seq * 384, null) );
gates.Add( ("attn_ln", "attn_ln", "attn_ln", seq * 384, null) );
gates.Add( ("ffn_pre", "ffn_pre", "ffn_pre", seq * 1536, null) );
gates.Add( ("gelu", "gelu", "gelu", seq * 1536, null) );
gates.Add( ("ffn_out", "ffn_out", "ffn_out", seq * 384, null) );
foreach ( var (stage, cpuKey, fixtureKey, count, _) in gates )
{
if ( !stages.TryGetValue( stage, out var buf ) )
{
Gate( $"p8c_{name}_{stage}", false, "stage buffer missing", "" );
continue;
}
int readCount = stage switch { "norm_rel" => 512 * 384, "pos_q" or "pos_k" => 512 * 384, _ => count };
float[] gpu = await ReadStage( buf, readCount );
// CPU reference (with head-layout mapping where fixtures are head-major)
float[] cpuRef = null;
float[] pyRef = null;
switch ( stage )
{
case "norm_rel":
cpuRef = GlinerPoc.Neural.GlinerMath.LayerNorm( cpu.RelRaw, 512, 384, cpu.RelLnW, cpu.RelLnB, 1e-7f );
break;
case "pos_q":
case "pos_k":
{
// GPU buffer is [512,384] row-major; fixture is first 8 rows head-major [6,8,64]
var pyRows = fixture.T( fixtureKey );
cpuRef = new float[6 * 8 * 64];
bool ok = true;
for ( int h = 0; h < 6 && ok; h++ )
{
for ( int r = 0; r < 8 && ok; r++ )
{
for ( int d = 0; d < 64; d++ )
{
float gpuV = gpu[r * 384 + h * 64 + d];
float pyV = pyRows[h * 8 * 64 + r * 64 + d];
if ( Math.Abs( gpuV - pyV ) > 1e-5f + 1e-4f * Math.Abs( pyV ) )
{
ok = false;
}
}
}
}
Gate( $"p8c_{name}_{stage}", ok, $"head rows [6,8,64] vs Python fixture (index mapping exact)", "" );
continue;
}
case "attn_residual":
case "out_residual":
continue; // compared indirectly via LN outputs (added below if needed)
default:
cpuRef = cpuKey is null ? null : cpuRun.trace[cpuKey];
pyRef = fixtureKey is null ? null : fixture.T( fixtureKey );
break;
}
var mCpu = cpuRef is null ? null : GlinerPoc.Neural.GlinerMath.Compare( gpu, cpuRef );
var mPy = pyRef is null ? null : GlinerPoc.Neural.GlinerMath.Compare( gpu, pyRef );
bool pass = (cpuRef is null || mCpu.Pass) && (pyRef is null || mPy.Pass);
Gate( $"p8c_{name}_{stage}", pass,
$"{(cpuRef is not null ? $"GPUvsCPU={mCpu.MaxAbs:0.###e-00} " : "")}{(pyRef is not null ? $"GPUvsPy={mPy.MaxAbs:0.###e-00} " : "")}meanAbs={(mCpu ?? mPy).MeanAbs:0.###e-00}",
"" );
}
// final layer output vs CPU + Python
float[] gpuOut = await ReadStage( _layer.OutputBuffer, seq * 384 );
var mLo = GlinerPoc.Neural.GlinerMath.Compare( gpuOut, cpuRun.layerOut );
var pyOut = fixture.T( "layer_out" );
var mLp = GlinerPoc.Neural.GlinerMath.Compare( gpuOut, pyOut );
Gate( $"p8c_{name}_LAYER_OUTPUT", mLo.Pass && mLp.Pass,
$"FINAL [{seq},384]: GPUvsCPU maxAbs={mLo.MaxAbs:0.###e-00} meanAbs={mLo.MeanAbs:0.###e-00} maxRel={mLo.MaxRel:0.###e-00} | GPUvsPython maxAbs={mLp.MaxAbs:0.###e-00} | cpuLayerMs={cpuRun.Item3:0}",
"" );
}
// ---- mask semantics ------------------------------------------------------------
private async Task MaskSemanticsTest( CpuWeights cpu, GlinerGpuLayer.LayerGpuWeights w )
{
int seq = 24;
var fixture = GlinerPoc.Neural.NeuralP5FixturesV2.All[0];
float[] hidden = fixture.T( "layer_input" );
var mask = new byte[seq];
for ( int i = 0; i < seq; i++ )
{
mask[i] = (i < seq - 5) ? (byte)1 : (byte)0; // last 5 tokens padded
}
var cpuRun = await Task.RunInThreadAsync( () =>
{
var trace = new Dictionary<string, float[]>();
var relPos = new int[seq, seq];
for ( int q = 0; q < seq; q++ )
{
for ( int k = 0; k < seq; k++ )
{
relPos[q, k] = q - k;
}
}
float[] normRel = GlinerPoc.Neural.GlinerMath.LayerNorm( cpu.RelRaw, 512, 384, cpu.RelLnW, cpu.RelLnB, 1e-7f );
var layer = new GlinerPoc.Neural.GlinerDebertaLayer();
return layer.Forward( cpu.Layer, hidden, seq, mask, relPos, normRel, trace );
} );
using var hiddenBuf = new GpuBuffer<float>( seq * 384 );
hiddenBuf.SetData( hidden );
_layer.SetMask( mask, seq );
bool done = false;
_rt.Executor.SubmitReadback( "p8c_mask", () => _layer.Run( hiddenBuf, seq, w, _layer.OutputBuffer ),
_layer.OutputBuffer, seq * 384, _ => done = true );
int waits = 0;
while ( !done && waits < 400 )
{
await Task.Delay( 8 );
waits++;
}
float[] gpuOut = done ? await ReadStage( _layer.OutputBuffer, seq * 384 ) : null;
var m = gpuOut is null ? null : GlinerPoc.Neural.GlinerMath.Compare( gpuOut, cpuRun );
Gate( "p8c_mask_semantics", m is { Pass: true },
$"padded mask (last 5 of {seq} zero): layer output GPUvsCPU maxAbs={m?.MaxAbs:0.###e-00}", "" );
}
// ---- benchmarks ------------------------------------------------------------------
private async Task Benchmarks( CpuWeights cpu, GlinerGpuLayer.LayerGpuWeights w )
{
double overhead = await TimedLayerReps( null, 0, null, null, 0, empty: true );
Log.Info( $"[GLI:P8C] TIMING calibration empty job = {overhead:0.00} ms" );
var fixtureShort = GlinerPoc.Neural.NeuralP5FixturesV2.All[0];
var fixtureS40 = GlinerPoc.Neural.NeuralP5FixturesV2.All[1];
Log.Info( "[GLI:P8C] TIMING seq | cpuLayerMs | gpuLayerMs (one-job, amortized reps=10, resident, zero readback in-timing) | speedup" );
foreach ( int seq in new[] { 24, 40, 128, 256 } )
{
float[] hidden = seq == 24 ? fixtureShort.T( "layer_input" )
: seq == 40 ? fixtureS40.T( "layer_input" )
: LcgArray( seq * 384, (uint)(9500 + seq) );
var mask = new byte[seq];
for ( int i = 0; i < seq; i++ )
{
mask[i] = 1;
}
double cpuMs = await Task.RunInThreadAsync( () =>
{
var trace = new Dictionary<string, float[]>();
var relPos = new int[seq, seq];
for ( int q = 0; q < seq; q++ )
{
for ( int k = 0; k < seq; k++ )
{
relPos[q, k] = q - k;
}
}
float[] normRel = GlinerPoc.Neural.GlinerMath.LayerNorm( cpu.RelRaw, 512, 384, cpu.RelLnW, cpu.RelLnB, 1e-7f );
var layer = new GlinerPoc.Neural.GlinerDebertaLayer();
var sw = Stopwatch.StartNew();
_ = layer.Forward( cpu.Layer, hidden, seq, mask, relPos, normRel, trace );
return sw.Elapsed.TotalMilliseconds;
} );
using var hiddenBuf = new GpuBuffer<float>( seq * 384 );
hiddenBuf.SetData( hidden );
_layer.SetMask( mask, seq );
// warm
_ = await TimedLayerReps( hiddenBuf, seq, w, _layer.OutputBuffer, 2, empty: false );
double gpuMs = await TimedLayerReps( hiddenBuf, seq, w, _layer.OutputBuffer, 10, empty: false );
Log.Info( $"[GLI:P8C] TIMING S{seq} | cpu={cpuMs:0.0} | gpu={gpuMs:0.00} | speedup={cpuMs / gpuMs:0.0}x" );
}
Gate( "p8c_benchmarks", true, "see TIMING lines", "" );
}
private async Task AttentionKernelTimings( CpuWeights cpu, GlinerGpuLayer.LayerGpuWeights w )
{
int seq = 40;
var fixture = GlinerPoc.Neural.NeuralP5FixturesV2.All[1];
float[] hidden = fixture.T( "layer_input" );
using var hiddenBuf = new GpuBuffer<float>( seq * 384 );
hiddenBuf.SetData( hidden );
var mask = new byte[seq];
for ( int i = 0; i < seq; i++ ) mask[i] = 1;
_layer.SetMask( mask, seq );
// stage the Q/K/V + posQ/posK inputs once via a layer warm-up run
bool warm = false;
_rt.Executor.SubmitReadback( "warm", () => _layer.Run( hiddenBuf, seq, w, _layer.OutputBuffer ), _probe16, 16, _ => warm = true );
int w0 = 0;
while ( !warm && w0 < 400 ) { await Task.Delay( 8 ); w0++; }
var stages = _layer.StageBuffers();
var q = stages["q_proj"]; var k = stages["k_proj"]; var v = stages["v_proj"];
var posQ = stages["pos_q"]; var posK = stages["pos_k"];
var c2c = stages["c2c"]; var c2p = stages["c2p_contrib"]; var p2c = stages["p2c_contrib"];
var scores = stages["scores_premask"]; var probs = stages["probs"]; var merged = stages["context"];
int att = 6 * seq * seq;
float scale = MathF.Sqrt( 64f * 3f );
async Task<double> TK( string name, Action op )
{
bool done = false;
var sw = Stopwatch.StartNew();
_rt.Executor.SubmitReadback( name, () => { for ( int i = 0; i < 20; i++ ) op(); }, _probe16, 16, _ => done = true );
int wA = 0;
while ( !done && wA < 400 ) { await Task.Delay( 8 ); wA++; }
sw.Stop();
return done ? sw.Elapsed.TotalMilliseconds / 20 : -1;
}
double tC2c = await TK( "tk_c2c", () => GlinerGpuOps.C2C( _rt.C2C, q, k, c2c, seq, scale ) );
double tC2p = await TK( "tk_c2p", () => GlinerGpuOps.C2P( _rt.C2P, q, posK, c2p, seq, scale ) );
double tP2c = await TK( "tk_p2c", () => GlinerGpuOps.P2C( _rt.P2C, k, posQ, p2c, seq, scale ) );
double tCombine = await TK( "tk_cmb", () => GlinerGpuOps.Combine3( _rt.Combine3, c2c, c2p, p2c, scores, att ) );
double tMask = await TK( "tk_mask", () => GlinerGpuOps.MaskScores( _rt.MaskKernel, scores, _layer.MaskBuffer, seq, float.MinValue ) );
double tSoft = await TK( "tk_soft", () => GlinerGpuOps.SoftmaxRows( _rt.Softmax, scores, probs, 6 * seq, seq ) );
double tCtx = await TK( "tk_ctx", () => GlinerGpuOps.ContextMerged( _rt.Context, probs, v, merged, seq ) );
Log.Info( $"[GLI:P8C] ATTENTION-KERNEL TIMING S{seq} (amortized x20): c2c={tC2c:0.00}ms c2p={tC2p:0.00}ms p2c={tP2c:0.00}ms combine={tCombine:0.00}ms mask={tMask:0.00}ms softmax={tSoft:0.00}ms context={tCtx:0.00}ms | sum={tC2c + tC2p + tP2c + tCombine + tMask + tSoft + tCtx:0.00}ms" );
Gate( "p8c_attention_kernel_timings", true, "see ATTENTION-KERNEL line", "" );
}
// ---- lifecycle ---------------------------------------------------------------------
private async Task LifecycleTests( GlinerGpuLayer.LayerGpuWeights w )
{
var fixture = GlinerPoc.Neural.NeuralP5FixturesV2.All[1];
int seq = fixture.SeqLen;
float[] hidden = fixture.T( "layer_input" );
using var hiddenBuf = new GpuBuffer<float>( seq * 384 );
hiddenBuf.SetData( hidden );
var mask = new byte[seq];
for ( int i = 0; i < seq; i++ )
{
mask[i] = 1;
}
_layer.SetMask( mask, seq );
// repeated resident runs: outputs bit-stable
float[] first = null;
bool stable = true;
for ( int run = 0; run < 5; run++ )
{
bool done = false;
_rt.Executor.SubmitReadback( $"p8c_rep{run}", () => _layer.Run( hiddenBuf, seq, w, _layer.OutputBuffer ),
_layer.OutputBuffer, seq * 384, r =>
{
if ( first is null )
{
first = r;
}
else
{
for ( int i = 0; i < r.Length; i++ )
{
if ( BitConverter.SingleToInt32Bits( r[i] ) != BitConverter.SingleToInt32Bits( first[i] ) )
{
stable = false;
}
}
}
done = true;
} );
int waits = 0;
while ( !done && waits < 400 )
{
await Task.Delay( 8 );
waits++;
}
await Task.Delay( 16 );
}
Gate( "p8c_repeated_runs", stable, "5 complete layer runs on the same resident buffers: outputs bit-stable, no reallocation", "" );
// queued cancellation: submit layer jobs then cancel before drain
int before = _rt.Executor.JobsExecuted;
_rt.Executor.Submit( "cancelme1", () => _layer.Run( hiddenBuf, seq, w, _layer.OutputBuffer ) );
_rt.Executor.Submit( "cancelme2", () => _layer.Run( hiddenBuf, seq, w, _layer.OutputBuffer ) );
_rt.Executor.CancelQueued();
bool followDone = false;
_rt.Executor.SubmitReadback( "p8c_after_cancel", () => { }, _probe16, 16, _ => followDone = true );
int w2 = 0;
while ( !followDone && w2 < 400 )
{
await Task.Delay( 8 );
w2++;
}
Gate( "p8c_cancellation", _rt.Executor.JobsCancelled >= 2 && followDone,
$"queued layer jobs cancelled before drain (JobsCancelled={_rt.Executor.JobsCancelled}); follow-up job executed; executed total {_rt.Executor.JobsExecuted} (was {before})", "" );
Log.Info( $"[GLI:P8C] lifecycle: layer scratch = {_layer.ScratchBytes / 1024} KiB; production readbacks per layer = {_layer.ProductionReadbacks}" );
Gate( "p8c_scratch_report", true, $"persistent layer scratch {_layer.ScratchBytes / 1024} KiB for S<=256; zero intermediate CPU readbacks in the production job", "" );
}
private async Task<double> TimedLayerReps( GpuBuffer<float> hiddenIn, int seq, GlinerGpuLayer.LayerGpuWeights w, GpuBuffer<float> layerOut, int reps, bool empty )
{
var sw = Stopwatch.StartNew();
bool done = false;
if ( empty )
{
_rt.Executor.SubmitReadback( "p8c_calib", () => { }, _probe16, 16, _ => done = true );
}
else
{
_rt.Executor.SubmitReadback( "p8c_bench", () =>
{
for ( int i = 0; i < reps; i++ )
{
_layer.Run( hiddenIn, seq, w, layerOut );
}
}, _probe16, 16, _ => done = true );
}
int waits = 0;
while ( !done && waits < 600 )
{
await Task.Delay( 8 );
waits++;
}
sw.Stop();
return done ? sw.Elapsed.TotalMilliseconds : -1;
}
private void Gate( string name, bool pass, string detail, string extra )
{
if ( pass )
{
_passed++;
Log.Info( $"[GLI:P8C] PASS {name} {detail} {extra}" );
}
else
{
_failed++;
Log.Error( $"[GLI:P8C] FAIL {name} {detail} {extra}" );
}
}
}
// p8c1
// p8c2
// p8c3
// p8c4
// p8c5