Gliner/Neural/GlinerDebertaLayer.cs
using System;

namespace GlinerPoc.Neural;

/// <summary>
/// Phase 5 — one complete DeBERTa-v2 encoder layer, exact port of
/// transformers 4.57.6 DebertaV2Layer (DisentangledSelfAttention +
/// DebertaV2SelfOutput + DebertaV2Intermediate + DebertaV2Output) for THIS
/// checkpoint's active configuration: pos_att_type=[p2c, c2p],
/// share_att_key=true, relative_attention=true, position_buckets=256,
/// max_relative_positions=-1 (raw q−k relative positions, no bucketing).
///
/// Pinned behaviours (see ARCHITECTURE §17):
/// - transpose_for_scores: [S,384] → [heads=6, S, 64], layer[h,s,d] = x[s,h·64+d]
/// - scale_factor = 1 + c2p + p2c = 3; scale = √(64·3); the scale divides K
///   (and the c2p/p2c CONTRIBUTIONS, after the gather) — not Q, not the scores
/// - pos projections reuse the content query_proj/key_proj on the normalized
///   rel_embeddings rows [0:512]
/// - c2p gather index: clamp(rel[q][k]+256, 0, 511)
/// - p2c gather index: clamp(−rel[k][q]+256, 0, 511) = clamp(q−k+256, 0, 511)
///   (build_rpos returns relative_pos unchanged for equal seq dims)
/// - mask: [1,1,S,S] boolean outer product mask[q]·mask[k]; masked positions
///   filled with float32 minimum before softmax
/// - residuals are POST-norm: LN(dense(x) + residual)
/// All kernels out-of-place; inputs never mutated.
/// </summary>
public sealed class GlinerDebertaLayer
{
	public sealed class LayerWeights
	{
		public float[] QueryW, QueryB, KeyW, KeyB, ValueW, ValueB;
		public float[] AttnDenseW, AttnDenseB, AttnLnW, AttnLnB;
		public float[] FfnUpW, FfnUpB, FfnDownW, FfnDownB;
		public float[] OutputLnW, OutputLnB;
	}

	private readonly int _heads = 6;
	private readonly int _headDim = 64;
	private readonly int _hidden = 384;
	private readonly float _scale;
	private readonly int _attSpan = 256;

	public GlinerDebertaLayer()
	{
		_scale = MathF.Sqrt(_headDim * 3f); // scale_factor 1 + c2p + p2c
	}

	public LayerWeights LoadLayer0(GlinerModelWeights weights)
	{
		const string p = "encoder.encoder.layer.0.";
		return new LayerWeights
		{
			QueryW = weights.GetTensor(p + "attention.self.query_proj.weight"),
			QueryB = weights.GetTensor(p + "attention.self.query_proj.bias"),
			KeyW = weights.GetTensor(p + "attention.self.key_proj.weight"),
			KeyB = weights.GetTensor(p + "attention.self.key_proj.bias"),
			ValueW = weights.GetTensor(p + "attention.self.value_proj.weight"),
			ValueB = weights.GetTensor(p + "attention.self.value_proj.bias"),
			AttnDenseW = weights.GetTensor(p + "attention.output.dense.weight"),
			AttnDenseB = weights.GetTensor(p + "attention.output.dense.bias"),
			AttnLnW = weights.GetTensor(p + "attention.output.LayerNorm.weight"),
			AttnLnB = weights.GetTensor(p + "attention.output.LayerNorm.bias"),
			FfnUpW = weights.GetTensor(p + "intermediate.dense.weight"),
			FfnUpB = weights.GetTensor(p + "intermediate.dense.bias"),
			FfnDownW = weights.GetTensor(p + "output.dense.weight"),
			FfnDownB = weights.GetTensor(p + "output.dense.bias"),
			OutputLnW = weights.GetTensor(p + "output.LayerNorm.weight"),
			OutputLnB = weights.GetTensor(p + "output.LayerNorm.bias"),
		};
	}

	/// <summary>[S,384] → [heads,S,64]: layer[h,s,d] = x[s,h·64+d].</summary>
	public void TransposeForScores(float[] x, int seq, float[] outHeads)
	{
		for ( int s = 0; s < seq; s++ )
		{
			for ( int h = 0; h < _heads; h++ )
			{
				int src = s * _hidden + h * _headDim;
				int dst = h * seq * _headDim + s * _headDim;
				for ( int d = 0; d < _headDim; d++ )
				{
					outHeads[dst + d] = x[src + d];
				}
			}
		}
	}

