Skip to content

Commit 4a89617

Browse files
committed
K2: 2D grid + scf.for runtime K-loop + shared memory tiling
1 parent 04eaf74 commit 4a89617

20 files changed

Lines changed: 1412 additions & 765 deletions

File tree

CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ find_package(LLVM REQUIRED CONFIG)
1111
# LLVM 18 from apt.llvm.org exports zstd::libzstd_shared but libzstd-dev may
1212
# not be installed. Manually define the cmake target if the .so is present.
1313
if(NOT TARGET zstd::libzstd_shared)
14-
find_library(ZSTD_LIB NAMES zstd)
14+
find_library(ZSTD_LIB NAMES zstd zstd.so.1 libzstd.so.1.4.8
15+
HINTS /usr/lib/aarch64-linux-gnu)
1516
if(ZSTD_LIB)
1617
add_library(zstd::libzstd_shared SHARED IMPORTED)
1718
set_target_properties(zstd::libzstd_shared PROPERTIES

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,9 @@ Mirrors [Modular's Blackwell series](https://www.modular.com/blog/matrix-multipl
152152
- [x] Bug fix: `reduce_sum` partial-warp shuffle (`blockSize < 32`) — passes correct `width` to `gpu::ShuffleOp` instead of hardcoded 32
153153
- [x] Bug fix: `emit_mul` scalar promotion — `_promote_scalar` in `jit.py` prevents `TypeError` when a constexpr int is passed as an IR operand
154154
- [x] Benchmark notebook — `examples/gemm_benchmark.ipynb` with cuBLAS reference numbers and gap analysis
155-
- [ ] K0: Naive GEMM — add `//` (FloorDiv) and `%` (Mod) to JIT `BinOp` handler
156-
- [ ] K1: Row GEMM — rename/reframe `tiled_matmul_kernel` in notebook
157-
- [ ] K2: Shmem GEMM — `program_id(1)` (2D grid) + `scf.for` runtime K-loop
155+
- [x] K0: Naive GEMM — `//` (FloorDiv) and `%` (Mod) in JIT `BinOp` handler
156+
- [x] K1: Row GEMM — one block per output row, constexpr K-loop
157+
- [x] K2: Shmem GEMM — `program_id(1)` (2D grid) + `ForRangeOp``scf.for` runtime K-loop + shared memory tiling
158158
- [ ] K3: Swizzled GEMM — 128-byte XOR swizzle to eliminate shmem bank conflicts
159159
- [ ] K4: Vectorized GEMM — `LDG.128` vectorized loads
160160
- [ ] K5: Pipelined GEMM — `cp.async` to overlap load and compute

bindings/python_bindings.cpp

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,30 @@ PYBIND11_MODULE(_tiny_ton_core, m) {
155155
return PyValue{self.emitSharedLoad(idx.val, bufferSize, et)};
156156
},
157157
py::arg("idx"), py::arg("buffer_size"), py::arg("dtype") = "f32")
158+
.def("begin_for_range",
159+
[](tinyton::IRBuilder &self, PyValue start, PyValue stop,
160+
PyValue step, const std::vector<PyValue> &initArgs) {
161+
std::vector<mlir::Value> inits;
162+
for (auto &pv : initArgs)
163+
inits.push_back(pv.val);
164+
auto results =
165+
self.beginForRange(start.val, stop.val, step.val, inits);
166+
std::vector<PyValue> out;
167+
for (auto v : results)
168+
out.push_back(PyValue{v});
169+
return out;
170+
})
171+
.def("end_for_range",
172+
[](tinyton::IRBuilder &self, const std::vector<PyValue> &yieldVals) {
173+
std::vector<mlir::Value> vals;
174+
for (auto &pv : yieldVals)
175+
vals.push_back(pv.val);
176+
auto results = self.endForRange(vals);
177+
std::vector<PyValue> out;
178+
for (auto v : results)
179+
out.push_back(PyValue{v});
180+
return out;
181+
})
158182
.def("emit_ret", [](tinyton::IRBuilder &self) { self.emitRet(); })
159183
.def("dump_mlir",
160184
[](tinyton::IRBuilder &self) {
@@ -201,7 +225,10 @@ PYBIND11_MODULE(_tiny_ton_core, m) {
201225
.def("set_args", &tinyton::SimulatedGPU::setArgs)
202226
.def("write_memory", &tinyton::SimulatedGPU::writeMemory)
203227
.def("read_memory", &tinyton::SimulatedGPU::readMemory)
204-
.def("run", &tinyton::SimulatedGPU::run);
228+
.def("run",
229+
[](tinyton::SimulatedGPU &self, int gridX, int gridY,
230+
int threadsPerBlock) { self.run(gridX, gridY, threadsPerBlock); },
231+
py::arg("grid_x"), py::arg("grid_y"), py::arg("threads_per_block"));
205232

206233
#ifdef TTN_ENABLE_CUDA
207234
m.def("has_cuda", [] { return tinyton::CUDARuntime::isAvailable(); });
@@ -231,13 +258,13 @@ PYBIND11_MODULE(_tiny_ton_core, m) {
231258
})
232259
.def("launch",
233260
[](tinyton::CUDARuntime &self, const std::string &ptx,
234-
const std::string &kernelName, int gridX, int blockX,
261+
const std::string &kernelName, int gridX, int gridY, int blockX,
235262
const std::vector<uintptr_t> &args) {
236263
std::vector<void *> voidArgs;
237264
voidArgs.reserve(args.size());
238265
for (auto a : args)
239266
voidArgs.push_back(reinterpret_cast<void *>(a));
240-
self.launch(ptx, kernelName, gridX, blockX, voidArgs);
267+
self.launch(ptx, kernelName, gridX, gridY, blockX, voidArgs);
241268
});
242269
#else
243270
m.def("has_cuda", [] { return false; });

docs/18-shmem-gemm.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# 18 — K2 Shmem GEMM: 2D Grid + Runtime scf.for + Shared Memory Tiling
2+
3+
## Overview
4+
5+
K2 is the third GEMM kernel in tiny-ton's progression toward cuBLAS-level performance.
6+
It introduces three new compiler features that are independently useful:
7+
8+
| Feature | What it unlocks |
9+
|---|---|
10+
| **2D grid launch**`tt.program_id(1)` | Natural 2D indexing for matrix tiles |
11+
| **Runtime `scf.for` loop**`ForRangeOp` | K can be a runtime argument; no IR explosion |
12+
| **Shared memory tiling**`tt.shared_store / tt.shared_load` | Block-local caching of A and B tiles |
13+
14+
## Algorithm
15+
16+
```
17+
Grid : (M // TM, N // TN) one block per output tile
18+
Block : TK threads
19+
20+
For each block (bm, bn):
21+
acc = 0
22+
for k0 in range(0, K, TK): ← runtime loop, K is not constexpr
23+
shmem_A[0..TK-1] = A[bm, k0..k0+TK-1] ← global → shared
24+
shmem_B[TK..2TK-1] = B[k0..k0+TK-1, bn] ← global → shared
25+
sync()
26+
acc += reduce_sum(shmem_A * shmem_B)
27+
sync()
28+
C[bm, bn] = acc
29+
```
30+
31+
With TM = TN = 1 (current), each block computes **one scalar** of C.
32+
Increasing TM / TN (K3+) harvests the shared memory reuse benefit.
33+
34+
## Architecture: Python DSL → PTX
35+
36+
```
37+
@tt.jit kernel
38+
39+
▼ jit.py: visit_For detects runtime bounds → ForRangeOp
40+
TinyTon IR
41+
tinyton.for_range %start, %K, %step iter_args(%acc = %init) {
42+
^bb(%iv: i32, %acc_: f32): ... tinyton.yield %acc2
43+
}
44+
45+
▼ TinyTonToGPU.cpp: lowerOneOp (recursive lambda)
46+
GPU dialect + SCF
47+
scf.for %iv = %start to %K step %step iter_args(%acc = %init) -> f32 {
48+
... scf.yield %acc2
49+
}
50+
51+
▼ Pipeline.cpp: createConvertSCFToCFPass()
52+
CF dialect
53+
cf.cond_br %done, ^exit, ^body
54+
^body: ... cf.br ^header
55+
56+
▼ CombinedGPULoweringPass → NVPTX backend
57+
PTX
58+
$loop_header:
59+
setp.ge.s32 %p0, %k0, %K
60+
@%p0 bra $loop_exit
61+
...
62+
add.s32 %k0, %k0, 16
63+
bra $loop_header
64+
```
65+
66+
## Compiler changes
67+
68+
### Workstream A — 2D Grid
69+
70+
| File | Change |
71+
|---|---|
72+
| `CUDARuntime.h/.cpp` | `launch(gridX, gridY, blockX, ...)``gridY` was hardcoded 1 |
73+
| `Simulator.h/.cpp` | `run(gridX, gridY, threadsPerBlock)` — PID axis=1 → `blockId / gridX` |
74+
| `python_bindings.cpp` | Updated `launch` and `run` bindings |
75+
| `jit.py` | `_launch_cuda` / `_launch_simulator` read `grid[1]` |
76+
77+
### Workstream B — Runtime scf.for
78+
79+
| File | Change |
80+
|---|---|
81+
| `TinyTonOps.td` | `ForRangeOp` (body region + iter_args) + `YieldOp` |
82+
| `Builder.h/.cpp` | `beginForRange` / `endForRange` |
83+
| `TinyTonToGPU.cpp` | Recursive `lowerOneOp` lambda converts `ForRangeOp``scf.for` |
84+
| `CMakeLists.txt` (Conversion) | `MLIRSCFDialect` |
85+
| `Pipeline.cpp` | `createConvertSCFToCFPass()` before `CombinedGPULoweringPass` |
86+
| `CMakeLists.txt` (Compiler) | `MLIRSCFToControlFlow` |
87+
| `python_bindings.cpp` | `begin_for_range` / `end_for_range` |
88+
| `jit.py` | `visit_For` falls through to `ForRangeOp` when bounds are runtime |
89+
90+
## Kernel source
91+
92+
```python
93+
@tt.jit
94+
def shmem_gemm(A_ptr, B_ptr, C_ptr, M, N, K,
95+
TM: tt.constexpr, TN: tt.constexpr, TK: tt.constexpr):
96+
bm = tt.program_id(0)
97+
bn = tt.program_id(1)
98+
tid = tt.arange(0, TK)
99+
acc = 0.0
100+
for k0 in range(0, K, TK):
101+
a_val = tt.load(A_ptr + bm * TM * K + k0 + tid)
102+
tt.shared_store(tid, a_val, buffer_size=2*TK)
103+
b_val = tt.load(B_ptr + (k0 + tid) * N + bn * TN)
104+
tt.shared_store(TK + tid, b_val, buffer_size=2*TK)
105+
tt.sync()
106+
a_sh = tt.shared_load(tid, buffer_size=2*TK)
107+
b_sh = tt.shared_load(TK + tid, buffer_size=2*TK)
108+
acc = acc + tt.reduce_sum(a_sh * b_sh)
109+
tt.sync()
110+
tt.store(C_ptr + bm * TM * N + bn * TN, acc)
111+
```
112+
113+
## Key result: compile time vs K1
114+
115+
| Kernel | K=128 compile | K=256 compile |
116+
|---|---|---|
117+
| K1 Row GEMM (constexpr unroll) | ~200 ms | **hangs (>60 s)** |
118+
| K2 Shmem GEMM (runtime scf.for) | ~200 ms | ~200 ms |
119+
120+
K2's IR size is **constant** regardless of K — `scf.for` emits a single loop
121+
instead of N×(K/TK) unrolled copies of the body.
122+
123+
## Next: K3 Swizzled GEMM
124+
125+
XOR-swizzle the shared memory addresses to eliminate bank conflicts:
126+
```python
127+
swizzled_idx = idx ^ ((idx >> 3) & 0x7)
128+
tt.shared_store(swizzled_idx, val, buffer_size=2*TK)
129+
```
130+
This requires an address-swizzle helper in the JIT and no new compiler passes.

0 commit comments

Comments
 (0)