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}" );
		}
	}
}