	/// <summary>
	/// Full layer-0 forward. hiddenStates [S,384]; attentionMask [S] (0/1);
	/// relPos [S,S] (raw q−k); normRel [512,384] (normalized rel embeddings).
	/// When trace is non-null, named intermediate stages are stored for parity
	/// gates. Returns the new hidden states [S,384].
	/// </summary>
	public float[] Forward(
		LayerWeights w,
		float[] hiddenStates,
		int seq,
		byte[] attentionMask,
		int[,] relPos,
		float[] normRel,
		Dictionary<string, float[]> trace = null,
		Dictionary<string, int[,]> intTrace = null )
	{
		int hCount = _heads;
		int hd = _headDim;

		// ---- Q/K/V projections + head split --------------------------------
		float[] qProj = GlinerMath.Linear( hiddenStates, seq, w.QueryW, w.QueryB, _hidden, _hidden );
		float[] kProj = GlinerMath.Linear( hiddenStates, seq, w.KeyW, w.KeyB, _hidden, _hidden );
		float[] vProj = GlinerMath.Linear( hiddenStates, seq, w.ValueW, w.ValueB, _hidden, _hidden );
		trace?.Add( "q_proj", qProj );
		trace?.Add( "k_proj", kProj );
		trace?.Add( "v_proj", vProj );
		float[] qh = new float[hCount * seq * hd];
		float[] kh = new float[hCount * seq * hd];
		float[] vh = new float[hCount * seq * hd];
		TransposeForScores( qProj, seq, qh );
		TransposeForScores( kProj, seq, kh );
		TransposeForScores( vProj, seq, vh );
		trace?.Add( "q_layer", qh );
		trace?.Add( "k_layer", kh );
		trace?.Add( "v_layer", vh );

		// ---- pos projections on normalized rel embeddings [0:512] -----------
		int relRows = _attSpan * 2;
		float[] posQProj = GlinerMath.Linear( normRel, relRows, w.QueryW, w.QueryB, _hidden, _hidden );
		float[] posKProj = GlinerMath.Linear( normRel, relRows, w.KeyW, w.KeyB, _hidden, _hidden );
		float[] posQ = new float[hCount * relRows * hd];
		float[] posK = new float[hCount * relRows * hd];
		TransposeForScores( posQProj, relRows, posQ );
		TransposeForScores( posKProj, relRows, posK );
		if ( trace is not null )
		{
			trace["pos_q_head_rows"] = new float[hCount * 8 * hd];
			trace["pos_k_head_rows"] = new float[hCount * 8 * hd];
			for ( int h = 0; h < hCount; h++ )
			{
				for ( int r = 0; r < 8; r++ )
				{
					for ( int d = 0; d < hd; d++ )
					{
						trace["pos_q_head_rows"][h * 8 * hd + r * hd + d] = posQ[h * relRows * hd + r * hd + d];
						trace["pos_k_head_rows"][h * 8 * hd + r * hd + d] = posK[h * relRows * hd + r * hd + d];
					}
				}
			}
		}

		// ---- c2c: Q · (Kᵀ / scale) — scale divides K BEFORE the product -----
		float[] c2c = new float[hCount * seq * seq];
		for ( int h = 0; h < hCount; h++ )
		{
			for ( int qq = 0; qq < seq; qq++ )
			{
				for ( int kk = 0; kk < seq; kk++ )
				{
					float sum = 0f;
					int qBase = h * seq * hd + qq * hd;
					int kBase = h * seq * hd + kk * hd;
					for ( int d = 0; d < hd; d++ )
					{
						sum += qh[qBase + d] * (kh[kBase + d] / _scale);
					}
					c2c[h * seq * seq + qq * seq + kk] = sum;
				}
			}
		}

		// ---- c2p / p2c -------------------------------------------------------
		float[] c2pContrib = new float[hCount * seq * seq];
		float[] p2cContrib = new float[hCount * seq * seq];
		for ( int h = 0; h < hCount; h++ )
		{
			for ( int q = 0; q < seq; q++ )
			{
				for ( int k = 0; k < seq; k++ )
				{
					int idx = Math.Clamp( relPos[q, k] + _attSpan, 0, relRows - 1 );
					float c2p = 0f;
					int qBase = h * seq * hd + q * hd;
					int pkBase = h * relRows * hd + idx * hd;
					for ( int d = 0; d < hd; d++ )
					{
						c2p += qh[qBase + d] * posK[pkBase + d];
					}
					c2pContrib[h * seq * seq + q * seq + k] = c2p / _scale;

					// p2c: p2c_att[h, k, r] = k_layer·posQ; gather at
					// clamp(−rel[k][q]+256) = clamp(q−k+256); result transposed.
					int pidx = Math.Clamp( relPos[k, q] * -1 + _attSpan, 0, relRows - 1 );
					float p2c = 0f;
					int kBase = h * seq * hd + k * hd;
					int pqBase = h * relRows * hd + pidx * hd;
					for ( int d = 0; d < hd; d++ )
					{
						p2c += kh[kBase + d] * posQ[pqBase + d];
					}
					p2cContrib[h * seq * seq + q * seq + k] = p2c / _scale;
				}
			}
		}

		// ---- combine + mask + softmax ----------------------------------------
		// mask4[q,k] = mask[q]·mask[k] (oracle get_attention_mask outer product)
		var probs = new float[hCount * seq * seq];
		float[] c2cTrace = trace is not null ? new float[hCount * seq * seq] : null;
		float[] c2pTrace = trace is not null ? new float[hCount * seq * seq] : null;
		float[] p2cTrace = trace is not null ? new float[hCount * seq * seq] : null;
		for ( int h = 0; h < hCount; h++ )
		{
			for ( int q = 0; q < seq; q++ )
			{
				float max = float.NegativeInfinity;
				int base2 = h * seq * seq;
				for ( int k = 0; k < seq; k++ )
				{
					bool allowed = attentionMask[q] != 0 && attentionMask[k] != 0;
					float s;
					if ( allowed )
					{
						float c2cV = c2c[base2 + q * seq + k];
						float c2pV = c2pContrib[base2 + q * seq + k];
						float p2cV = p2cContrib[base2 + q * seq + k];
						s = c2cV + c2pV + p2cV;
						if ( trace is not null )
						{
							c2cTrace[base2 + q * seq + k] = c2cV;
							c2pTrace[base2 + q * seq + k] = c2pV;
							p2cTrace[base2 + q * seq + k] = p2cV;
						}
					}
					else
					{
						s = float.MinValue;
					}
					probs[base2 + q * seq + k] = s;
					if ( s > max )
					{
						max = s;
					}
				}
				float sum = 0f;
				for ( int k = 0; k < seq; k++ )
				{
					float e = MathF.Exp( probs[base2 + q * seq + k] - max );
					probs[base2 + q * seq + k] = e;
					sum += e;
				}
				float inv = 1f / sum;
				for ( int k = 0; k < seq; k++ )
				{
					probs[base2 + q * seq + k] *= inv;
				}
			}
		}
		if ( trace is not null )
		{
			trace["c2c"] = c2cTrace;
			trace["c2p_contrib"] = c2pTrace;
			trace["p2c_contrib"] = p2cTrace;
		}

		trace?.Add( "probs", probs );
		// ---- context + head merge ---------------------------------------------
		float[] context = new float[hCount * seq * hd];
		for ( int h = 0; h < hCount; h++ )
		{
			for ( int q = 0; q < seq; q++ )
			{
				for ( int d = 0; d < hd; d++ )
				{
					float sum = 0f;
					for ( int k = 0; k < seq; k++ )
					{
						sum += probs[h * seq * seq + q * seq + k] * vh[h * seq * hd + k * hd + d];
					}
					context[h * seq * hd + q * hd + d] = sum;
				}
			}
		}
		float[] merged = new float[seq * _hidden];
		for ( int s = 0; s < seq; s++ )
		{
			for ( int h = 0; h < hCount; h++ )
			{
				for ( int d = 0; d < hd; d++ )
				{
					merged[s * _hidden + h * hd + d] = context[h * seq * hd + s * hd + d];
				}
			}
		}

		// ---- context + head merge trace ---------------------------------------
		// "context" is the attention module OUTPUT (merged [S,384]), matching
		// the oracle capture; per-head layout is kept separately.
		trace?.Add( "context", merged );
		trace?.Add( "context_heads", context );

		// ---- attention output dense + residual + LN (POST-norm) ---------------
		float[] attnDense = GlinerMath.Linear( merged, seq, w.AttnDenseW, w.AttnDenseB, _hidden, _hidden );
		float[] attnSum = GlinerMath.Add( attnDense, hiddenStates );
		float[] postAttn = GlinerMath.LayerNorm( attnSum, seq, _hidden, w.AttnLnW, w.AttnLnB, 1e-7f );
		trace?.Add( "attn_dense", attnDense );
		trace?.Add( "attn_ln", postAttn );

		// ---- FFN ---------------------------------------------------------------
		float[] up = GlinerMath.Linear( postAttn, seq, w.FfnUpW, w.FfnUpB, _hidden, 1536 );
		float[] act = GlinerMath.GeluErf( up );
		float[] down = GlinerMath.Linear( act, seq, w.FfnDownW, w.FfnDownB, 1536, _hidden );
		float[] outSum = GlinerMath.Add( down, postAttn );
		trace?.Add( "ffn_pre", up );
		trace?.Add( "gelu", act );
		trace?.Add( "ffn_out", down );
		if ( trace is not null )
		{
			// pre-mask combined scores (c2c + rel terms) — reconstruct from
			// stored traces so the fixture gate can compare them directly
			var pre = new float[hCount * seq * seq];
			for ( int i = 0; i < pre.Length; i++ )
			{
				pre[i] = c2c[i] + c2pContrib[i] + p2cContrib[i];
			}
			trace["scores_premask"] = pre;
		}
		return GlinerMath.LayerNorm( outSum, seq, _hidden, w.OutputLnW, w.OutputLnB, 1e-7f );
	}
}