Gliner/Gpu/GlinerGpuModel.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Sandbox;

namespace GlinerPoc.Gpu;

/// <summary>
/// Phase 8D.2/8D.3 — model-level GPU weight owner. Validates the tensor
/// schema for ALL 12 encoder layers + classifier (mirroring CPU Phase 6),
/// uploads everything ONCE to persistent GpuBuffers, and prepares the shared
/// normalized relative table plus LAYER-SPECIFIC posQ/posK projections
/// (share_att_key: each layer projects normRel with ITS OWN Q/K weights).
/// The word-embedding table is NOT GPU-resident (P8B strategy B: the CPU
/// gathers the needed rows per request — ≤384 KiB upload).
/// </summary>
public sealed class GlinerGpuModelWeights : IDisposable
{
	private readonly GlinerGpuRuntime _rt;
	public GlinerGpuLayer.LayerGpuWeights[] Layers { get; } = new GlinerGpuLayer.LayerGpuWeights[12];
	public GlinerGpuBufferDesc Cls0W, Cls0B, Cls3W, Cls3B;
	public GlinerGpuBufferDesc RelLnW, RelLnB;
	/// <summary>Shared normalized relative table [512,384] (GPU-prepared).</summary>
	public GpuBuffer<float> NormRel;
	/// <summary>Per-layer positional states [512,384] each (layer-specific projections).</summary>
	public GpuBuffer<float>[] PosQ { get; } = new GpuBuffer<float>[12];
	public GpuBuffer<float>[] PosK { get; } = new GpuBuffer<float>[12];

	public long WeightBytes { get; private set; }
	public int BufferCount { get; private set; }
	public double UploadMs { get; private set; }
	public double PrepareMs { get; private set; }

	public GlinerGpuModelWeights( GlinerGpuRuntime rt ) => _rt = rt;

	private static readonly (string Suffix, int Rank, long Elements)[] LayerSchema = new[]
	{
		("attention.self.query_proj.weight", 2, 384L * 384),
		("attention.self.query_proj.bias", 1, 384),
		("attention.self.key_proj.weight", 2, 384L * 384),
		("attention.self.key_proj.bias", 1, 384),
		("attention.self.value_proj.weight", 2, 384L * 384),
		("attention.self.value_proj.bias", 1, 384),
		("attention.output.dense.weight", 2, 384L * 384),
		("attention.output.dense.bias", 1, 384),
		("attention.output.LayerNorm.weight", 1, 384),
		("attention.output.LayerNorm.bias", 1, 384),
		("intermediate.dense.weight", 2, 1536L * 384),
		("intermediate.dense.bias", 1, 1536),
		("output.dense.weight", 2, 384L * 1536),
		("output.dense.bias", 1, 384),
		("output.LayerNorm.weight", 1, 384),
		("output.LayerNorm.bias", 1, 384),
	};

