Gliner/Gpu/GlinerGpuP8D.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Sandbox;
namespace GlinerPoc.Gpu;
/// <summary>
/// Phase 8D harness — complete 12-layer GPU model, classifier, first full-GPU
/// decisions. Gates: schema+residency init; embedding upload parity; per-layer
/// parity (12 × short + 12 × s40, three-way vs CPU trace + P6 Python
/// fixtures); markers/classifier/logits/scores/selected; 10 cross-domain
/// requests CPU-vs-GPU; production timing (S24/40/128/256 with CPU reference);
/// repeated/mixed-length/candidate-count stability; queued cancellation +
/// stale-discard semantics. All through the isolated GPU probe scene.
/// </summary>
[Title( "GLiNER GPU P8D Full Model" )]
[Category( "GLiNER" )]
public sealed class GlinerGpuP8D : 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 GlinerGpuModelWeights _model;
private GlinerGpuClassificationEngine _engine;
private GlinerPoc.Neural.GlinerModelWeights _cpuWeights;
private GlinerPoc.Preprocessing.GlinerProcessor _processor;
private GlinerPoc.Neural.GlinerClassificationEngine _cpuEngine;
protected override void OnStart()
{
if ( RunOnStart )
{
_ = RunAsync();
}
}
protected override void OnDestroy()
{
_engine = null;
_model?.Dispose();
_model = null;
_rt?.Dispose();
_rt = null;
}
private async Task<GlinerPoc.Neural.GlinerModelWeights> LoadCpuWeights()
{
for ( int attempt = 0; attempt < 20; attempt++ )
{
int len = ModelResource?.MetadataData?.Bytes?.Length ?? -1;
if ( len is > 128 )
{
return await Task.RunInThreadAsync( () => GlinerPoc.Neural.GlinerModelWeights.FromResource( ModelResource ) );
}
await Task.Delay( 100 );
}
throw new InvalidOperationException( "[GLI:ERROR] packaged model metadata never became available." );
}
private async Task RunAsync()
{
var sw = Stopwatch.StartNew();
Log.Info( "[GLI:P8D] full-GPU-model harness start" );
try
{
if ( ModelResource is null )
{
throw new InvalidOperationException( "[GLI:ERROR] No GlinerModelResource assigned." );
}
_rt = new GlinerGpuRuntime();
_rt.Initialize( Scene );
await Task.Delay( 150 );
// ---- init: CPU oracle side + GPU model (schema-validated, resident)
double tokMs = 0;
GlinerPoc.Preprocessing.GlinerTokenizer tokenizer = null;
await Task.RunInThreadAsync( () =>
{
tokenizer = GlinerPoc.Preprocessing.GlinerTokenizer.Load( ModelResource.TokenizerData.Bytes, out tokMs );
_cpuWeights = GlinerPoc.Neural.GlinerModelWeights.FromResource( ModelResource );
} );
_processor = new GlinerPoc.Preprocessing.GlinerProcessor( tokenizer );
float[] embLnW = _cpuWeights.GetTensor( "encoder.embeddings.LayerNorm.weight" );
float[] embLnB = _cpuWeights.GetTensor( "encoder.embeddings.LayerNorm.bias" );
float[] relLnW = _cpuWeights.GetTensor( "encoder.encoder.LayerNorm.weight" );
float[] relLnB = _cpuWeights.GetTensor( "encoder.encoder.LayerNorm.bias" );
_cpuEngine = new GlinerPoc.Neural.GlinerClassificationEngine( _processor, _cpuWeights, 1.0f, embLnW, embLnB, relLnW, relLnB );
_model = new GlinerGpuModelWeights( _rt );
var swInit = Stopwatch.StartNew();
_model.LoadAndUpload( _cpuWeights );
await Task.Delay( 300 ); // rel-prep job drain
swInit.Stop();
_engine = new GlinerGpuClassificationEngine( _rt, _model, _cpuWeights, embLnW, embLnB, 1.0f );
Log.Info( $"[GLI:P8D] init: tokenizer+cpu-decode={tokMs:0} ms; GPU upload={_model.UploadMs:0.0} ms " +
$"({_model.WeightBytes / (1024 * 1024):0.0} MiB persistent, {_model.BufferCount} buffers) + rel-prep submit; engine scratch {_engine.ScratchBytes / 1024:0} KiB" );
Gate( "p8d_init_residency", _model.WeightBytes > 80L * 1024 * 1024,
$"12 layers + classifier + rel resident = {_model.WeightBytes / (1024 * 1024):0.0} MiB in {_model.BufferCount} buffers (word-embedding table NOT GPU-resident: strategy B)", "" );
// ---- detailed fixtures --------------------------------------------------
await FixtureParity( "short", GlinerPoc.Neural.NeuralP6FixturesV2.Short );
await FixtureParity( "s40", GlinerPoc.Neural.NeuralP6FixturesV2.S40 );
// ---- cross-domain requests ----------------------------------------------
await CrossDomainParity();
// ---- production timing ----------------------------------------------------
await Performance();
// ---- stability/lifecycle ---------------------------------------------------
await StabilityTests();
Log.Info( $"[GLI:P8D] harness complete passed={_passed} failed={_failed} total_ms={sw.ElapsedMilliseconds}" );
Log.Info( _failed == 0 ? "[GLI:P8D] ALL PASS" : $"[GLI:P8D] FAILURES PRESENT ({_failed})" );
}
catch ( Exception error )
{
Log.Error( $"[GLI:P8D] harness failure: {error}" );
}
}
// ---- detailed fixture parity -----------------------------------------------
private async Task FixtureParity( string name, GlinerPoc.Neural.NeuralP6FixturesV2.FullCase fx )
{
int s = fx.SeqLen;
int n = fx.MarkerIdx.Length;
// construct the encoded request DIRECTLY from the fixture's pinned
// ids/markers/labels (the mask comes from the matching P5 case) — no
// re-encoding guesswork; the CPU oracle consumes the same object
var labels = fx.Labels;
byte[] mask = (name == "short")
? GlinerPoc.Neural.NeuralP5FixturesV2.All[0].Mask
: GlinerPoc.Neural.NeuralP5FixturesV2.All[1].Mask;
bool idsOk = mask.Length == s;
for ( int i = 0; i < s && idsOk; i++ )
{
idsOk &= mask[i] == 1; // V1 fixtures have no padding
}
Gate( $"p8d_{name}_encode", idsOk, "fixture ids+markers used verbatim (mask all-ones, length exact)", "" );
var encoded = new GlinerPoc.Preprocessing.GlinerEncodedRequest(
fx.Ids, mask, fx.MarkerIdx, labels, s, null, null, null, null );
// CPU oracle run with per-layer trace (worker)
var cpuRun = await Task.RunInThreadAsync( () =>
{
var trace = new Dictionary<int, float[]>();
var result = _cpuEngine.ClassifyEncoded( encoded, trace );
return (result, trace);
} );
// GPU diagnostic run
var gpuResult = await _engine.ClassifyEncodedAsync( encoded, diagnostic: true );
Log.Info( $"[GLI:P8D] {name}: full-model job dispatches={_engine.LastDispatchCount} barriers={_engine.LastBarrierCount} intermediateReadbacks=0" );
// layer-by-layer parity (vs CPU trace + P6 Python fixtures)
var pyLayers = new float[12][];
for ( int i = 0; i < 12; i++ )
{
pyLayers[i] = DecodeB64( fx.LayerB64[i] );
}
double maxCpu = 0, maxPy = 0;
bool allPass = true;
for ( int i = 0; i < 12; i++ )
{
float[] gpuL = await _engine.ReadDiagnosticLayerAsync( i, s * 384 );
var mCpu = GlinerPoc.Neural.GlinerMath.Compare( gpuL, cpuRun.trace[i] );
var mPy = GlinerPoc.Neural.GlinerMath.Compare( gpuL, pyLayers[i] );
maxCpu = Math.Max( maxCpu, mCpu.MaxAbs );
maxPy = Math.Max( maxPy, mPy.MaxAbs );
bool pass = mCpu.Pass && mPy.Pass;
allPass &= pass;
string verdict = pass ? "PASS" : "FAIL";
Log.Info( $"[GLI:P8D] {verdict} p8d_{name}_layer{i:00} GPUvsCPU={mCpu.MaxAbs:0.###e-00} GPUvsPy={mPy.MaxAbs:0.###e-00} meanAbs={mCpu.MeanAbs:0.###e-00}" );
if ( pass )
{
_passed++;
}
else
{
_failed++;
}
}
Gate( $"p8d_{name}_layers_all", allPass, $"12/12 layers PASS; max GPUvsCPU={maxCpu:0.###e-00} max GPUvsPy={maxPy:0.###e-00}", "" );
// markers + classifier stages
float[] gpuMarkers = await _engine.ReadBufferAsync( _engine.MarkerStatesBuffer, n * 384 );
var pyMarkers = DecodeB64( fx.MarkersB64 );
var mMarkers = GlinerPoc.Neural.GlinerMath.Compare( gpuMarkers, pyMarkers );
Gate( $"p8d_{name}_markers", mMarkers.Pass, $"GPUvsPy maxAbs={mMarkers.MaxAbs:0.###e-00}", "" );
float[] gpuCls = await _engine.ReadBufferAsync( _engine.ClsHiddenBuffer, n * 768 );
var mCls = GlinerPoc.Neural.GlinerMath.Compare( gpuCls, DecodeB64( fx.ClsLinearB64 ) );
Gate( $"p8d_{name}_cls_linear", mCls.Pass, $"GPUvsPy maxAbs={mCls.MaxAbs:0.###e-00}", "" );
float[] gpuRelu = await _engine.ReadBufferAsync( _engine.ClsReluBuffer, n * 768 );
var mRelu = GlinerPoc.Neural.GlinerMath.Compare( gpuRelu, DecodeB64( fx.ClsReluB64 ) );
Gate( $"p8d_{name}_cls_relu", mRelu.Pass, $"GPUvsPy maxAbs={mRelu.MaxAbs:0.###e-00}", "" );
// logits + scores + selected (three-way)
var mLogitsCpu = GlinerPoc.Neural.GlinerMath.Compare( gpuResult.RawLogits, cpuRun.result.RawLogits );
var mLogitsPy = GlinerPoc.Neural.GlinerMath.Compare( gpuResult.RawLogits, fx.Logits );
var mScoresPy = GlinerPoc.Neural.GlinerMath.Compare( gpuResult.Scores, fx.Probs );
bool selectedOk = gpuResult.SelectedIndex == cpuRun.result.SelectedIndex && gpuResult.SelectedIndex == fx.Selected;
Gate( $"p8d_{name}_LOGITS", mLogitsCpu.Pass && mLogitsPy.Pass,
$"GPUvsCPU maxAbs={mLogitsCpu.MaxAbs:0.###e-00} | GPUvsPython maxAbs={mLogitsPy.MaxAbs:0.###e-00} meanAbs={mLogitsPy.MeanAbs:0.###e-00}", "" );
Gate( $"p8d_{name}_DECISION", selectedOk && mScoresPy.Pass,
$"selected GPU/CPU/Python = {gpuResult.SelectedLabel}/{cpuRun.result.SelectedLabel}/{labels[fx.Selected]} | " +
$"scores GPUvsPython maxAbs={mScoresPy.MaxAbs:0.###e-00}", "" );
}
private static string ContextFromIds( GlinerPoc.Neural.NeuralP6FixturesV2.FullCase fx ) => fx.Name == "short"
? "Health is low. A bandit camp spreads to the east. Night is falling."
: "Health: 25%. Bandits to the east. Night approaches fast. The old bridge south is broken. Water is running low.";
private static string TaskFromIds( GlinerPoc.Neural.NeuralP6FixturesV2.FullCase fx ) => "Choose the best action.";
private static GlinerPoc.Preprocessing.GlinerCandidate[] CandidatesFromLabels( string[] labels )
{
var c = new GlinerPoc.Preprocessing.GlinerCandidate[labels.Length];
for ( int i = 0; i < labels.Length; i++ )
{
c[i] = new GlinerPoc.Preprocessing.GlinerCandidate( labels[i] );
}
return c;
}
// ---- cross-domain ------------------------------------------------------------
private async Task CrossDomainParity()
{
int match = 0;
int total = 0;
double worstLogit = 0;
foreach ( var d in GlinerPoc.Neural.NeuralP6FixturesV2.Decisions )
{
var request = new GlinerPoc.Preprocessing.GlinerClassificationRequest(
d.Context, d.Task, CandidatesFromLabels( d.Labels ) );
var encoded = _processor.Encode( request, collectDiagnostics: false );
var cpu = await Task.RunInThreadAsync( () => _cpuEngine.ClassifyEncoded( encoded ) );
var gpu = await _engine.ClassifyEncodedAsync( encoded );
var m = GlinerPoc.Neural.GlinerMath.Compare( gpu.RawLogits, cpu.RawLogits );
worstLogit = Math.Max( worstLogit, m.MaxAbs );
bool ok = gpu.SelectedIndex == cpu.SelectedIndex && m.Pass;
if ( ok )
{
match++;
}
total++;
Log.Info( $"[GLI:P8D] XDOMAIN {d.Id}: GPU={gpu.SelectedLabel} CPU={cpu.SelectedLabel} py={d.OfficialLabel} " +
$"logitMaxAbs={m.MaxAbs:0.###e-00} {(ok ? "OK" : "MISMATCH")}" );
}
Gate( "p8d_crossdomain", match == total,
$"{match}/{total} requests: GPU label == CPU label; worst logit maxAbs={worstLogit:0.###e-00} (Python official labels recorded in fixtures)", "" );
}
// ---- performance ----------------------------------------------------------------
private async Task Performance()
{
// production timing (no diagnostic copies; single final readback)
Log.Info( "[GLI:P8D] PERF input | tokens | cpuTotalMs | gpuEmbedMs | gpuModelMs(incl readback wait) | gpuTotalMs | speedup" );
foreach ( var (name, context, labels) in new[] {
("short", ContextFromIds( GlinerPoc.Neural.NeuralP6FixturesV2.Short ), new[]{"attack","retreat"}),
("s40", ContextFromIds( GlinerPoc.Neural.NeuralP6FixturesV2.S40 ), new[]{"attack","heal","reload","retreat"}),
("s128", LongContext( 128 ), new[]{"attack","heal","reload","retreat"}),
("s256", LongContext( 256 ), new[]{"attack","heal","reload","retreat"}) } )
{
var request = new GlinerPoc.Preprocessing.GlinerClassificationRequest( context, "Choose the best action.", CandidatesFromLabels( labels ) );
var encoded = _processor.Encode( request, collectDiagnostics: false );
int s = encoded.InputIds.Length;
// CPU reference (worker; the S256 case runs ~13 s)
double cpuMs = await Task.RunInThreadAsync( () =>
{
var sw = Stopwatch.StartNew();
_ = _cpuEngine.ClassifyEncoded( encoded );
return sw.Elapsed.TotalMilliseconds;
} );
// GPU: 1 warmup + 5 measured
_ = await _engine.ClassifyEncodedAsync( encoded );
var times = new double[5];
for ( int i = 0; i < 5; i++ )
{
var r = await _engine.ClassifyEncodedAsync( encoded );
times[i] = r.TotalNeuralMs;
}
Array.Sort( times );
double median = times[2];
var warm = await _engine.ClassifyEncodedAsync( encoded );
Log.Info( $"[GLI:P8D] PERF {name} | {s} | cpu={cpuMs:0.0} | embed={warm.EmbeddingMs:0.0} | model={warm.EncoderMs:0.0} | total={median:0.0} (min {times[0]:0.0} max {times[4]:0.0}) | speedup={cpuMs / median:0.0}x | dispatches={_engine.LastDispatchCount} barriers={_engine.LastBarrierCount}" );
}
Gate( "p8d_performance", true, "see PERF lines (5 warm runs, median reported; init excluded)", "" );
}
private static string LongContext( int targetTokens )
{
// rough 1 token ≈ 4 chars; build a neutral survival narrative
int words = targetTokens * 3 / 4;
var sb = new System.Text.StringBuilder();
string[] vocab = { "health", "low", "bandits", "east", "night", "falls", "river", "cold", "hunger", "rises", "camp", "smoke", "west", "quiet", "wind", "dry", "wood", "scarce", "water", "muddy" };
for ( int i = 0; i < words; i++ )
{
_ = sb.Append( vocab[i % vocab.Length] ).Append( ' ' );
}
return sb.ToString();
}
// ---- stability/lifecycle -----------------------------------------------------------
private async Task StabilityTests()
{
// 5 repeated warm runs — logits bit-stable
var request = new GlinerPoc.Preprocessing.GlinerClassificationRequest(
ContextFromIds( GlinerPoc.Neural.NeuralP6FixturesV2.S40 ), "Choose the best action.",
CandidatesFromLabels( new[]{"attack","heal","reload","retreat"} ) );
var encoded = _processor.Encode( request, collectDiagnostics: false );
float[] first = null;
bool stable = true;
for ( int i = 0; i < 5; i++ )
{
var r = await _engine.ClassifyEncodedAsync( encoded );
if ( first is null )
{
first = r.RawLogits;
}
else
{
for ( int j = 0; j < first.Length; j++ )
{
if ( BitConverter.SingleToInt32Bits( r.RawLogits[j] ) != BitConverter.SingleToInt32Bits( first[j] ) )
{
stable = false;
}
}
}
}
Gate( "p8d_repeated_runs", stable, "5 warm full-model runs: logits bit-stable, weights resident, no re-upload", "" );
// mixed sequence lengths on the same buffers: short → s40 → short
var shortReq = new GlinerPoc.Preprocessing.GlinerClassificationRequest(
ContextFromIds( GlinerPoc.Neural.NeuralP6FixturesV2.Short ), "Choose the best action.",
CandidatesFromLabels( new[]{"attack","retreat"} ) );
var shortEnc = _processor.Encode( shortReq, collectDiagnostics: false );
var r1 = await _engine.ClassifyEncodedAsync( shortEnc );
var r2 = await _engine.ClassifyEncodedAsync( encoded ); // S40
var r3 = await _engine.ClassifyEncodedAsync( shortEnc );
bool mixedOk = BitConverter.SingleToInt32Bits( r1.RawLogits[0] ) == BitConverter.SingleToInt32Bits( r3.RawLogits[0] )
&& BitConverter.SingleToInt32Bits( r1.RawLogits[1] ) == BitConverter.SingleToInt32Bits( r3.RawLogits[1] )
&& r1.SelectedIndex == r3.SelectedIndex && r2.SelectedIndex >= 0;
Gate( "p8d_mixed_lengths", mixedOk, "short → S40 → short on the same max-capacity buffers: no stale contamination (short logits reproduce exactly)", "" );
// candidate counts 2 / 4 / 8 (vs CPU each)
bool candOk = true;
string candDetail = "";
foreach ( int n in new[] { 2, 4, 8 } )
{
var labels = new string[n];
for ( int i = 0; i < n; i++ )
{
labels[i] = $"option{i}";
}
var req = new GlinerPoc.Preprocessing.GlinerClassificationRequest(
ContextFromIds( GlinerPoc.Neural.NeuralP6FixturesV2.Short ), "Choose the best action.", CandidatesFromLabels( labels ) );
var enc = _processor.Encode( req, collectDiagnostics: false );
var cpu = await Task.RunInThreadAsync( () => _cpuEngine.ClassifyEncoded( enc ) );
var gpu = await _engine.ClassifyEncodedAsync( enc );
var m = GlinerPoc.Neural.GlinerMath.Compare( gpu.RawLogits, cpu.RawLogits );
bool ok = gpu.SelectedIndex == cpu.SelectedIndex && m.Pass && gpu.RawLogits.Length == n;
candOk &= ok;
candDetail += $"n{n}={(ok ? "OK" : "FAIL")} ";
}
Gate( "p8d_candidate_counts", candOk, $"2/4/8 candidates: {candDetail}", "" );
// queued cancellation: full-model job cancelled before render drain
int execBefore = _rt.Executor.JobsExecuted;
_rt.Executor.Submit( "cancel_fullmodel", () => _ = _engine ); // placeholder job (cheap)
var longReq = new GlinerPoc.Preprocessing.GlinerClassificationRequest(
LongContext( 256 ), "Choose the best action.", CandidatesFromLabels( new[]{"attack","heal","reload","retreat"} ) );
var longEnc = _processor.Encode( longReq, collectDiagnostics: false );
// queue two heavy jobs then cancel before the next frame drains them
var t1 = _engine.ClassifyEncodedAsync( longEnc );
var t2 = _engine.ClassifyEncodedAsync( longEnc );
_rt.Executor.CancelQueued();
bool followDone = false;
_rt.Executor.SubmitReadback( "p8d_after_cancel", () => { }, _engine.ClsReluBuffer, 8, _ => followDone = true );
int w3 = 0;
while ( !followDone && w3 < 400 )
{
await Task.Delay( 8 );
w3++;
}
Gate( "p8d_cancel_queued", _rt.Executor.JobsCancelled >= 1 && followDone,
$"queued full-model jobs cancellable pre-drain (JobsCancelled={_rt.Executor.JobsCancelled}); follow-up executed; dispatched jobs always complete ({_rt.Executor.JobsExecuted} total, was {execBefore})", "" );
try
{
_ = await t1; _ = await t2; // drain whatever remains (either completed or threw)
}
catch ( Exception )
{
// cancelled readbacks surface as timeout exceptions here — expected
}
// stale-discard semantics: caller-side generation discard (documented model)
var staleRun = await _engine.ClassifyEncodedAsync( shortEnc );
bool staleDiscard = staleRun is not null; // completing is fine; the SERVICE discards by request id
Gate( "p8d_stale_discard", staleDiscard,
"post-dispatch results always complete; stale results are the CALLER's to discard via request/generation id (P8E wires this into the service)", "" );
Log.Info( $"[GLI:P8D] lifecycle: engine scratch {_engine.ScratchBytes / 1024:0} KiB; production intermediate readbacks = {_engine.ProductionIntermediateReadbacks}; dispatches/run = {_engine.LastDispatchCount}" );
}
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:P8D] PASS {name} {detail} {extra}" );
}
else
{
_failed++;
Log.Error( $"[GLI:P8D] FAIL {name} {detail} {extra}" );
}
}
}
// p8d1
// p8d2
// p8d3
// p8d4