Shader Monad for Imperative Shader Construction #
The ShaderM monad provides an imperative interface for building WGSL compute shaders. It tracks:
- Accumulated statements
- Fresh variable generation
- Shared memory declarations
- Automatic buffer binding
Usage pattern:
def myShader : ShaderM Unit := do
let gid ← globalId
let idx := swizzleX gid
-- Declare buffers
input ← declareInputBuffer "input" (.array (.scalar .f32) 1024)
output ← declareOutputBuffer "output" (.array (.scalar .f32) 1024)
-- Read, compute, write
val ← readBuffer input idx
let result := val * litF32 2.0
writeBuffer output idx result
Buffer access mode for storage buffers
- read : BufferAccessMode
- readWrite : BufferAccessMode
Instances For
Equations
- One or more equations did not get rendered due to their size.
Instances For
Equations
- Hesper.WGSL.Monad.instBEqBufferAccessMode.beq x✝ y✝ = (x✝.ctorIdx == y✝.ctorIdx)
Instances For
Shader Construction State
- varCounter : Nat
- needsSubgroups : Bool
Optional
__launch_bounds__analogue: caps register allocation per thread. CUDA backend lowers to PTX.maxnreg. None = ptxas default (which may pick a higher count and reduce occupancy).Optional minimum CTAs per SM hint. Combined with maxntid (derived from workgroup size), this is the full
__launch_bounds__(threads, minCtas)equivalent — ptxas uses it to size register allocation.
Instances For
The Shader Monad
Equations
Instances For
Initial state for shader construction
Equations
Instances For
Run a shader computation and extract the result and final state
Equations
Instances For
Run a shader computation and extract only the final state
Equations
Instances For
Emit a statement to the shader body
Equations
- One or more equations did not get rendered due to their size.
Instances For
Generate a fresh variable name with given prefix
Equations
- One or more equations did not get rendered due to their size.
Instances For
Declare a private variable with fresh name
Equations
- Hesper.WGSL.Monad.ShaderM.var ty init = do let name ← Hesper.WGSL.Monad.ShaderM.freshVar "v" Hesper.WGSL.Monad.ShaderM.emitStmt (Hesper.WGSL.Stmt.varDecl name ty (some ⟨ty, init⟩)) pure name
Instances For
Declare a named private variable
Equations
- Hesper.WGSL.Monad.ShaderM.varNamed name ty init = Hesper.WGSL.Monad.ShaderM.emitStmt (Hesper.WGSL.Stmt.varDecl name ty (some ⟨ty, init⟩))
Instances For
Bind a sub-expression to a named PTX register and return an Exp.var
referring to it. Use this when an Exp is referenced multiple times
inside an unrolled loop body or fan-out: without it, each reference is
inlined as a fresh AST traversal and the CodeGen emits the same
arithmetic at every use site (causing the 17× instruction inflation
seen in V9 vs llama.cpp). Wrapping the value with let' materialises
it as one PTX register that all uses share.
Example:
let kRowBase ← ShaderM.let' (.scalar .u32) (Exp.add base offset)
for pk in [0:4] do
-- `kRowBase` is a single Exp.var, so the four iterations all
-- read the same register instead of recomputing `base + offset`.
let idx := Exp.add kRowBase (Exp.litU32 (pk * 32))
...
Functionally equivalent to do let n ← var ty e; pure (Exp.var n),
but the intent is clearer at the call site.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Assign expression to variable
Equations
- Hesper.WGSL.Monad.ShaderM.assign varName expr = Hesper.WGSL.Monad.ShaderM.emitStmt (Hesper.WGSL.Stmt.assign varName ty expr)
Instances For
Assign to array index
Equations
- Hesper.WGSL.Monad.ShaderM.assignIndex arrName idx value = Hesper.WGSL.Monad.ShaderM.emitStmt (Hesper.WGSL.Stmt.assignIndex arrName idx ty value)
Instances For
If-then-else statement
Equations
- One or more equations did not get rendered due to their size.
Instances For
While loop - implemented as for loop with always-true condition
Equations
- Hesper.WGSL.Monad.ShaderM.while_ _cond body = do let __discr ← body.captureStmts match __discr with | (fst, bodyStmts) => Hesper.WGSL.Monad.ShaderM.emitStmt (Hesper.WGSL.Stmt.block bodyStmts)
Instances For
For loop (start to end, incrementing by step) Builds proper WGSL for loop: for (var i: u32 = start; i < end; i = i + step)
Equations
- One or more equations did not get rendered due to their size.
Instances For
Compile-time unrolled loop: emits n copies of the body inline,
with i bound to a Lean Nat (0, 1, ..., n-1). Use when iteration
count is statically known and the unroll is desired (eg. dot-product
over a fixed dim count, sub-warp partition iters).
The Nat i is a meta value — to use it in an Exp context, lift via
Exp.litU32 i or simply (i : Exp _) (the Exp OfNat instance).
Equivalent to writing for i in [0:n] do body i, but the helper name
documents intent: "this is meta-time unrolled, not a runtime loop".
Mirrors CUDA C++ #pragma unroll for (int i = 0; i < N; ++i).
For runtime loops use ShaderM.loop (or its alias runtimeFor).
Equations
- Hesper.WGSL.Monad.ShaderM.unrollFor n body = (List.range n).forM body
Instances For
LICM helpers (loop invariant code motion) #
Approximate "free variable" check: serialise an Exp via Exp.toWGSL and
look for the variable name as a whole token (name not preceded/followed
by an alphanumeric or underscore). This is correct for ShaderM-emitted
names because they're unique fresh names like v3 / i7 — there's no
risk of accidental match against a substring of another identifier.
Why string-match rather than a structural traversal: Exp has 50+
constructors and many of them hold sub-Exps. A structural
Exp.containsVar would be a lot of pattern matching that would have to
stay in sync as new Exp cases are added. toWGSL is the existing
single source of truth that walks every sub-Exp; reusing it for the
free-var check costs O(n) extra string work per stmt but is robust to
Exp evolution. In practice the runtime impact is negligible because
LICM only runs at kernel-emission time, not on the GPU.
Higher-order loop: pass loop variable as Exp. Now applies LICM:
varDecls in the body whose init expression doesn't reference the
loop variable (or any other inner-loop binding) are hoisted before
the for-loop. This automates the manual let'-outside-the-loop
pattern that V9 / earlier kernels used by hand.
Usage: loop start end step fun i => do { ... use i ... }
Equations
- One or more equations did not get rendered due to their size.
Instances For
Variant of loop that applies LICM to hoist varDecls whose init
is loop-invariant out of the for-loop. Use only when the body
contains no shared-memory writes that other iterations read, no
inter-iteration carries via externally-declared mutable vars beyond
those captured by assign, and only varDecl (no if/inner
for/barrier) sites that you want hoisted.
Semantics-changing in subtle ways — verify bit-parity before merging a kernel that uses this.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Runtime loop alias for loop, named to pair with unrollFor so the
meta-vs-runtime distinction is explicit at the call site:
ShaderM.unrollFor 8 fun i => ... -- meta-time, 8 inline copies
ShaderM.runtimeFor 0 cacheLen 1 fun i => ... -- runtime for-loop
Mirrors CUDA C++ for (int i = 0; ...) (no #pragma unroll).
Equations
- Hesper.WGSL.Monad.ShaderM.runtimeFor start end_ step bodyFn = Hesper.WGSL.Monad.ShaderM.loop start end_ step bodyFn
Instances For
Block scope: emits { ... body ... } so var declared inside the body
is block-scoped in WGSL, allowing Naga/Tint/Vulkan-driver register
allocator to reuse the physical register once the scope exits.
This is the analog of CUDA { ... } block scope for register-pressure
reduction. For example, when a temporary is needed only inside a hot
inner loop iteration, putting it in a scope instead of at function
level lets the compiler reuse its register slot afterwards.
Usage:
ShaderM.scope do
let tmp ← ShaderM.var (.scalar .f32) (Exp.litF32 0.0)
-- ... use tmp ...
-- tmp's register is released at end of scope
Note: Lean-side let bindings to tmp outside the do-block can still
refer to the var name string, but reading from it via Exp.var after
scope-exit is undefined behavior — the WGSL register is gone.
Discipline: only use tmp inside the scope body.
Equations
- body.scope = do let __discr ← body.captureStmts match __discr with | (result, bodyStmts) => do Hesper.WGSL.Monad.ShaderM.emitStmt (Hesper.WGSL.Stmt.block bodyStmts) pure result
Instances For
unrollFor + per-iter scope. Each of the n inline copies of the
body lives in its own { ... } block, so any ShaderM.var declared
inside is block-scoped and the WGSL→PTX backend can reuse the
physical register across iters.
Collapses the V8/V9/V11 idiom
for i in [0:n] do ShaderM.scope do
...
into
ShaderM.unrollForScoped n fun i => do
...
Mirrors CUDA C++ #pragma unroll for (int i = ...) where each
unrolled body is implicitly its own register-allocation scope.
Equations
- Hesper.WGSL.Monad.ShaderM.unrollForScoped n body = (List.range n).forM fun (i : Nat) => (body i).scope
Instances For
Workgroup barrier (synchronization)
Equations
Instances For
Warp-level barrier (CUDA __syncwarp()). PTX backend emits the
cheap bar.warp.sync 0xFFFFFFFF; WGSL backend falls back to
workgroupBarrier(). Use when only intra-warp ordering is needed
— eg. between Phase 1 (KQ dot) and Phase 2a (cross-sub-warp max
reduce) in the flash-attn vec kernel where all 32 lanes of a warp
must finish writing before reading.
Equations
Instances For
Cap per-thread register usage at n. ptxas will spill to local memory
if it would otherwise exceed this. Used for occupancy tuning — fewer
registers per thread → more blocks per SM. CUDA-only (lowers to PTX
.maxnreg). WGSL backend ignores.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Promise that ≥ n CTAs will run per SM. Combined with the workgroup
size (which gives ptxas .maxntid), this is the __launch_bounds__
equivalent — lets ptxas pick a register budget that fits N blocks.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Pure: raw u64 pointer to element idx of a global buffer.
elemSize is the byte size of one element (typically 4 for u32/f32).
Used as the global-address operand for cpAsync. CUDA-only.
Equations
- Hesper.WGSL.Monad.ShaderM.bufferAddr bufName elemSize idx = Hesper.WGSL.Exp.bufferAddr bufName elemSize idx
Instances For
── cp.async (sm_80+) ──
Issue an async global→shared memory copy of bytes bytes
(must be 4, 8, or 16). Non-blocking — completion synchronised
via cpAsyncCommit + cpAsyncWait. Used by llama.cpp's MMQ
pipeline to overlap the next K-iteration's load with the
current K-iteration's compute. WGSL backend has no equivalent.
Equations
- Hesper.WGSL.Monad.ShaderM.cpAsync smemAddr globalAddr bytes = Hesper.WGSL.Monad.ShaderM.emitStmt (Hesper.WGSL.Stmt.exprStmt (smemAddr.cpAsyncCgSharedGlobal globalAddr bytes))
Instances For
cp.async.ca cache-all variant — supports bytes ∈ {4, 8, 16}.
Slower per byte than .cg for 16-byte transfers but the only option
for narrower transfers.
Equations
- Hesper.WGSL.Monad.ShaderM.cpAsyncCa smemAddr globalAddr bytes = Hesper.WGSL.Monad.ShaderM.emitStmt (Hesper.WGSL.Stmt.exprStmt (smemAddr.cpAsyncCaSharedGlobal globalAddr bytes))
Instances For
cp.async.commit_group — bundle all preceding cpAsync issues
by this thread into one group for later cpAsyncWait.
Equations
Instances For
cp.async.wait_group N — block this thread until all but the most
recent N committed groups have completed. N=0 waits for all.
Equations
Instances For
Global invocation ID (3D)
Equations
- Hesper.WGSL.Monad.ShaderM.globalId = pure (Hesper.WGSL.Exp.var "global_invocation_id")
Instances For
Local invocation ID (3D)
Equations
- Hesper.WGSL.Monad.ShaderM.localId = pure (Hesper.WGSL.Exp.var "local_invocation_id")
Instances For
Workgroup ID (3D)
Equations
- Hesper.WGSL.Monad.ShaderM.workgroupId = pure (Hesper.WGSL.Exp.var "workgroup_id")
Instances For
Number of workgroups (3D)
Equations
- Hesper.WGSL.Monad.ShaderM.numWorkgroups = pure (Hesper.WGSL.Exp.var "num_workgroups")
Instances For
Thread index helpers (CUDA-style shortcuts) #
These mirror CUDA's threadIdx.x, blockIdx.x, etc. for the common
1D case so kernel code reads close to the CUDA original. The vec3
versions (localId, workgroupId) are still available when 2D/3D
index components are needed.
threadIdx.x — local invocation X coordinate (= "tid" in CUDA kernels).
Equations
- Hesper.WGSL.Monad.ShaderM.tidX = do let __do_lift ← Hesper.WGSL.Monad.ShaderM.localId pure __do_lift.vec3X
Instances For
threadIdx.y
Equations
- Hesper.WGSL.Monad.ShaderM.tidY = do let __do_lift ← Hesper.WGSL.Monad.ShaderM.localId pure __do_lift.vec3Y
Instances For
threadIdx.z
Equations
- Hesper.WGSL.Monad.ShaderM.tidZ = do let __do_lift ← Hesper.WGSL.Monad.ShaderM.localId pure __do_lift.vecZ
Instances For
blockIdx.x — workgroup X coordinate.
Equations
- Hesper.WGSL.Monad.ShaderM.bidX = do let __do_lift ← Hesper.WGSL.Monad.ShaderM.workgroupId pure __do_lift.vec3X
Instances For
blockIdx.y
Equations
- Hesper.WGSL.Monad.ShaderM.bidY = do let __do_lift ← Hesper.WGSL.Monad.ShaderM.workgroupId pure __do_lift.vec3Y
Instances For
blockIdx.z
Equations
- Hesper.WGSL.Monad.ShaderM.bidZ = do let __do_lift ← Hesper.WGSL.Monad.ShaderM.workgroupId pure __do_lift.vecZ
Instances For
Warp / sub-warp index decomposition #
NVIDIA warps are 32 lanes. These helpers compute laneId (tid & 31)
and warpId (tid >>> 5) once and return them as Exp values, so
kernel code that uses them many times shares one PTX register. The
inner expressions are simple enough that lean codegen folds the
extraction efficiently — no let' needed at the use site.
Sub-warp partition (used in V8/V11 attention vec kernels) splits a
warp into N-lane groups via subWarpSplit n.
Lane index within the warp (0..31). Equivalent to CUDA's
threadIdx.x & 31 when threads are 1D, or threadIdx.x % WARP_SIZE.
Equations
- Hesper.WGSL.Monad.ShaderM.laneId = do let __do_lift ← Hesper.WGSL.Monad.ShaderM.tidX pure (__do_lift &&& Hesper.WGSL.Exp.litU32 31)
Instances For
Warp index within the workgroup. Equivalent to CUDA's
threadIdx.x / WARP_SIZE (= >> 5). For 2D blocks where
threadIdx.y selects warps, use tidY directly.
Equations
- Hesper.WGSL.Monad.ShaderM.warpId = do let __do_lift ← Hesper.WGSL.Monad.ShaderM.tidX pure (__do_lift >>> Hesper.WGSL.Exp.litU32 5)
Instances For
Split a warp's lane into (subWarp, subLane) for nthreads_KQ-style
sub-warp partitioning where each sub-warp handles a different K
position in parallel. Returns (laneId / n, laneId % n).
Example (V11 sub-warp partition with n = 8):
let (sw, sl) ← ShaderM.subWarpSplit 8
-- sw ∈ [0, 4), sl ∈ [0, 8)
let kPos := warpBase + sw * 8 + iKQ0 -- this sub-warp's K
let dimBase := sl * 32 -- this lane's D slice
Mirrors llama.cpp's
(threadIdx.x & ~(nthreads_KQ-1), threadIdx.x % nthreads_KQ).
Equations
- One or more equations did not get rendered due to their size.
Instances For
Warp-level reductions #
warpReduceSum n e reduces e across the n-lane group within the warp:
n = 32(full warp) → emits a singlesubgroupAdd(5-shfl on PTX, hardware wide reduction on WGSL).n = 8(sub-warp, eg. nthreads_KQ from llama.cpp) → emits 3 shfl-xor by 1, 2, 4. All 8 lanes end with the sum of their 8-lane group.- Other
n(must be power of 2, ≤ 32) → log2(n) shfl-xor butterflies.
After the call, every lane in the n-lane group holds the same reduced
value. Mirrors llama.cpp's warp_reduce_sum<nthreads> template.
Example (V11-like sub-warp dot product):
let partialVar ← ShaderM.mutVar (.scalar .f32) 0
ShaderM.unrollFor dimsPerLane fun k => partialVar +↦ q[k]! * kVec[k]!
let sum ← ShaderM.warpReduceSum 8 partialVar.read -- 3 shfl
Sum-reduce e across an n-lane group within the warp. Uses
Exp.subgroupAdd when n = 32, otherwise emits log2(n) butterfly
shuffles via Exp.subgroupShuffleXor.
Pre: n is a power of 2 and 1 ≤ n ≤ 32.
Each lane in the n-lane group receives the same sum. Cost: log2(n) shfl + n−1 add per lane (so a sub-warp reduce of 8 lanes is 3 shfl
- 3 add — half the cost of the full 32-lane reduce, useful when only 8-lane groups need to coordinate).
Equations
- One or more equations did not get rendered due to their size.
Instances For
Max-reduce variant: warpReduceMax n e. Same shfl-xor butterfly,
using Exp.max instead of +. After the call, every lane in the
group holds the same max.
Equations
- One or more equations did not get rendered due to their size.
Instances For
One step of online softmax (Milakov & Gimelshein 2018 / FlashAttention).
Given the running max kqMax, running sum kqSum, and a new score
kqScore, computes:
kqMaxNew = max(kqMax, kqScore)
scale = exp(kqMax - kqMaxNew) -- multiplier for old VKQ accumulator
kqExp = exp(kqScore - kqMaxNew) -- per-thread weight for current K
kqSumNew = kqSum * scale + kqExp
kqMax := kqMaxNew -- updated in-place
kqSum := kqSumNew -- updated in-place
Returns (kqMaxNew, scale, kqExp). The caller rescales their VKQ
accumulators by scale and accumulates kqExp * V for this K
position.
kqMaxName/kqSumName are the names of the running registers
so we can assign them in place. The new max is also returned as
an Exp so the caller can use it without an extra Exp.var lookup.
Mirrors the inner-loop accumulation in llama.cpp's
flash_attn_ext_vec (lines 273, 287-291 in fattn-vec.cuh).
Equations
- One or more equations did not get rendered due to their size.
Instances For
Read from a global storage buffer at index Note: You need to provide the element type explicitly
Equations
- Hesper.WGSL.Monad.ShaderM.readBuffer bufferName idx = pure ((Hesper.WGSL.Exp.var bufferName).index idx)
Instances For
Read a single byte (zero-extended to u32) from a buffer declared as
array<u32, n>, addressed by byte index. Lowers to one ld.global.u8
on CUDA; emulated via u32-load+shift+mask on WGSL. Essential for Q6_K
scale reads (avoids issuing a full u32 load per byte).
Equations
- Hesper.WGSL.Monad.ShaderM.readBufferByte bufferName byteIdx = pure (Hesper.WGSL.Exp.loadByteFromU32Buf bufferName byteIdx)
Instances For
Read a halfword (16 bits, zero-extended to u32) from a buffer declared
as array<u32, n>, addressed by byte index. Lowers to ld.global.u16
on CUDA. Use for fp16 block scales.
Equations
- Hesper.WGSL.Monad.ShaderM.readBufferU16 bufferName byteIdx = pure (Hesper.WGSL.Exp.loadU16FromU32Buf bufferName byteIdx)
Instances For
128-bit (4× u32) wide read from a buffer declared array<u32, n>,
addressed by u32 index. The starting index must be 4-aligned and the
buffer pointer 16-byte aligned (caller's responsibility — typical
callers iterate by pk * 4).
On CUDA this lowers to a single ld.global.nc.v4.u32 instruction
(one MIO op delivering 16 bytes), 4× more efficient than four scalar
ld.global.u32 reads in MIO-pipe-saturated kernels (FlashAttn V11).
On WGSL it emulates as four scalar reads.
Returns four Exp (.scalar .u32) referring to fresh declared vars.
Each value can be used independently with Exp.unpack2x16float etc.
Equations
- One or more equations did not get rendered due to their size.
Instances For
128-bit (4× f32) wide read from shared memory. f32Idx must be
4-aligned. CUDA lowers to one ld.shared.v4.f32; WGSL emulates as
four scalar reads.
Used to relieve MIO-pipe saturation on shared-memory traffic (FlashAttn V11 partial aggregation: 4× LDS instruction count reduction).
Equations
- One or more equations did not get rendered due to their size.
Instances For
Write to a global storage buffer at index
Equations
- Hesper.WGSL.Monad.ShaderM.writeBuffer bufferName idx value = Hesper.WGSL.Monad.ShaderM.assignIndex bufferName idx value
Instances For
Read from workgroup shared memory at index
Equations
- Hesper.WGSL.Monad.ShaderM.readWorkgroup sharedName idx = pure ((Hesper.WGSL.Exp.var sharedName).index idx)
Instances For
Write to workgroup shared memory at index
Equations
- Hesper.WGSL.Monad.ShaderM.writeWorkgroup sharedName idx value = Hesper.WGSL.Monad.ShaderM.assignIndex sharedName idx value
Instances For
Declare a storage buffer with explicit access mode
Equations
- One or more equations did not get rendered due to their size.
Instances For
Declare a read-only storage buffer.
Semantically equivalent to declareInputBuffer on WGSL, but on CUDA this
emits ld.global.nc.* (the read-only L1/tex cache hint, aka __ldg).
Use for weight matrices and other buffers the kernel never writes to.
It is a logical error to call writeBuffer on the returned name.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Declare a read-only buffer array binding: N separate runtime-sized
storage buffers addressable by runtime layer index. The host side must
pass a device-pointer-table — N × 8 bytes holding each layer's
CUdeviceptr — as the kernel argument bound to name. Inside the
kernel, read elements with readBufferArray name layerIdx elemIdx.
This is the primitive that lets a single kernel iterate over all 42
transformer layers, collapsing 42 dispatches into 1. The existing
per-layer kernels remain; use bufferArray only when a kernel actually
wants to fuse across layers.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Read element elemIdx from the bufIdx-th buffer of a bufferArray.
Emits a single-indirection load via the pointer table.
Equations
- Hesper.WGSL.Monad.ShaderM.readBufferArray name bufIdx elemIdx = pure ((Hesper.WGSL.Exp.var name).indexBuf bufIdx elemIdx)
Instances For
Write value to arr[bufIdx][elemIdx] where arr is a bufferArray.
Equations
- Hesper.WGSL.Monad.ShaderM.writeBufferArray name bufIdx elemIdx value = Hesper.WGSL.Monad.ShaderM.emitStmt (Hesper.WGSL.Stmt.assignIndexBuf name bufIdx elemIdx ty value)
Instances For
Float32 literal
Equations
Instances For
Int32 literal
Equations
Instances For
UInt32 literal
Equations
Instances For
Declare an array of subgroup_matrix_left matrices with initialization
Equations
- One or more equations did not get rendered due to their size.
Instances For
Declare an array of subgroup_matrix_right matrices with initialization
Equations
- One or more equations did not get rendered due to their size.
Instances For
Declare an array of subgroup_matrix_result matrices with initialization
Equations
- One or more equations did not get rendered due to their size.
Instances For
Load subgroup_matrix_left from buffer
Example: Ax[i] = subgroupMatrixLoad<subgroup_matrix_left<f32,8,8>>(&A, offset, false, stride)
Equations
- One or more equations did not get rendered due to their size.
Instances For
Load subgroup_matrix_right from buffer
Equations
- One or more equations did not get rendered due to their size.
Instances For
loadMatrixRight with an explicit column-major flag — for loading a right (k×n)
fragment DIRECTLY from a storage buffer laid out n-major (e.g. Bᵀ [N,K] row-major:
element (k,n) at b[n·K+k] ⇒ colMajor=true, stride=K). Buffer-space subgroupMatrixLoad
is supported by the pinned May Tint (verified; lowers to device simdgroup_load).
Equations
- One or more equations did not get rendered due to their size.
Instances For
Perform matrix multiply-accumulate: acc = a * b + acc
Example: accxx[idx] = subgroupMatrixMultiplyAccumulate(Ax[i], Bx[j], accxx[idx])
Equations
- One or more equations did not get rendered due to their size.
Instances For
Mixed-precision variant of matrixMultiplyAccumulate: A and B use
inSt, C / D use outSt. This matches NVIDIA cooperative matrix's
native (f16, f16) → f32 config.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Store subgroup_matrix_result to buffer
Example: subgroupMatrixStore(&C, offset, accxx[idx], false, stride)
Equations
- One or more equations did not get rendered due to their size.
Instances For
Fragment-as-local-variable helpers (real-CUDA-style WMMA) #
The array-of-fragments helpers above (loadMatrixLeft, matrixMultiplyAccumulate,
storeMatrixResult) match WGSL's array<subgroup_matrix_left, N> model —
nice for blocked algorithms but awkward to lower to PTX since fragments live
in registers, not memory. The helpers below match the more direct
nvcuda::wmma::fragment a_frag; load_matrix_sync(a_frag, ...); style: each
fragment is a single named variable, with no array indirection. This is the
recommended path for new WMMA kernels — the array helpers remain for
backward-compat and WGSL-targeted code.
Declare a left fragment (subgroup_matrix_left<st, m, k>) bound to a
name, initialized via a load from bufferName at byte-offset offset
with row-major stride (in elements).
Equations
- One or more equations did not get rendered due to their size.
Instances For
Declare a right fragment, loaded col-major (the WGSL subgroupMatrixLoadRight
instruction, which lowers to PTX wmma.load.b...col.f16).
Equations
- One or more equations did not get rendered due to their size.
Instances For
Declare a result fragment initialized to zero. Maps to PTX
mov.f32 %f0..7, 0; for the f32 case (8 regs).
Equations
- One or more equations did not get rendered due to their size.
Instances For
cName ← cName * (a × b). Uses the existing Exp.subgroupMatrix- MultiplyAccumulate; lowers to one wmma.mma.sync instruction and
rebinds cName to the result fragment.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Store a result fragment to a buffer at byte-offset offset with
row-major stride (in elements).
Equations
- One or more equations did not get rendered due to their size.
Instances For
Static loop unrolling for matrix operations
Executes an action for each index in the range [0, count)
Equations
- Hesper.WGSL.Monad.ShaderM.staticLoop count body = do forIn [:count] PUnit.unit fun (i : Nat) (r : PUnit) => do body i pure (ForInStep.yield PUnit.unit) pure PUnit.unit
Instances For
Typed mutable variables #
MutVar ty wraps a private (PTX-register / WGSL var) variable with its
type carried in Lean. This closes the second cognitive gap from porting
CUDA C++ — operator overloading in Step 1 still needed (Exp.var name : Exp _)
type annotations everywhere. With MutVar, v.read + x and v +↦ x
type-check directly because read returns a typed Exp ty.
Idiomatic use:
let acc ← ShaderM.mutVar (.scalar .f32) 0
for k in [0:8] do
acc +↦ qVec[k]! * kVec[k]! -- in-place +=
let final := acc.read
Mutable shader-side variable carrying its type in Lean. Construct via
ShaderM.mutVar — never by hand (the contained name must be one
emitted by ShaderM.var so PTX/WGSL codegen sees a declaration).
- name : String
Instances For
Equations
Equations
- One or more equations did not get rendered due to their size.
Instances For
Read the current value as a typed Exp ty.
Equations
- v.read = Hesper.WGSL.Exp.var v.name
Instances For
Mutating-add operator. v +↦ x ≡ MutVar.addAssign v x.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Mutating-multiply operator.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Mutating-write operator (≡ MutVar.write). v ↦= x.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Declare a typed mutable variable. Use v.read / v.write /
v +↦ x / v ↦= x. The successor to ShaderM.var.
Example:
let acc ← ShaderM.mutVar (.scalar .f32) 0
for k in [0:dimsPerLane] do
acc +↦ qVec[k]! * kVec[k]!
Equations
- One or more equations did not get rendered due to their size.
Instances For
Buffer pointers (Ptr ty) #
Ptr ty is a (bufferName, offset) pair carrying its element type so
the load / store calls don't need explicit (ty := ...) annotations
each time. Mirrors CUDA C++ T *p arithmetic — particularly the common
attention/matmul pattern:
const float *K = K_base + kvHead * maxSeq * D;
for (int k = 0; k < cacheLen; ++k) {
sum += *K * q;
K += D;
}
In ShaderM:
let K ← ShaderM.ptr (.scalar .f32) "k_cache" (kvHead * maxSeqLen * D)
ShaderM.runtimeFor 0 cacheLen 1 fun _ => do
let v ← K.load
acc +↦ v * q
K := K.advance D -- value-level advance (Ptr is immutable)
For mutating advance inside a runtime loop, use MutPtr (declared
below). For pure pointer arithmetic at meta-time, use Ptr.atOffset.
Ptr is intentionally pure (no ShaderM effect). The constructor
ShaderM.ptr only computes a let'-bound base offset for safety,
which is the common case.
Buffer pointer: typed (buffer name, current u32 offset, declared array size) triple.
buf: buffer name registered viadeclareInputBufferetc.offset: current element index (NOT byte) into that buffer.bufLen: the buffer's declared array length (passed toreadBuffer's{n}for the WGSLarray<ty, n>type). Set this to the same value used atdeclareInputBuffertime.
Ptr is value-level (no ShaderM effect needed for arithmetic).
Use ShaderM.ptr to construct one with a let'-stored base.
- buf : String
- offset : Exp (WGSLType.scalar ScalarType.u32)
- bufLen : Nat
Instances For
Advance the pointer by delta elements, returning a new Ptr.
Mirrors CUDA p + delta (or p += delta if you reassign). Pure
— no shader-side effect.
Instances For
Read at p[k] without permanently advancing. Mirrors CUDA p[k].
Composes well with unrollFor:
ShaderM.unrollFor n fun k =>
let v ← (p.atOffset (Exp.litU32 k)).load
...
Instances For
Construct a Ptr with a base offset materialised in a register
(via let'). Use when the base offset is computed once (eg.
kvHead * maxSeqLen * D) and the resulting pointer is then
walked many times in inner loops.
bufLen must match the array size declared via
declareInputBuffer for buf.
Example:
let K ← ShaderM.ptr (.scalar .u32) "k_cache_f16" kvWords
(kvHead * maxSeqLen * (D/2))
-- K.offset is a single PTX register; subsequent advance/atOffset
-- only emit `add` against this register.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Mutable pointer — like Ptr but offset is a var (read/write
register) instead of an Exp, so advance actually updates the
register in-place rather than constructing a new value.
Mirrors the CUDA outer-loop idiom
K += blockIdx.y * nthreads * nb11; // initial
for (int k = ...; k < kmax; k += step,
K += step * nb11, V += step * nb21) { // advance per iter
sum += vec_dot_KQ(K + i_KQ * nb11, ...);// inner per-iter offset
}
Step 6's Ptr only handles the inner per-iter offset; MutPtr
handles the outer per-iter pointer advance.
- buf : String
- offsetVar : String
Name of the u32 register that holds the current offset. Mutable — gets reassigned by
MutPtr.advance. - bufLen : Nat
Instances For
Equations
- One or more equations did not get rendered due to their size.
Instances For
Equations
Current offset as an Exp.var so it can be used in index
arithmetic without materialising another register.
Equations
Instances For
Read at p.offset + extra without advancing. Mirrors CUDA p[k]
inside an unrolled inner loop.
Instances For
Advance the pointer by delta. The next load / loadAt reads
from the new offset. Mirrors CUDA p += delta.
Instances For
Construct a MutPtr whose offset register is initialised from
baseOffset. The resulting pointer can be advanced in-place
inside an outer loop, matching CUDA's K += stride idiom.
Example (V11 outer K loop):
let K ← ShaderM.mutPtr (.scalar .u32) "k_cache_f16" kvWords
(kvHead * maxSeqLen * (D/2) + laneId)
ShaderM.runtimeFor splitStart splitEnd (Exp.litU32 wgSize) fun _ => do
...
let kPacked ← K.loadAt (Exp.litU32 (pk * 32))
...
K.advance (Exp.litU32 (wgSize * (D/2)))
Equations
- One or more equations did not get rendered due to their size.
Instances For
Typed register array — n named ShaderM vars that share a type and
can be indexed by a meta-time Nat. Replaces the V8/V11 idiom
let mut q0Vars : Array String := #[]
for pk in [0:n] do
let v ← ShaderM.var ty (init pk)
q0Vars := q0Vars.push v
-- later:
let q0 : Exp _ := Exp.var q0Vars[pk]!
with
let q0 ← RegArray.mk ty n init
-- later:
let q0Exp := q0.get pk
q0.set pk newVal
The Array String field stays in Lean meta land — at codegen time
it materialises as n separate varDecl stmts (one per slot).
Mirrors CUDA's T arr[N]; register array.
Instances For
Equations
- One or more equations did not get rendered due to their size.
Instances For
Equations
Read slot i as an Exp. Returns Exp.litU32 0 if out-of-range
(shouldn't happen in well-typed code; the bounds check is a safety
net against meta-time index typos).