	/// <summary>8D.1 schema validation + one-time upload of every model tensor.</summary>
	public void LoadAndUpload( GlinerPoc.Neural.GlinerModelWeights cpu )
	{
		var swTotal = Stopwatch.StartNew();
		var manifest = cpu.Manifest;

		void Validate( string name, int rank, long elements )
		{
			var rec = manifest.GetRequiredTensor( name );
			if ( rec.Rank != rank || rec.ElementCount != elements )
			{
				throw new InvalidOperationException(
					$"[GLI:ERROR] tensor '{name}' shape mismatch: rank {rec.Rank}({string.Join(',', rec.Shape)}) " +
					$"elements {rec.ElementCount}, expected rank {rank} elements {elements}" );
			}
		}

		// schema validation: all 12 layers + classifier + rel tensors
		for ( int i = 0; i < 12; i++ )
		{
			string p = $"encoder.encoder.layer.{i}.";
			foreach ( var (suffix, rank, elements) in LayerSchema )
			{
				Validate( p + suffix, rank, elements );
			}
		}
		Validate( "classifier.0.weight", 2, 768L * 384 );
		Validate( "classifier.0.bias", 1, 768 );
		Validate( "classifier.3.weight", 2, 768 );
		Validate( "classifier.3.bias", 1, 1 );
		Validate( "encoder.encoder.rel_embeddings.weight", 2, 512L * 384 );
		Validate( "encoder.encoder.LayerNorm.weight", 1, 384 );
		Validate( "encoder.encoder.LayerNorm.bias", 1, 384 );

		// upload (one-time; CPU-side staging comes from the CPU decode cache)
		var swUpload = Stopwatch.StartNew();
		for ( int i = 0; i < 12; i++ )
		{
			string p = $"encoder.encoder.layer.{i}.";
			var lw = new GlinerGpuLayer.LayerGpuWeights();
			(lw.QueryW, _) = _rt.UploadPersistent( $"l{i}_qw", cpu.GetTensor( p + LayerSchema[0].Suffix ), 384, 384, $"l{i} Q" );
			(lw.QueryB, _) = _rt.UploadPersistent( $"l{i}_qb", cpu.GetTensor( p + LayerSchema[1].Suffix ), 1, 384, $"l{i} Qb" );
			(lw.KeyW, _) = _rt.UploadPersistent( $"l{i}_kw", cpu.GetTensor( p + LayerSchema[2].Suffix ), 384, 384, $"l{i} K" );
			(lw.KeyB, _) = _rt.UploadPersistent( $"l{i}_kb", cpu.GetTensor( p + LayerSchema[3].Suffix ), 1, 384, $"l{i} Kb" );
			(lw.ValueW, _) = _rt.UploadPersistent( $"l{i}_vw", cpu.GetTensor( p + LayerSchema[4].Suffix ), 384, 384, $"l{i} V" );
			(lw.ValueB, _) = _rt.UploadPersistent( $"l{i}_vb", cpu.GetTensor( p + LayerSchema[5].Suffix ), 1, 384, $"l{i} Vb" );
			(lw.AttnDenseW, _) = _rt.UploadPersistent( $"l{i}_adw", cpu.GetTensor( p + LayerSchema[6].Suffix ), 384, 384, $"l{i} attn" );
			(lw.AttnDenseB, _) = _rt.UploadPersistent( $"l{i}_adb", cpu.GetTensor( p + LayerSchema[7].Suffix ), 1, 384, $"l{i} attn b" );
			(lw.AttnLnW, _) = _rt.UploadPersistent( $"l{i}_alnw", cpu.GetTensor( p + LayerSchema[8].Suffix ), 1, 384, $"l{i} attn LN" );
			(lw.AttnLnB, _) = _rt.UploadPersistent( $"l{i}_alnb", cpu.GetTensor( p + LayerSchema[9].Suffix ), 1, 384, $"l{i} attn LN b" );
			(lw.FfnUpW, _) = _rt.UploadPersistent( $"l{i}_fuw", cpu.GetTensor( p + LayerSchema[10].Suffix ), 1536, 384, $"l{i} FFN up" );
			(lw.FfnUpB, _) = _rt.UploadPersistent( $"l{i}_fub", cpu.GetTensor( p + LayerSchema[11].Suffix ), 1, 1536, $"l{i} FFN up b" );
			(lw.FfnDownW, _) = _rt.UploadPersistent( $"l{i}_fdw", cpu.GetTensor( p + LayerSchema[12].Suffix ), 384, 1536, $"l{i} FFN dn" );
			(lw.FfnDownB, _) = _rt.UploadPersistent( $"l{i}_fdb", cpu.GetTensor( p + LayerSchema[13].Suffix ), 1, 384, $"l{i} FFN dn b" );
			(lw.OutLnW, _) = _rt.UploadPersistent( $"l{i}_olnw", cpu.GetTensor( p + LayerSchema[14].Suffix ), 1, 384, $"l{i} out LN" );
			(lw.OutLnB, _) = _rt.UploadPersistent( $"l{i}_olnb", cpu.GetTensor( p + LayerSchema[15].Suffix ), 1, 384, $"l{i} out LN b" );
			Layers[i] = lw;
		}
		(Cls0W, _) = _rt.UploadPersistent( "cls0w", cpu.GetTensor( "classifier.0.weight" ), 768, 384, "classifier.0" );
		(Cls0B, _) = _rt.UploadPersistent( "cls0b", cpu.GetTensor( "classifier.0.bias" ), 1, 768, "classifier.0 b" );
		(Cls3W, _) = _rt.UploadPersistent( "cls3w", cpu.GetTensor( "classifier.3.weight" ), 1, 768, "classifier.3" );
		(Cls3B, _) = _rt.UploadPersistent( "cls3b", cpu.GetTensor( "classifier.3.bias" ), 1, 1, "classifier.3 b" );
		(RelLnW, _) = _rt.UploadPersistent( "rlnw", cpu.GetTensor( "encoder.encoder.LayerNorm.weight" ), 1, 384, "rel LN" );
		(RelLnB, _) = _rt.UploadPersistent( "rlnb", cpu.GetTensor( "encoder.encoder.LayerNorm.bias" ), 1, 384, "rel LN b" );
		swUpload.Stop();
		UploadMs = swUpload.Elapsed.TotalMilliseconds;

		// GPU-side relative preparation: shared normRel + 12 LAYER-SPECIFIC pos
		// projections (one executor job)
		var rawRelDesc = _rt.UploadPersistent( "relraw", cpu.GetTensor( "encoder.encoder.rel_embeddings.weight" ), 512, 384, "rel raw" ).Desc;
		var swPrep = Stopwatch.StartNew();
		NormRel = new GpuBuffer<float>( 512 * 384 );
		for ( int i = 0; i < 12; i++ )
		{
			PosQ[i] = new GpuBuffer<float>( 512 * 384 );
			PosK[i] = new GpuBuffer<float>( 512 * 384 );
		}
		_rt.Executor.Submit( "model_rel_prep", () =>
		{
			GlinerGpuOps.LayerNorm( _rt.LayerNorm, rawRelDesc.Buffer, RelLnW.Buffer, RelLnB.Buffer, NormRel, 512, 384, 1e-7f );
			Graphics.UavBarrier( NormRel );
			for ( int i = 0; i < 12; i++ )
			{
				GlinerGpuOps.LinearTiled16( _rt.GemmTiled16, NormRel, Layers[i].QueryW.Buffer, Layers[i].QueryB.Buffer, PosQ[i], 512, 384, 384, true );
				Graphics.UavBarrier( PosQ[i] );
				GlinerGpuOps.LinearTiled16( _rt.GemmTiled16, NormRel, Layers[i].KeyW.Buffer, Layers[i].KeyB.Buffer, PosK[i], 512, 384, 384, true );
				Graphics.UavBarrier( PosK[i] );
			}
		} );
		swPrep.Stop();
		PrepareMs = swPrep.Elapsed.TotalMilliseconds; // submit time; completion awaited by first use barriers

		WeightBytes = _rt.PersistentBytes;
		BufferCount = 0;
		foreach ( var kv in _rt.Buffers )
		{
			BufferCount++;
		}
		swTotal.Stop();
		_ = swTotal;
	}

