Skip to content

Commit a139bca

Browse files
committed
fix docs for static_dispatch
1 parent 73e1e77 commit a139bca

3 files changed

Lines changed: 420 additions & 223 deletions

File tree

docs/static_dispatch.md

Lines changed: 191 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
`StaticDispatch` is a developer-maintained static dispatch table that maps shape conditions directly to pre-determined kernel factories. **No autotune, no benchmarking, no caching** — conditions are evaluated in order, and the first match wins immediately.
66

7+
The dispatch table is designed to be created once at module level and reused across calls. Per-call varying data (tensors, scalars) is passed through a `context` dict, so factories don't need to capture variables via closures.
8+
79
---
810

911
## When to Use
@@ -26,25 +28,37 @@ Signature: `(m: int, n: int, k: int, aligned: bool, **extra: Any) -> bool`
2628

2729
A callable that determines whether the current shape matches this table entry. Conditions are evaluated **in table order**; the first that returns `True` wins.
2830

29-
### Factory (Double-Lambda)
31+
### Factory
3032

31-
Signature: `() -> Callable[[], None]`
33+
Signature: `(context_key1=..., context_key2=..., ...) -> Callable[[], None]`
3234

33-
A zero-arg callable that returns a **runner**. The runner is itself a zero-arg callable that executes the kernel.
35+
A named function (recommended over lambdas) that accepts per-call varying arguments via keyword arguments and returns a **runner**a zero-arg callable that executes the kernel. The arguments are passed from the `context` dict in `lookup_and_build()`.
3436

35-
**Critical**: When used with `@libentry()`-decorated Triton kernels, the factory must use a **double-lambda** wrapper:
37+
When the dispatch table is created at module level, factories are pure references to named functions — no closures, no lambdas rebuilt on every call. The per-call data flows in through `context`.
3638

