Gliner/Gpu/GlinerGpuOps.cs
using System;
using Sandbox;
namespace GlinerPoc.Gpu;
/// <summary>
/// Phase 8B.7+ — the FP32 numerical operations of the GLiNER GPU backend.
///
/// CONTRACT: every method here MUST be called from inside a render-context
/// job (GlinerGpuExecutor work action) — they bind Graphics.Attributes and
/// dispatch ComputeShaders, which only execute correctly with a live
/// Graphics.Context (P8A/P8A.1). Calling them outside the executor is a bug.
///
/// All ops are FP32. Ops never allocate buffers; callers pass pre-created
/// runtime buffers. Dispatch arguments are THREAD counts (the engine divides
/// by the shader's numthreads).
/// </summary>
public static class GlinerGpuOps
{
// ---- canary ---------------------------------------------------------------
public static void VecAddConstant( ComputeShader sh, GpuBuffer<float> src, GpuBuffer<float> dst, int count, float constant )
{
var a = Graphics.Attributes;
a.Set( "InputValues", src );
a.Set( "OutputValues", dst );
a.Set( "ValueCount", count );
a.Set( "AddConstant", constant );
sh.Dispatch( count, 1, 1 );
}
// ---- GEMM family: Y = X*W^T (+ bias); X [rows,inDim], W [outDim,inDim] ------
private static void BindGemm( GpuBuffer<float> x, GpuBuffer<float> w, GpuBuffer<float> bias, GpuBuffer<float> y,
int rows, int inDim, int outDim, bool withBias )
{
var a = Graphics.Attributes;
a.Set( "X", x );
a.Set( "W", w );
a.Set( "Bias", bias );
a.Set( "Y", y );
a.Set( "RowCount", rows );
a.Set( "InDim", inDim );
a.Set( "OutDim", outDim );
a.Set( "HasBias", withBias ? 1 : 0 );
}
/// <summary>Naïve one-thread-per-output GEMM (P8A.1 kernel — permanent parity reference).</summary>
public static void LinearNaive( ComputeShader sh, GpuBuffer<float> x, GpuBuffer<float> w, GpuBuffer<float> bias, GpuBuffer<float> y,
int rows, int inDim, int outDim, bool withBias )
{
BindGemm( x, w, bias, y, rows, inDim, outDim, withBias );
sh.Dispatch( rows, outDim, 1 );
}
/// <summary>Tiled 16×16 groupshared GEMM (P8B.8 production kernel).</summary>
public static void LinearTiled16( ComputeShader sh, GpuBuffer<float> x, GpuBuffer<float> w, GpuBuffer<float> bias, GpuBuffer<float> y,
int rows, int inDim, int outDim, bool withBias )
{
BindGemm( x, w, bias, y, rows, inDim, outDim, withBias );
sh.Dispatch( rows, outDim, 1 );
}
/// <summary>Tiled 8×8 groupshared GEMM (P8B.8 variant).</summary>
public static void LinearTiled8( ComputeShader sh, GpuBuffer<float> x, GpuBuffer<float> w, GpuBuffer<float> bias, GpuBuffer<float> y,
int rows, int inDim, int outDim, bool withBias )
{
BindGemm( x, w, bias, y, rows, inDim, outDim, withBias );
sh.Dispatch( rows, outDim, 1 );
}
// ---- elementwise ------------------------------------------------------------
public static void Add( ComputeShader sh, GpuBuffer<float> a, GpuBuffer<float> b, GpuBuffer<float> c, int count )
{
var at = Graphics.Attributes;
at.Set( "A", a );
at.Set( "B", b );
at.Set( "C", c );
at.Set( "ValueCount", count );
sh.Dispatch( count, 1, 1 );
}
public static void Relu( ComputeShader sh, GpuBuffer<float> x, GpuBuffer<float> y, int count )
{
var a = Graphics.Attributes;
a.Set( "X", x );
a.Set( "Y", y );
a.Set( "ValueCount", count );
sh.Dispatch( count, 1, 1 );
}
/// <summary>Exact erf GELU (A&S 7.1.26, same polynomial as the CPU oracle).</summary>
public static void Gelu( ComputeShader sh, GpuBuffer<float> x, GpuBuffer<float> y, int count )
{
var a = Graphics.Attributes;
a.Set( "X", x );
a.Set( "Y", y );
a.Set( "ValueCount", count );
sh.Dispatch( count, 1, 1 );
}
// ---- row reductions (sequential order = CPU oracle order) --------------------
/// <summary>LayerNorm over the last dim; one thread per row; eps inside sqrt.</summary>
public static void LayerNorm( ComputeShader sh, GpuBuffer<float> x, GpuBuffer<float> weight, GpuBuffer<float> bias, GpuBuffer<float> y,
int rows, int dim, float eps )
{
var a = Graphics.Attributes;
a.Set( "X", x );
a.Set( "LnWeight", weight );
a.Set( "LnBias", bias );
a.Set( "Y", y );
a.Set( "RowCount", rows );
a.Set( "Dim", dim );
a.Set( "Eps", eps );
sh.Dispatch( rows, 1, 1 );
}
/// <summary>Row-wise stable softmax (max-shift). Row length ≤ 256 (V1 limit).</summary>
public static void SoftmaxRows( ComputeShader sh, GpuBuffer<float> x, GpuBuffer<float> y, int rows, int dim )
{
var a = Graphics.Attributes;
a.Set( "X", x );
a.Set( "Y", y );
a.Set( "RowCount", rows );
a.Set( "Dim", dim );
sh.Dispatch( rows, 1, 1 );
}
// ---- index/copy ----------------------------------------------------------------
public static void Copy( ComputeShader sh, GpuBuffer<float> src, GpuBuffer<float> dst, int count, int srcOffset = 0 )
{
var a = Graphics.Attributes;
a.Set( "Src", src );
a.Set( "Dst", dst );
a.Set( "ValueCount", count );
a.Set( "SrcOffset", srcOffset );
sh.Dispatch( count, 1, 1 );
}
/// <summary>Row gather: dst[r,j] = src[indices[r], j]. Exact index parity required.</summary>
public static void GatherRows( ComputeShader sh, GpuBuffer<float> src, GpuBuffer<int> indices, GpuBuffer<float> dst, int rows, int width )
{
var a = Graphics.Attributes;
a.Set( "Src", src );
a.Set( "Indices", indices );
a.Set( "Dst", dst );
a.Set( "RowCount", rows );
a.Set( "Width", width );
sh.Dispatch( rows * width, 1, 1 );
}
// ---- P8C attention kernels (virtual head layout, no transposes) -----------
/// <summary>c2c: out[h,q,k] = Σ_d Q[h,q,d]·(K[h,k,d]/scale). Buffers row-major [rows,384].</summary>
public static void C2C( ComputeShader sh, GpuBuffer<float> q, GpuBuffer<float> k, GpuBuffer<float> o, int rows, float scale )
{
var a = Graphics.Attributes;
a.Set( "Q", q );
a.Set( "K", k );
a.Set( "Out", o );
a.Set( "Rows", rows );
a.Set( "Scale", scale );
sh.Dispatch( 6 * rows * rows, 1, 1 );
}
/// <summary>c2p: out[h,q,k] = (Σ_d Q[h,q,d]·posK[h,clamp(q-k+256),d])/scale. posK [512,384].</summary>
public static void C2P( ComputeShader sh, GpuBuffer<float> q, GpuBuffer<float> posK, GpuBuffer<float> o, int rows, float scale )
{
var a = Graphics.Attributes;
a.Set( "Q", q );
a.Set( "PosK", posK );
a.Set( "Out", o );
a.Set( "Rows", rows );
a.Set( "Scale", scale );
sh.Dispatch( 6 * rows * rows, 1, 1 );
}
/// <summary>p2c: out[h,q,k] = (Σ_d K[h,k,d]·posQ[h,clamp(q-k+256),d])/scale (pre-transpose indexing). posQ [512,384].</summary>
public static void P2C( ComputeShader sh, GpuBuffer<float> k, GpuBuffer<float> posQ, GpuBuffer<float> o, int rows, float scale )
{
var a = Graphics.Attributes;
a.Set( "K", k );
a.Set( "PosQ", posQ );
a.Set( "Out", o );
a.Set( "Rows", rows );
a.Set( "Scale", scale );
sh.Dispatch( 6 * rows * rows, 1, 1 );
}
/// <summary>Elementwise combine of three tensors.</summary>
public static void Combine3( ComputeShader sh, GpuBuffer<float> a, GpuBuffer<float> b, GpuBuffer<float> c, GpuBuffer<float> o, int count )
{
var at = Graphics.Attributes;
at.Set( "A", a );
at.Set( "B", b );
at.Set( "C", c );
at.Set( "Out", o );
at.Set( "ValueCount", count );
sh.Dispatch( count, 1, 1 );
}
/// <summary>Encoder padding mask (outer product); invalid scores ← float.MinValue.</summary>
public static void MaskScores( ComputeShader sh, GpuBuffer<float> scores, GpuBuffer<uint> mask, int rows, float maskFill )
{
var a = Graphics.Attributes;
a.Set( "Scores", scores );
a.Set( "Mask", mask );
a.Set( "Rows", rows );
a.Set( "MaskFill", maskFill );
sh.Dispatch( 6 * rows * rows, 1, 1 );
}
/// <summary>Attention context + head merge: merged[q*384+hd] = Σ_k probs[h,q,k]·V[k*384+hd].</summary>
public static void ContextMerged( ComputeShader sh, GpuBuffer<float> probs, GpuBuffer<float> v, GpuBuffer<float> merged, int rows )
{
var a = Graphics.Attributes;
a.Set( "Probs", probs );
a.Set( "V", v );
a.Set( "Merged", merged );
a.Set( "Rows", rows );
sh.Dispatch( rows * 384, 1, 1 );
}
}