	public void Dispose()
	{
		NormRel?.Dispose();
		for ( int i = 0; i < 12; i++ )
		{
			PosQ[i]?.Dispose();
			PosK[i]?.Dispose();
		}
	}
}

/// <summary>
/// Phase 8D.40/.41 — the GPU classification engine (P8E integration target).
/// Receives a GlinerEncodedRequest, performs the CPU embedding gather+LN
/// (strategy B — exact Phase-4 parity by construction), submits the COMPLETE
/// model as ONE render-context job (12 layers ping-pong → marker gather →
/// classifier), reads back ONLY the candidate logits, and applies the
/// validated CPU postprocessing (temperature → softmax → argmax). No Razor/
/// service dependencies; async, non-blocking; cancellation-before-dispatch
/// via the executor; stale results discarded by the caller's request id.
/// Backend identity: "Native GPU FP32".
/// </summary>
public sealed class GlinerGpuClassificationEngine
{
	public const string BackendName = "Native GPU FP32";

	private readonly GlinerGpuRuntime _rt;
	private readonly GlinerGpuModelWeights _model;
	private readonly GlinerGpuLayer _layer;
	private readonly GlinerPoc.Neural.GlinerModelWeights _cpuWeights;
	private readonly float[] _embLnW, _embLnB;
	private readonly float _temperature;