37-
```python
38-
# Correct: double lambda
39-
lambda: lambda: kernel_fn[grid](arg1, arg2, ...)
40-
# ^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
41-
# factory runner (deferred to runner())
39+
**Critical**: When used with `@libentry()`-decorated Triton kernels, the factory must return the kernel call wrapped in a **lambda**:
4240

43-
# Wrong: single lambda — kernel executes immediately in factory()
44-
lambda: kernel_fn[grid](arg1, arg2, ...)
41+
```python
42+
# Module level — defined once:
43+
def build_my_kernel(A, B, C, alpha, beta, m, n, k, lda, ldb, ldc):
44+
grid = lambda meta: (
45+
triton.cdiv(m, meta["BLOCK_M"]) * triton.cdiv(n, meta["BLOCK_N"]),
46+
)
47+
return lambda: _my_kernel[grid](
48+
A, B, C, alpha, beta, m, n, k, lda, ldb, ldc,
49+
)
50+
51+
# Per-call — context carries the varying data:
52+
runner = dispatch.lookup_and_build(
53+
m, n, k, aligned,
54+
context=dict(A=A, B=B, C=C, m=m, n=n, k=k,
55+
lda=lda, ldb=ldb, ldc=ldc,
56+
alpha=alpha, beta=beta),
57+
)
58+
runner()
4559
```
4660

47-
**Why**: `kernel_fn[grid](args...)` calls `LibEntry.run()`, which launches the kernel immediately and returns a `(kernel, constexprs)` tuple — not a callable runner. The extra `lambda:` defers execution to `runner()` time.
61+
**Why lambda**: `kernel_fn[grid](args...)` calls `LibEntry.run()`, which launches the kernel immediately and returns a `(kernel, constexprs)` tuple — not a callable runner. The `lambda:` defers execution to `runner()` time.
4862

4963
---
5064

@@ -53,16 +67,17 @@ lambda: kernel_fn[grid](arg1, arg2, ...)
5367
Unlike `SizeAutoDispatch` with its multi-tier cache, `StaticDispatch` has a minimal design:
5468

5569
```
56-
lookup_and_build(m, n, k, aligned, **extra)
70+
lookup_and_build(m, n, k, aligned, *, context, **extra)
5771
58-
├─ Entry 1: condition(m,n,k,aligned)? ─── True → factory() → runner
59-
├─ Entry 2: condition(m,n,k,aligned)? ─── True → factory() → runner
72+
├─ Entry 1: condition(m,n,k,aligned)? ─── True → factory(**context) → runner
73+
├─ Entry 2: condition(m,n,k,aligned)? ─── True → factory(**context) → runner
6074
├─ ...
6175
└─ No match → raise ValueError
6276
```
6377

6478
- **No filtering logic** — conditions encode all matching criteria inline (no separate `aligned`/`filter` params)
6579
- **No cache** — every call re-evaluates conditions (cheap, just boolean logic)
80+
- **`context` dict** — passes per-call varying data (tensors, scalars) to factories, so the dispatch table itself can live at module level
6681
- **Throw on miss** — if no entry matches, raises `ValueError` (the last entry should always be a catch-all)
6782

6883
---
@@ -82,7 +97,7 @@ dispatch = StaticDispatch(table)
8297
### lookup_and_build()
8398

8499
```python
85-
runner = dispatch.lookup_and_build(m, n, k, aligned, **extra)
100+
runner = dispatch.lookup_and_build(m, n, k, aligned, *, context=None, **extra)
86101
runner()
87102
```
88103

@@ -92,6 +107,7 @@ runner()
92107
| `n` | `int` | N dimension |
93108
| `k` | `int` | K dimension |
94109
| `aligned` | `bool` | Whether inputs are memory-aligned |
110+
| `context` | `dict` or `None` | Per-call varying data (tensors, scalars, etc.) passed as keyword arguments to the matched factory. When `None`, factories are called with no arguments. |
95111
| `**extra` || Additional keyword arguments passed to each condition |
96112

97113
**Returns**: `Callable[[], None]` — zero-arg runner; calling it executes the selected kernel.
@@ -102,82 +118,119 @@ runner()
102118

103119
## Real-World Example: hgemm NN
104120

105-
This example comes from [hopper/ops/gemm.py](../src/flag_blas/runtime/backend/_nvidia/hopper/ops/gemm.py), the `hgemm` function's NN branch:
121+
This example comes from [hopper/ops/gemm.py](../src/flag_blas/runtime/backend/_nvidia/hopper/ops/gemm.py), the `hgemm` function's NN branch.
122+
123+
### Module level — defined once
106124

107125
```python
108126
from flag_blas.runtime.dispatch import StaticDispatch
109127
from triton.tools.tensor_descriptor import TensorDescriptor
110128

