Skip to content

Commit caa6ddb

Browse files
feat(cuda): TexTable — layered 1-D lookup-table textures with point/hardware filtering
Immutable layered 1-D float32 tables evaluated by piecewise-linear interpolation at grid-space coordinates, backed by a layered cudaArray + cudaTextureObject_t on the CUDA build and a host-memory parity stub by default. Two filter modes fixed at construction: point (two point fetches + explicit contraction-blocked float32 lerp, bit-identical between the CUDA kernel and the CPU stub) and hardware linear (zero-ALU texture-unit lerp with CUDA's 9-bit fixed-point weight, tolerance-validated; the stub emulates the quantized weight). - csrc/cuda/common/torchlean_cuda_textable.h: boxed ABI + symbol contract - csrc/cuda/textures/torchlean_cuda_textable{.cu,_stub.c}: external class, layered-array construction, fetch kernel/loop, metadata accessors - lakefile: extern_lib torchlean_cuda_textable via buildNativeBackendLib - Cuda.Trusted: opaque TexTable handle; Cuda.TexTable: extern bindings - Cuda.KernelSpec: texLerpSpec (point-mode lerp over decomposed indices) - Tests/Runtime/Cuda/TexTable: bit-exact point mode vs an executable Float32 reference, tolerance-gated hardware mode, integer-node texel-center probes, clamp/width=1/empty edges; wired into the suite
1 parent 6ff32cb commit caa6ddb

11 files changed

Lines changed: 788 additions & 0 deletions

File tree

NN/Runtime/Autograd/Engine/Cuda/KernelSpec.lean

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,26 @@ def gatherThenScatterToZeroSpec {n k : Nat} (x : FlatBuffer n) (idx : Fin k →
298298
FlatBuffer n :=
299299
scatterAddSpec (fun _ => IEEE32Exec.posZero) (gatherVecSpec x idx) idx
300300

301+
/-! ## Lookup-table texture lerp -/
302+
303+
/--
304+
Pure spec for the lookup-table fetch of `NN.Runtime.Autograd.Engine.Cuda.TexTable` in *point*
305+
mode, after coordinate decomposition: given a layered table, per-element layer selections, the
306+
two proof-carrying sample indices `j`/`j'` (the clamped `⌊u⌋` and `⌊u⌋+1`), and the float32
307+
fractional weight `f = u − ⌊u⌋`, each output is the guarded float32 lerp
308+
`tab[L][j] + f · (tab[L][j'] − tab[L][j])`.
309+
310+
The coordinate decomposition itself (clamping `u` to `[0, width−1]`, `floor`, the exactness of
311+
`u − ⌊u⌋` in float32) and hardware mode's 9-bit weight quantization live at the native trust
312+
boundary; both are validated bit-level by the executable `Float32` parity tests in
313+
`NN.Tests.Runtime.Cuda.TexTable`, per this file's three-layer split.
314+
-/
315+
def texLerpSpec {w l k : Nat} (tab : Fin l → Fin w → RefScalar)
316+
(layer : Fin k → Fin l) (j j' : Fin k → Fin w) (f : Fin k → RefScalar) : FlatBuffer k :=
317+
fun i =>
318+
IEEE32Exec.add (tab (layer i) (j i))
319+
(IEEE32Exec.mul (f i) (IEEE32Exec.sub (tab (layer i) (j' i)) (tab (layer i) (j i))))
320+
301321
/-! ## Batched row-major matrix multiplication -/
302322

303323
/-- Linear row-major index for `A[b, i, k]` with shape `(batch, m, n)`. -/

NN/Runtime/Autograd/Engine/Cuda/NativeSources.lean

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,21 @@ agrees with CPU stubs and reference cases on the supported path.
9999
- `csrc/cuda/blas/torchlean_dgemm_cuda_stub.c`
100100
Portable CPU mirror of the DGEMM FFI symbol.
101101
102+
- `csrc/cuda/common/torchlean_cuda_textable.h`
103+
Boxed ABI for immutable layered 1-D lookup-table textures (float32) and their fetch/metadata
104+
symbol declarations.
105+
Lean side modules: `NN.Runtime.Autograd.Engine.Cuda.Trusted`,
106+
`NN.Runtime.Autograd.Engine.Cuda.TexTable`.
107+
108+
- `csrc/cuda/textures/torchlean_cuda_textable.cu`
109+
Layered `cudaArray` + texture-object construction and the interpolated fetch kernel (point mode
110+
with an explicit contraction-blocked lerp, or hardware linear filtering).
111+
Lean side module: `NN.Runtime.Autograd.Engine.Cuda.TexTable`.
112+
113+
- `csrc/cuda/textures/torchlean_cuda_textable_stub.c`
114+
Portable CPU mirror of the lookup-table texture FFI surface; point mode is bit-identical to the
115+
CUDA kernel, hardware mode emulates the 9-bit lerp weight.
116+
102117
## What to read next
103118
104119
- `NN.Runtime.Autograd.Engine.Cuda.Float32Contract` states the float32 agreement assumptions.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/-
2+
Copyright (c) 2026 TorchLean
3+
Released under MIT license as described in the file LICENSE.
4+
Authors: TorchLean Team
5+
6+
Layered 1-D lookup-table textures (float32).
7+
8+
Implementation:
9+
- CUDA: `csrc/cuda/textures/torchlean_cuda_textable.cu`
10+
- CPU stub (default `lake build`): `csrc/cuda/textures/torchlean_cuda_textable_stub.c`
11+
-/
12+
13+
module
14+
15+
public import NN.Runtime.Autograd.Engine.Cuda.Trusted
16+
public import NN.Runtime.Autograd.Engine.Cuda.Buffer
17+
18+
@[expose] public section
19+
20+
namespace Runtime
21+
namespace Autograd
22+
namespace Cuda
23+
24+
namespace TexTable
25+
26+
/-!
27+
# Lookup-table textures
28+
29+
A `TexTable` holds a small, immutable, *layered* 1-D table of float32 samples over a uniform
30+
abscissa — tabulated transfer functions, calibration curves, and similar tabulated 1-D functions
31+
whose analytic form is expensive or unavailable (common in signal processing and radar/optical
32+
remote sensing). On the CUDA build it is backed by a layered `cudaArray` bound to a texture
33+
object, so fetches go through the GPU's dedicated texture cache; the default build uses a
34+
host-memory parity stub.
35+
36+
## Coordinate convention
37+
38+
`fetch` evaluates the table by piecewise-linear interpolation at *grid-space* coordinates
39+
`u ∈ [0, width − 1]` (unnormalized; clamped at both ends), in the layer selected by an
40+
integral-valued float index (clamped to `[0, layers − 1]`; layers are **not** interpolated
41+
across). With `i = ⌊clamp(u, 0, width−1)⌋` and `f = u − i`, the result is
42+
`table[layer][i] + f · (table[layer][i+1] − table[layer][i])` — the uniform-grid analogue of
43+
`np.interp`. The native kernels add any texel-center offset internally; callers never add `0.5`.
44+
45+
## Filter modes (fixed at construction)
46+
47+
- `hwFilter := false` (**point mode**, default): two point fetches plus an explicit float32 lerp.
48+
Bit-reproducible — the CUDA build and the CPU stub return identical float32 results.
49+
- `hwFilter := true` (**hardware mode**): the texture unit interpolates in hardware at zero ALU
50+
cost. CUDA specifies a 9-bit fixed-point lerp weight (8 fractional bits), so results carry a
51+
weight-quantization error of at most `2⁻⁸ · |table[i+1] − table[i]|` per fetch and must be
52+
compared by tolerance; the stub emulates the quantized weight.
53+
54+
Tables are forward-only values: fetches record no tape and define no gradient.
55+
-/
56+
57+
/--
58+
Build a table from `layers * width` samples in layer-major order (row `l` occupies
59+
`data[l*width ..< (l+1)*width]`), narrowed to float32. Panics unless `width ≥ 1`, `layers ≥ 1`,
60+
and `data.size = layers * width`.
61+
-/
62+
@[extern "torchlean_cuda_textable_make"]
63+
opaque ofFloatArray (data : @& FloatArray) (width layers : @& Nat) (hwFilter : Bool := false) :
64+
TexTable
65+
66+
/-- Samples per layer. -/
67+
@[extern "torchlean_cuda_textable_width"]
68+
opaque width (t : @& TexTable) : Nat
69+
70+
/-- Number of layers. -/
71+
@[extern "torchlean_cuda_textable_layers"]
72+
opaque layers (t : @& TexTable) : Nat
73+
74+
/-- `true` iff the table was built with hardware filtering (`hwFilter := true`). -/
75+
@[extern "torchlean_cuda_textable_filter_hw"]
76+
opaque filterHw (t : @& TexTable) : Bool
77+
78+
/--
79+
Evaluate the table at per-element grid coordinates `coords` (clamped to `[0, width − 1]`) in the
80+
layers selected by `layerIdx` (integral-valued float32 indices, clamped to `[0, layers − 1]`).
81+
`coords` and `layerIdx` must have equal sizes; the result is a fresh buffer of that size.
82+
-/
83+
@[extern "torchlean_cuda_textable_fetch"]
84+
opaque fetch (t : @& TexTable) (coords layerIdx : @& Buffer) : Buffer
85+
86+
end TexTable
87+
88+
end Cuda
89+
end Autograd
90+
end Runtime

NN/Runtime/Autograd/Engine/Cuda/Trusted.lean

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,26 @@ def Buffer : Type := BufferImpl.val
5151

5252
instance : Nonempty Buffer := BufferImpl.property
5353

54+
/--
55+
Opaque handle to an immutable, layered 1-D lookup table of float32 samples (a CUDA texture object
56+
over a layered `cudaArray` when built with `-K cuda=true`, otherwise a CPU stub table).
57+
58+
Implementation:
59+
- CUDA: `csrc/cuda/textures/torchlean_cuda_textable.cu`
60+
- CPU stub (default `lake build`): `csrc/cuda/textures/torchlean_cuda_textable_stub.c`
61+
-/
62+
opaque TexTableImpl : NonemptyType.{0}
63+
64+
/--
65+
Runtime representation used for native lookup-table texture handles.
66+
67+
As with `Buffer`, the `NonemptyType` wrapper only gives extern declarations a nonempty result
68+
type; values are created only by the native table constructor.
69+
-/
70+
def TexTable : Type := TexTableImpl.val
71+
72+
instance : Nonempty TexTable := TexTableImpl.property
73+
5474
end Cuda
5575
end Autograd
5676
end Runtime

NN/Tests/Runtime/Cuda/Suite.lean

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public import NN.Tests.Runtime.Cuda.MatmulBmm
2121
public import NN.Tests.Runtime.Cuda.Fft
2222
public import NN.Tests.Runtime.Cuda.ViewsBroadcastReduce
2323
public import NN.Tests.Runtime.Cuda.LinearMseConcatSliceGather
24+
public import NN.Tests.Runtime.Cuda.TexTable
2425
public import NN.Tests.Runtime.Cuda.Stress
2526

2627
/-!
@@ -56,6 +57,7 @@ def run : IO Unit := do
5657
Fft.run
5758
ViewsBroadcastReduce.run
5859
LinearMseConcatSliceGather.run
60+
TexTable.run
5961
Stress.run
6062
IO.println "=== CUDA kernel coverage suite completed ==="
6163

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/-
2+
Copyright (c) 2026 TorchLean
3+
Released under MIT license as described in the file LICENSE.
4+
Authors: TorchLean Team
5+
-/
6+
7+
module
8+
9+
public import NN.Runtime.Autograd.Engine.Cuda.Buffer
10+
public import NN.Runtime.Autograd.Engine.Cuda.TexTable
11+
public import NN.Tests.Runtime.Cuda.Utils
12+
13+
/-!
14+
# CUDA Kernel Coverage: Lookup-Table Textures
15+
16+
Validates `Runtime.Autograd.Cuda.TexTable` against an executable `Float32` reference:
17+
18+
- **point mode** must match the reference **bit-for-bit** in both builds (the CPU stub and the
19+
CUDA kernel evaluate the same clamp/floor/lerp in float32 with FMA contraction blocked);
20+
- **hardware mode** is compared by tolerance (`2⁻⁸` of the local sample gap): CUDA's texture unit
21+
uses a 9-bit fixed-point lerp weight whose rounding is unspecified;
22+
- **integer-node fetches** must be bit-exact in *both* modes (the lerp weight is exactly 0 there) —
23+
this probe catches any texel-center (`±0.5`) coordinate-convention error;
24+
- edge behavior: clamping below/above the abscissa range, layer-index clamping, `width = 1`,
25+
and empty coordinate buffers.
26+
-/
27+
28+
@[expose] public section
29+
30+
namespace Tests
31+
namespace Cuda
32+
namespace TexTable
33+
34+
open Runtime.Autograd.Cuda
35+
36+
/-- Float32 clamp mirroring the native kernels' `fminf(fmaxf(u, 0), wmax)`. -/
37+
def clampF32 (x lo hi : Float32) : Float32 :=
38+
if x < lo then lo else if x > hi then hi else x
39+
40+
/--
41+
Executable reference for `TexTable.fetch`, computed in Lean core `Float32` (IEEE binary32, the
42+
same arithmetic as the native kernels). Mirrors the CPU stub statement-for-statement; in point
43+
mode the CUDA kernel is bit-identical by construction, in hardware mode the CUDA texture unit may
44+
round the 9-bit weight differently (hence tolerance).
45+
-/
46+
def refFetch (tab : Array Float32) (width layers : Nat) (hw : Bool)
47+
(u layer : Float32) : Float32 :=
48+
let wmax : Float32 := Float32.ofNat (width - 1)
49+
let uc := clampF32 u 0.0 wmax
50+
let lF := clampF32 (layer + 0.5) 0.0 (Float32.ofNat (layers - 1))
51+
let L := lF.toFloat.toUInt64.toNat
52+
let j := uc.floor
53+
let f := uc - j
54+
let f := if hw then Float32.floor (f * 256.0 + 0.5) / 256.0 else f
55+
let j0 := j.toFloat.toUInt64.toNat
56+
let j1 := if j0 + 1 < width then j0 + 1 else width - 1
57+
let a := tab[L * width + j0]!
58+
let b := tab[L * width + j1]!
59+
a + f * (b - a)
60+
61+
/-- The float64 sample grid used by every test below (3 layers × 7 samples, non-trivial values). -/
62+
def sampleData (width layers : Nat) : FloatArray := Id.run do
63+
let mut a := FloatArray.emptyWithCapacity (width * layers)
64+
for l in [0:layers] do
65+
for i in [0:width] do
66+
let x := Float.ofNat i
67+
let y := Float.ofNat l
68+
a := a.push (0.17 * x - 0.031 * x * x + 0.4 * y + 0.05)
69+
return a
70+
71+
/-- Run one fetch through the native path and the reference, returning both as `Float32`. -/
72+
def runFetch (t : TexTable) (tab32 : Array Float32) (width layers : Nat) (hw : Bool)
73+
(coords layerIdx : FloatArray) : IO (Array Float32 × Array Float32) := do
74+
let out := Buffer.toFloatArray
75+
(Runtime.Autograd.Cuda.TexTable.fetch t (Buffer.ofFloatArray coords)
76+
(Buffer.ofFloatArray layerIdx))
77+
let mut native : Array Float32 := #[]
78+
let mut refv : Array Float32 := #[]
79+
for i in [0:out.size] do
80+
native := native.push (out[i]!).toFloat32
81+
refv := refv.push
82+
(refFetch tab32 width layers hw (coords[i]!).toFloat32 (layerIdx[i]!).toFloat32)
83+
return (native, refv)
84+
85+
/-- Assert bitwise equality of native and reference results. -/
86+
def assertBits (msg : String) (native refv : Array Float32) : IO Unit := do
87+
for i in [0:native.size] do
88+
let x := native[i]!
89+
let y := refv[i]!
90+
unless x.toBits == y.toBits do
91+
throw <| IO.userError
92+
s!"{msg}[{i}]: native {x} (bits {x.toBits}) ≠ reference {y} (bits {y.toBits})"
93+
94+
/-- Assert tolerance agreement of native and reference results. -/
95+
def assertTol (msg : String) (native refv : Array Float32) (tol : Float) : IO Unit := do
96+
for i in [0:native.size] do
97+
let x := (native[i]!).toFloat
98+
let y := (refv[i]!).toFloat
99+
Utils.assertApprox s!"{msg}[{i}]" x y (tol := tol)
100+
101+
def widthN : Nat := 7
102+
def layersN : Nat := 3
103+
104+
/-- Interior, boundary, and out-of-range grid coordinates across all layers. -/
105+
def probeCoords : FloatArray :=
106+
FloatArray.mk #[0.0, 0.25, 1.0, 2.5, 3.75, 5.999, 6.0, -1.5, 9.75, 4.125, 2.875, 0.001]
107+
108+
def probeLayers : FloatArray :=
109+
FloatArray.mk #[0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2]
110+
111+
def runPointMode : IO Unit := do
112+
IO.println "== textable point mode (bit-exact) =="
113+
let data := sampleData widthN layersN
114+
let tab32 := (Array.range data.size).map fun i => (data[i]!).toFloat32
115+
let t := Runtime.Autograd.Cuda.TexTable.ofFloatArray data widthN layersN (hwFilter := false)
116+
unless Runtime.Autograd.Cuda.TexTable.width t == widthN &&
117+
Runtime.Autograd.Cuda.TexTable.layers t == layersN &&
118+
Runtime.Autograd.Cuda.TexTable.filterHw t == false do
119+
throw <| IO.userError "textable point: metadata mismatch"
120+
let (native, refv) ← runFetch t tab32 widthN layersN false probeCoords probeLayers
121+
assertBits "textable point" native refv
122+
123+
def runHardwareMode : IO Unit := do
124+
IO.println "== textable hardware mode (tolerance) =="
125+
let data := sampleData widthN layersN
126+
let tab32 := (Array.range data.size).map fun i => (data[i]!).toFloat32
127+
let t := Runtime.Autograd.Cuda.TexTable.ofFloatArray data widthN layersN (hwFilter := true)
128+
unless Runtime.Autograd.Cuda.TexTable.filterHw t == true do
129+
throw <| IO.userError "textable hw: metadata mismatch"
130+
let (native, refv) ← runFetch t tab32 widthN layersN true probeCoords probeLayers
131+
-- Weight quantization bounds the error by 2⁻⁸ · max |Δ sample| (≈ 0.17 here), plus slack for
132+
-- the unspecified hardware weight rounding.
133+
assertTol "textable hw" native refv (tol := 2.0e-3)
134+
135+
/-- Integer-node fetches return stored samples bit-exactly in both modes (texel-center probe). -/
136+
def runIntegerNodes : IO Unit := do
137+
IO.println "== textable integer nodes (bit-exact, both modes) =="
138+
let data := sampleData widthN layersN
139+
let mut coords : FloatArray := FloatArray.emptyWithCapacity (widthN * layersN)
140+
let mut lidx : FloatArray := FloatArray.emptyWithCapacity (widthN * layersN)
141+
for l in [0:layersN] do
142+
for i in [0:widthN] do
143+
coords := coords.push (Float.ofNat i)
144+
lidx := lidx.push (Float.ofNat l)
145+
for hw in [false, true] do
146+
let t := Runtime.Autograd.Cuda.TexTable.ofFloatArray data widthN layersN (hwFilter := hw)
147+
let out := Buffer.toFloatArray
148+
(Runtime.Autograd.Cuda.TexTable.fetch t (Buffer.ofFloatArray coords)
149+
(Buffer.ofFloatArray lidx))
150+
for i in [0:out.size] do
151+
let got := (out[i]!).toFloat32
152+
let want := (data[i]!).toFloat32
153+
unless got.toBits == want.toBits do
154+
throw <| IO.userError
155+
s!"textable integer node (hw={hw})[{i}]: got {got}, want stored sample {want}"
156+
157+
def runEdges : IO Unit := do
158+
IO.println "== textable edges (width=1, layer clamp, empty) =="
159+
-- width = 1: every coordinate hits the single sample.
160+
let one := FloatArray.mk #[0.75, -0.75]
161+
let t1 := Runtime.Autograd.Cuda.TexTable.ofFloatArray one 1 2 (hwFilter := false)
162+
let outs := Buffer.toFloatArray
163+
(Runtime.Autograd.Cuda.TexTable.fetch t1
164+
(Buffer.ofFloatArray (FloatArray.mk #[0.0, 3.5, -2.0]))
165+
(Buffer.ofFloatArray (FloatArray.mk #[0, 0, 1])))
166+
let expect : Array Float := #[0.75, 0.75, -0.75]
167+
for i in [0:outs.size] do
168+
unless ((outs[i]!).toFloat32).toBits == ((expect[i]!).toFloat32).toBits do
169+
throw <| IO.userError s!"textable width=1[{i}]: got {outs[i]!}, want {expect[i]!}"
170+
-- layer index out of range clamps to the last layer.
171+
let outc := Buffer.toFloatArray
172+
(Runtime.Autograd.Cuda.TexTable.fetch t1
173+
(Buffer.ofFloatArray (FloatArray.mk #[0.0]))
174+
(Buffer.ofFloatArray (FloatArray.mk #[7.0])))
175+
unless ((outc[0]!).toFloat32).toBits == ((-0.75 : Float).toFloat32).toBits do
176+
throw <| IO.userError s!"textable layer clamp: got {outc[0]!}, want -0.75"
177+
-- empty coordinate buffer yields an empty result.
178+
let oute := Buffer.toFloatArray
179+
(Runtime.Autograd.Cuda.TexTable.fetch t1
180+
(Buffer.ofFloatArray (FloatArray.mk #[]))
181+
(Buffer.ofFloatArray (FloatArray.mk #[])))
182+
unless oute.size == 0 do
183+
throw <| IO.userError s!"textable empty: expected empty result, got size {oute.size}"
184+
185+
/-- Unified TexTable test entrypoint. -/
186+
def run : IO Unit := do
187+
runPointMode
188+
runHardwareMode
189+
runIntegerNodes
190+
runEdges
191+
192+
end TexTable
193+
end Cuda
194+
end Tests

csrc/cuda/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ Current CUDA coverage:
136136
| `NN/Tests/Runtime/Cuda/Fft.lean` | Packed real FFT, inverse FFT, spectral convolution, and finite-difference gradient checks. |
137137
| `NN/Tests/Runtime/Cuda/ViewsBroadcastReduce.lean` | Reshape, transpose, rank-3 permutations, broadcast, reduce-sum/mean, and empty-axis behavior. |
138138
| `NN/Tests/Runtime/Cuda/LinearMseConcatSliceGather.lean` | Linear layer, MSE loss, vector concat/slice, scalar gather, row gather, and gradients. |
139+
| `NN/Tests/Runtime/Cuda/TexTable.lean` | Layered 1-D lookup-table textures: bit-exact point-mode lerp, tolerance-gated hardware filtering, integer-node texel-center probes, clamping and empty-buffer edges. |
139140
| `NN/Tests/Runtime/Cuda/Stress.lean` | RNG determinism, explicit buffer release, duplicate-parent gradient accumulation, large buffers, reductions, and cuBLAS rectangular matmul. |
140141
| `NN/Tests/Runtime/Cuda/Suite.lean` | The unified entrypoint imported by the repository-level test suite. |
141142

0 commit comments

Comments
 (0)