	// engine-owned persistent buffers (S ≤ 256, candidates ≤ 8)
	private readonly GpuBuffer<float> _hiddenA, _hiddenB;
	private readonly GpuBuffer<float> _markerStates, _clsHidden, _clsRelu, _logits;
	private readonly GpuBuffer<int> _markerIdx;
	/// <summary>Diagnostic per-layer snapshots (populated ONLY in diagnostic mode).</summary>
	private readonly GpuBuffer<float>[] _diagLayers = new GpuBuffer<float>[12];

	public int LastDispatchCount { get; private set; }
	public int LastBarrierCount { get; private set; }
	/// <summary>Intermediate CPU readbacks performed by production runs — always 0.</summary>
	public int ProductionIntermediateReadbacks => 0;
	public double InitializationMs => _model.UploadMs + _model.PrepareMs;
	public GlinerGpuModelWeights Model => _model;

	public GlinerGpuClassificationEngine( GlinerGpuRuntime rt, GlinerGpuModelWeights model,
		GlinerPoc.Neural.GlinerModelWeights cpuWeights, float[] embeddingLnW, float[] embeddingLnB,
		float temperature, int maxSeq = 256 )
	{
		_rt = rt;
		_model = model;
		_cpuWeights = cpuWeights;
		_embLnW = embeddingLnW;
		_embLnB = embeddingLnB;
		_temperature = temperature;
		_layer = new GlinerGpuLayer( rt, maxSeq );

		_hiddenA = new GpuBuffer<float>( maxSeq * 384 );
		_hiddenB = new GpuBuffer<float>( maxSeq * 384 );
		_markerStates = new GpuBuffer<float>( 8 * 384 );
		_clsHidden = new GpuBuffer<float>( 8 * 768 );
		_clsRelu = new GpuBuffer<float>( 8 * 768 );
		_logits = new GpuBuffer<float>( 8 );
		_markerIdx = new GpuBuffer<int>( 8 );
		for ( int i = 0; i < 12; i++ )
		{
			_diagLayers[i] = new GpuBuffer<float>( maxSeq * 384 );
		}
	}

	public long ScratchBytes =>
		(2L * 256 * 384 + 8L * 384 + 2L * 8 * 768 + 8L) * 4 + _layer.ScratchBytes
		+ 12L * 256 * 384 * 4 /* diagnostic snapshots */;