111-
dispatch = StaticDispatch([
112-
# ── Priority 1 (highest) ─────────────────────────────────────
113-
# Skinny matrix with aligned large dimensions.
114-
# Uses kernel4 with TensorDescriptor and hardcoded optimal config
115-
# (no autotune needed — config proven optimal for this shape class).
116-
(
117-
lambda m, n, k, aligned, **_kw:
118-
aligned and (m * n > 2048 * 2048) and min(m, n) >= 64
119-
and ((m >= 16384 and max(n, k) <= 2048)
120-
or (n >= 16384 and max(m, k) <= 2048)),
121-
lambda: lambda: _hgemm_nn_kernel4[(
122-
triton.cdiv(m, 128) * triton.cdiv(n, 256),
123-
)](
124-
TensorDescriptor(
125-
base=A, shape=[m, k], strides=[lda, 1],
126-
block_shape=[128, 64],
127-
),
128-
TensorDescriptor(
129-
base=B, shape=[k, n], strides=[ldb, 1],
130-
block_shape=[64, 256],
131-
),
132-
TensorDescriptor(
133-
base=C, shape=[m, n], strides=[ldc, 1],
134-
block_shape=[128, 256],
135-
),
136-
alpha, beta, m, n, k, beta_is_zero,
137-
BLOCK_M=128, BLOCK_N=256, BLOCK_K=64, GROUP_M=8,
138-
num_stages=4, num_warps=8, num_ctas=1,
139-
),
140-
),
141-
142-
# ── Priority 2 ───────────────────────────────────────────────
143-
# Aligned + large dimensions (m×n > 4M, min dim ≥ 64).
144-
# Uses kernel3 with TensorDescriptor and autotuned configs
145-
# (its @libtuner picks the best BLOCK_M/N/K etc. at runtime).
146-
(
147-
lambda m, n, k, aligned, **_kw:
148-
aligned and (m * n > 2048 * 2048) and min(m, n) >= 64,
149-
lambda: lambda: _hgemm_nn_kernel3[grid](
150-
A, B, C, alpha, beta, m, n, k,
151-
lda, ldb, ldc, beta_is_zero,
129+
# ── Condition predicates (named functions, not lambdas) ──────────
130+
131+
def _hgemm_nn_is_skinny_aligned_large(m, n, k, aligned, **_kw):
132+
return (
133+
aligned
134+
and (m * n > 2048 * 2048)
135+
and min(m, n) >= 64
136+
and (
137+
(m >= 16384 and max(n, k) <= 2048)
138+
or (n >= 16384 and max(m, k) <= 2048)
139+
)
140+
)
141+
142+
def _hgemm_nn_is_aligned_large(m, n, k, aligned, **_kw):
143+
return aligned and (m * n > 2048 * 2048) and min(m, n) >= 64
144+
145+
def _hgemm_nn_is_aligned_small(m, n, k, aligned, **_kw):
146+
return aligned and max(m, n) <= 1024
147+
148+
def _hgemm_nn_is_default(**_kw):
149+
return True
150+
151+
# ── Factory functions (accept context dict keys as kwargs) ───────
152+
153+
def _hgemm_nn_build_kernel4(A, B, C, m, n, k, lda, ldb, ldc,
154+
alpha, beta, beta_is_zero):
155+
return lambda: _hgemm_nn_kernel4[(
156+
triton.cdiv(m, 128) * triton.cdiv(n, 256),
157+
)](
158+
TensorDescriptor(
159+
base=A, shape=[m, k], strides=[lda, 1],
160+
block_shape=[128, 64],
152161
),
153-
),
154-
155-
# ── Priority 3 ───────────────────────────────────────────────
156-
# Aligned + small/moderate dimensions (max ≤ 1024).
157-
# Uses kernel2 with block_ptr and autotuned configs.
158-
(
159-
lambda m, n, k, aligned, **_kw:
160-
aligned and max(m, n) <= 1024,
161-
lambda: lambda: _hgemm_nn_kernel2[grid](
162-
A, B, C, alpha, beta, m, n, k,
163-
lda, ldb, ldc, beta_is_zero,
162+
TensorDescriptor(
163+
base=B, shape=[k, n], strides=[ldb, 1],
164+
block_shape=[64, 256],
164165
),
165-
),
166-
167-
# ── Priority 4 (catch-all) ───────────────────────────────────
168-
# Everything else: unaligned or moderate/large but not covered above.
169-
# Uses the original level3 kernel with pointer-based access.
170-
(
171-
lambda **_kw: True,
172-
lambda: lambda: _hgemm_nn_kernel[grid](
173-
A, B, C, alpha, beta, m, n, k,
174-
lda, ldb, ldc, beta_is_zero,
166+
TensorDescriptor(
167+
base=C, shape=[m, n], strides=[ldc, 1],
168+
block_shape=[128, 256],
175169
),
176-
),
170+
alpha, beta, m, n, k, beta_is_zero,
171+
BLOCK_M=128, BLOCK_N=256, BLOCK_K=64, GROUP_M=8,
172+
num_stages=4, num_warps=8, num_ctas=1,
173+
)
174+
175+
def _hgemm_nn_build_kernel3(A, B, C, m, n, k, lda, ldb, ldc,
176+
alpha, beta, beta_is_zero):
177+
grid = lambda meta: (
178+
triton.cdiv(m, meta["BLOCK_M"]) * triton.cdiv(n, meta["BLOCK_N"]),
179+
)
180+
return lambda: _hgemm_nn_kernel3[grid](
181+
A, B, C, alpha, beta, m, n, k, lda, ldb, ldc, beta_is_zero,
182+
)
183+
184+
def _hgemm_nn_build_kernel2(A, B, C, m, n, k, lda, ldb, ldc,
185+
alpha, beta, beta_is_zero):
186+
grid = lambda meta: (
187+
triton.cdiv(m, meta["BLOCK_M"]) * triton.cdiv(n, meta["BLOCK_N"]),
188+
)
189+
return lambda: _hgemm_nn_kernel2[grid](
190+
A, B, C, alpha, beta, m, n, k, lda, ldb, ldc, beta_is_zero,
191+
)
192+
193+
def _hgemm_nn_build_kernel(A, B, C, m, n, k, lda, ldb, ldc,
194+
alpha, beta, beta_is_zero):
195+
grid = lambda meta: (
196+
triton.cdiv(m, meta["BLOCK_M"]) * triton.cdiv(n, meta["BLOCK_N"]),
197+
)
198+
return lambda: _hgemm_nn_kernel[grid](
199+
A, B, C, alpha, beta, m, n, k, lda, ldb, ldc, beta_is_zero,
200+
)
201+
202+
_HGEMM_NN_DISPATCH = StaticDispatch([
203+
# skinny + aligned large → kernel4 (TensorDescriptor, hardcoded config)
204+
(_hgemm_nn_is_skinny_aligned_large, _hgemm_nn_build_kernel4),
205+
# aligned large → kernel3 (TensorDescriptor, autotuned config)
206+
(_hgemm_nn_is_aligned_large, _hgemm_nn_build_kernel3),
207+
# aligned small → kernel2 (block_ptr)
208+
(_hgemm_nn_is_aligned_small, _hgemm_nn_build_kernel2),
209+
# default → kernel (original)
210+
(_hgemm_nn_is_default, _hgemm_nn_build_kernel),
177211
])
212+
```
178213

179-
runner = dispatch.lookup_and_build(m, n, k, aligned)
180-
runner()
214+
### Per-call — inside `hgemm()`
215+
216+
```python
217+
def hgemm(transa, transb, m, n, k, alpha, A, lda, B, ldb, beta, C, ldc):
218+
# ... validation ...
219+
beta_is_zero = beta == 0.0
220+
aligned = _is_gemm_aligned(A, lda, B, ldb, C, ldc)
221+
222+
with torch_device_fn.device(A.device):
223+
if transa == CUBLAS_OP_N and transb == CUBLAS_OP_N:
224+
runner = _HGEMM_NN_DISPATCH.lookup_and_build(
225+
m, n, k, aligned,
226+
context=dict(
227+
A=A, B=B, C=C, m=m, n=n, k=k,
228+
lda=lda, ldb=ldb, ldc=ldc,
229+
alpha=alpha, beta=beta, beta_is_zero=beta_is_zero,
230+
),
231+
)
232+
runner()
233+
# ... other transa/transb branches ...
181234
```
182235

183236
### Dispatch Logic Summary
@@ -206,7 +259,7 @@ runner()
206259
2. **Ordered matching**: Conditions evaluated top-to-bottom; first `True` wins.
207260
3. **Catch-all required**: The last entry must match any shape (prevent `ValueError`).
208261
4. **Mutually exclusive conditions**: Entries should not overlap to make behavior predictable. Higher-priority entries should have more specific conditions.
209-
5. **Double-lambda factories**: Required when using `@libentry()`-decorated Triton kernels. The inner `lambda:` defers `LibEntry.run()` to `runner()` time.
262+
5. **Named functions, not lambdas in the table**: Conditions and factories should be module-level named functions referenced by name in the `StaticDispatch` table. Per-call varying data (tensors, scalars) is passed via the `context` dict to `lookup_and_build()`. This avoids recreating closures on every call.
210263

211264
---
212265

@@ -241,37 +294,68 @@ runner()
241294

242295
### Common Patterns
243296

244-
**Priority by dimension**:
297+
**Priority by dimension** (module-level named functions):
245298
```python
246-
[
247-
(lambda m, *_kw, **__: m > 8192, factory_a), # very large
248-
(lambda m, *_kw, **__: m > 1024, factory_b), # large
249-
(lambda m, *_kw, **__: m > 256, factory_c), # medium
250-
(lambda **_kw: True, factory_d), # small
251-
]
299+
def is_very_large(m, **_kw):
300+
return m > 8192
301+
302+
def is_large(m, **_kw):
303+
return m > 1024
304+
305+
def is_medium(m, **_kw):
306+
return m > 256
307+
308+
def is_default(**_kw):
309+
return True
310+
311+
_DISPATCH = StaticDispatch([
312+
(is_very_large, build_kernel_a),
313+
(is_large, build_kernel_b),
314+
(is_medium, build_kernel_c),
315+
(is_default, build_kernel_d),
316+
])
252317
```
253318

254319
**Priority by alignment**:
255320
```python
256-
[
257-
(lambda aligned, **_kw: aligned and is_large(**kw), aligned_large_factory),
258-
(lambda aligned, **_kw: aligned, aligned_small_factory),
259-
(lambda **_kw: True, unaligned_factory),
260-
]
321+
def is_aligned_large(aligned, m, n, k, **_kw):
322+
return aligned and (m * n > 2048 * 2048)
323+
324+
def is_aligned_only(aligned, **_kw):
325+
return aligned
326+
327+
def is_default(**_kw):
328+
return True
329+
330+
_DISPATCH = StaticDispatch([
331+
(is_aligned_large, build_aligned_large),
332+
(is_aligned_only, build_aligned),
333+
(is_default, build_fallback),
334+
])
261335
```
262336

263337
**Combined dimensions + alignment** (as in hgemm_nn):
264338
```python
265-
[
266-
(lambda aligned, m, n, k, **_kw:
267-
aligned and meets_criteria_A(m, n, k),
268-
factory_a),
269-
(lambda aligned, m, n, k, **_kw:
270-
aligned and meets_criteria_B(m, n, k),
271-
factory_b),
272-
(lambda **_kw: True,
273-
fallback_factory),
274-
]
339+
def is_skinny_aligned_large(m, n, k, aligned, **_kw):
340+
return (aligned and (m * n > 2048 * 2048) and min(m, n) >= 64
341+
and ((m >= 16384 and max(n, k) <= 2048)
342+
or (n >= 16384 and max(m, k) <= 2048)))
343+
344+
def is_aligned_large(m, n, k, aligned, **_kw):
345+
return aligned and (m * n > 2048 * 2048) and min(m, n) >= 64
346+
347+
def is_aligned_small(m, n, k, aligned, **_kw):
348+
return aligned and max(m, n) <= 1024
349+
350+
def is_default(**_kw):
351+
return True
352+
353+
_DISPATCH = StaticDispatch([
354+
(is_skinny_aligned_large, build_kernel4),
355+
(is_aligned_large, build_kernel3),
356+
(is_aligned_small, build_kernel2),
357+
(is_default, build_kernel),
358+
])
275359
```
276360

277361
---
@@ -308,6 +392,6 @@ The exception propagates uncaught. Keep condition lambdas simple (boolean arithm
308392

309393
Yes. For example, use `StaticDispatch` for well-understood shape classes and `SizeAutoDispatch` for the remainder. Just ensure you return the runner from the appropriate dispatch.
310394

311-
### Q: Why double-lambda for @libentry kernels?
395+
### Q: Why return a lambda from the factory?
312396

313-
`@libentry()` wraps a Triton `JITFunction` in a `LibEntry` object. Calling `entry[grid](args...)` triggers `LibEntry.run()`, which compiles (if needed), launches the kernel, and returns `(kernel_obj, constexprs)`. The inner `lambda:` wraps this into a callable runner without executing the kernel.
397+
`@libentry()` wraps a Triton `JITFunction` in a `LibEntry` object. Calling `entry[grid](args...)` triggers `LibEntry.run()`, which compiles (if needed), launches the kernel, and returns `(kernel_obj, constexprs)`. The returned `lambda:` wraps this into a callable runner without executing the kernel immediately.

0 commit comments

Comments
 (0)