	/// <summary>
	/// Full GPU classification of an encoded request. Embedding prep runs on
	/// the CPU (exact oracle stage); everything from hidden upload to logits
	/// is ONE render job; only the n candidate logits cross back to the CPU.
	/// diagnostic=true additionally copies each layer output into snapshot
	/// buffers (read back afterwards via ReadDiagnostics — never in-line).
	/// </summary>
	public async Task<GlinerPoc.Neural.GlinerDecisionResult> ClassifyEncodedAsync(
		GlinerPoc.Preprocessing.GlinerEncodedRequest encoded, bool diagnostic = false,
		Func<bool> cancelled = null )
	{
		int s = encoded.InputIds.Length;
		int n = encoded.ClassificationMarkerIndices.Length;
		var swAll = Stopwatch.StartNew();

		// ---- CPU embedding stage (strategy B; exact Phase-4 parity) ----
		var swEmb = Stopwatch.StartNew();
		float[] hidden;
		float[] gathered = new float[s * 384];
		for ( int i = 0; i < s; i++ )
		{
			float[] row = _cpuWeights.GetRows( "encoder.embeddings.word_embeddings.weight", encoded.InputIds[i], 1 );
			for ( int j = 0; j < 384; j++ )
			{
				gathered[i * 384 + j] = row[j];
			}
		}
		hidden = GlinerPoc.Neural.GlinerMath.LayerNorm( gathered, s, 384, _embLnW, _embLnB, 1e-7f );
		swEmb.Stop();

		// ---- uploads (request-local only; weights/pos already resident) ----
		var swUp = Stopwatch.StartNew();
		GpuBuffer<float> hiddenIn = _hiddenA;
		hiddenIn.SetData( hidden, 0 );
		var mask = new uint[s];
		for ( int i = 0; i < s; i++ )
		{
			mask[i] = encoded.AttentionMask[i] != 0 ? 1u : 0u;
		}
		_layer.SetMask( encoded.AttentionMask, s );
		var idx = new int[n];
		for ( int i = 0; i < n; i++ )
		{
			idx[i] = encoded.ClassificationMarkerIndices[i];
		}
		_markerIdx.SetData( idx );
		swUp.Stop();

		// ---- ONE job: 12 layers + marker gather + classifier ----
		bool done = false;
		float[] logitsOut = null;
		var swGpu = Stopwatch.StartNew();
		_rt.Executor.SubmitReadback( "p8d_full", () =>
		{
			int d = 0, b = 0;
			GpuBuffer<float> finalHidden = null;
			for ( int i = 0; i < 12; i++ )
			{
				GpuBuffer<float> xin = (i % 2 == 0) ? _hiddenA : _hiddenB;
				GpuBuffer<float> xout = (i % 2 == 0) ? _hiddenB : _hiddenA;
				_layer.Run( xin, s, _model.Layers[i], xout, _model.PosQ[i], _model.PosK[i] );
				d += _layer.LastDispatchCount;
				b += _layer.LastBarrierCount;
				// layer-boundary barrier: layer N's final LN write → layer N+1
				// Q/K/V reads (audited — internal layer barriers do NOT cover
				// the external consumer)
				Graphics.UavBarrier( xout );
				b++;
				if ( diagnostic )
				{
					GlinerGpuOps.Copy( _rt.Copy, xout, _diagLayers[i], s * 384 );
					d++;
				}
				finalHidden = xout;
			}
			// marker gather (candidate order preserved)
			GlinerGpuOps.GatherRows( _rt.Gather, finalHidden, _markerIdx, _markerStates, n, 384 );
			d++;
			Graphics.UavBarrier( _markerStates ); b++;
			// classifier 384→768 → ReLU → 768→1
			GlinerGpuOps.LinearTiled16( _rt.GemmTiled16, _markerStates, _model.Cls0W.Buffer, _model.Cls0B.Buffer, _clsHidden, n, 384, 768, true );
			d++;
			Graphics.UavBarrier( _clsHidden ); b++;
			GlinerGpuOps.Relu( _rt.Relu, _clsHidden, _clsRelu, n * 768 );
			d++;
			Graphics.UavBarrier( _clsRelu ); b++;
			GlinerGpuOps.LinearTiled16( _rt.GemmTiled16, _clsRelu, _model.Cls3W.Buffer, _model.Cls3B.Buffer, _logits, n, 768, 1, true );
			d++;
			LastDispatchCount = d;
			LastBarrierCount = b;
		}, _logits, n, r =>
		{
			logitsOut = r;
			done = true;
		} );
		int waits = 0;
		bool cancelSeen = false;
		while ( !done && waits < 1200 )
		{
			await Task.Delay( 8 );
			waits++;
			if ( cancelled is not null && cancelled() )
			{
				// GPU cancellation semantics (P8E.12): before the render job
				// starts it is DROPPED (CancelQueued); once dispatched it always
				// completes — either way this caller reports Cancelled and any
				// late completion is discarded upstream by request ownership.
				_rt.Executor.CancelQueued();
				cancelSeen = true;
				break;
			}
		}
		swGpu.Stop();
		if ( cancelSeen )
		{
			return null;
		}
		if ( !done )
		{
			throw new InvalidOperationException( "[GLI:ERROR] full-model job never completed." );
		}

		// ---- CPU postprocessing (the P8D.22 boundary) ----
		var swPost = Stopwatch.StartNew();
		float[] raw = new float[n];
		float[] scaled = new float[n];
		for ( int i = 0; i < n; i++ )
		{
			raw[i] = logitsOut[i];
			scaled[i] = raw[i] / _temperature;
		}
		float[] scores = Softmax1D( scaled );
		int best = 0;
		for ( int i = 1; i < n; i++ )
		{
			if ( scores[i] > scores[best] )
			{
				best = i;
			}
		}
		swPost.Stop();
		swAll.Stop();

		var labels = new string[n];
		for ( int i = 0; i < n; i++ )
		{
			labels[i] = encoded.CandidateOrder[i];
		}
		return new GlinerPoc.Neural.GlinerDecisionResult( labels, raw, scaled, scores, best, s )
		{
			EmbeddingMs = swEmb.Elapsed.TotalMilliseconds + swUp.Elapsed.TotalMilliseconds,
			EncoderMs = swGpu.Elapsed.TotalMilliseconds,
			ClassifierMs = swPost.Elapsed.TotalMilliseconds,
			TotalNeuralMs = swAll.Elapsed.TotalMilliseconds,
		};
	}

	/// <summary>Diagnostic readback of the per-layer snapshots (post-run only).</summary>
	public async Task<float[]> ReadDiagnosticLayerAsync( int layerIndex, int count )
	{
		bool done = false;
		float[] result = null;
		_rt.Executor.SubmitReadback( $"p8d_diag_{layerIndex}", () => { }, _diagLayers[layerIndex], count, r =>
		{
			result = r;
			done = true;
		} );
		int waits = 0;
		while ( !done && waits < 400 )
		{
			await Task.Delay( 8 );
			waits++;
		}
		return result;
	}

	/// <summary>Diagnostic readback of marker states / classifier hidden / relu buffers.</summary>
	public async Task<float[]> ReadBufferAsync( GpuBuffer<float> buffer, int count )
	{
		bool done = false;
		float[] result = null;
		_rt.Executor.SubmitReadback( "p8d_read", () => { }, buffer, count, r =>
		{
			result = r;
			done = true;
		} );
		int waits = 0;
		while ( !done && waits < 400 )
		{
			await Task.Delay( 8 );
			waits++;
		}
		return result;
	}

	public GpuBuffer<float> MarkerStatesBuffer => _markerStates;
	public GpuBuffer<float> ClsHiddenBuffer => _clsHidden;
	public GpuBuffer<float> ClsReluBuffer => _clsRelu;

	private static float[] Softmax1D( float[] x )
	{
		float max = x[0];
		for ( int i = 1; i < x.Length; i++ )
		{
			if ( x[i] > max )
			{
				max = x[i];
			}
		}
		var y = new float[x.Length];
		float sum = 0f;
		for ( int i = 0; i < x.Length; i++ )
		{
			float e = MathF.Exp( x[i] - max );
			y[i] = e;
			sum += e;
		}
		float inv = 1f / sum;
		for ( int i = 0; i < x.Length; i++ )
		{
			y[i] *= inv;
		}
		return y;
	}
}