kernel-fun 0.2.0.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. kernel_fun-0.2.0.dev1/.github/workflows/release.yml +95 -0
  2. kernel_fun-0.2.0.dev1/.gitignore +31 -0
  3. kernel_fun-0.2.0.dev1/ALGORITHM.md +229 -0
  4. kernel_fun-0.2.0.dev1/INTEGRATION.md +222 -0
  5. kernel_fun-0.2.0.dev1/LICENSE +201 -0
  6. kernel_fun-0.2.0.dev1/NOTICE +60 -0
  7. kernel_fun-0.2.0.dev1/PKG-INFO +347 -0
  8. kernel_fun-0.2.0.dev1/README.md +314 -0
  9. kernel_fun-0.2.0.dev1/THIRD_PARTY_NOTICES.md +175 -0
  10. kernel_fun-0.2.0.dev1/pyproject.toml +120 -0
  11. kernel_fun-0.2.0.dev1/src/kernel_fun/__init__.py +67 -0
  12. kernel_fun-0.2.0.dev1/src/kernel_fun/_common/__init__.py +11 -0
  13. kernel_fun-0.2.0.dev1/src/kernel_fun/_common/cache.py +99 -0
  14. kernel_fun-0.2.0.dev1/src/kernel_fun/_common/compat.py +168 -0
  15. kernel_fun-0.2.0.dev1/src/kernel_fun/_common/support.py +267 -0
  16. kernel_fun-0.2.0.dev1/src/kernel_fun/cconv/__init__.py +24 -0
  17. kernel_fun-0.2.0.dev1/src/kernel_fun/cconv/_kernels/__init__.py +6 -0
  18. kernel_fun-0.2.0.dev1/src/kernel_fun/cconv/_kernels/strip.py +450 -0
  19. kernel_fun-0.2.0.dev1/src/kernel_fun/cconv/_provenance.py +11 -0
  20. kernel_fun-0.2.0.dev1/src/kernel_fun/cconv/ops.py +256 -0
  21. kernel_fun-0.2.0.dev1/src/kernel_fun/kda/__init__.py +23 -0
  22. kernel_fun-0.2.0.dev1/src/kernel_fun/kda/_kernels/__init__.py +7 -0
  23. kernel_fun-0.2.0.dev1/src/kernel_fun/kda/_kernels/bwd_dhu.py +994 -0
  24. kernel_fun-0.2.0.dev1/src/kernel_fun/kda/_kernels/bwd_intra.py +1089 -0
  25. kernel_fun-0.2.0.dev1/src/kernel_fun/kda/_kernels/bwd_intra_triton.py +328 -0
  26. kernel_fun-0.2.0.dev1/src/kernel_fun/kda/_kernels/bwd_scan.py +1105 -0
  27. kernel_fun-0.2.0.dev1/src/kernel_fun/kda/_kernels/bwd_wy.py +320 -0
  28. kernel_fun-0.2.0.dev1/src/kernel_fun/kda/_kernels/bwd_wy_t.py +309 -0
  29. kernel_fun-0.2.0.dev1/src/kernel_fun/kda/_kernels/fwd_intra_triton.py +101 -0
  30. kernel_fun-0.2.0.dev1/src/kernel_fun/kda/_kernels/fwd_state.py +1065 -0
  31. kernel_fun-0.2.0.dev1/src/kernel_fun/kda/_provenance.py +18 -0
  32. kernel_fun-0.2.0.dev1/src/kernel_fun/kda/autograd.py +114 -0
  33. kernel_fun-0.2.0.dev1/src/kernel_fun/kda/chain.py +150 -0
  34. kernel_fun-0.2.0.dev1/src/kernel_fun/kda/ops.py +342 -0
  35. kernel_fun-0.2.0.dev1/tests/__init__.py +0 -0
  36. kernel_fun-0.2.0.dev1/tests/_inputs.py +105 -0
  37. kernel_fun-0.2.0.dev1/tests/conftest.py +46 -0
  38. kernel_fun-0.2.0.dev1/tests/test_cconv.py +368 -0
  39. kernel_fun-0.2.0.dev1/tests/test_cta_policy.py +172 -0
  40. kernel_fun-0.2.0.dev1/tests/test_fallback.py +250 -0
  41. kernel_fun-0.2.0.dev1/tests/test_parity.py +116 -0
  42. kernel_fun-0.2.0.dev1/tests/test_runtime.py +187 -0
  43. kernel_fun-0.2.0.dev1/tools/drift.py +135 -0
  44. kernel_fun-0.2.0.dev1/tools/graphprobe.py +168 -0
  45. kernel_fun-0.2.0.dev1/tools/prodtime.py +118 -0
  46. kernel_fun-0.2.0.dev1/tools/vendor.py +234 -0
@@ -0,0 +1,95 @@
1
+ # Publishing kernel-fun to PyPI.
2
+ #
3
+ # Auth is PyPI Trusted Publishing (OIDC): no API token is stored in this repo or in GitHub
4
+ # secrets. PyPI matches four claims out of the token GitHub mints for this job -- owner,
5
+ # repository, THIS FILENAME, and the environment named below. Renaming this file, or
6
+ # renaming an environment, breaks publishing with a claims mismatch until the publisher
7
+ # entry on PyPI is edited to match. See README.md "Releasing".
8
+ #
9
+ # TestPyPI workflow_dispatch, target=testpypi -> environment `testpypi`
10
+ # PyPI push a `v*` tag (or dispatch) -> environment `pypi`
11
+ #
12
+ # A version on PyPI can never be reused, so the build job refuses a tag that disagrees with
13
+ # `__version__`; the tag is the thing humans read, and the metadata is what actually ships.
14
+ name: Release
15
+
16
+ on:
17
+ push:
18
+ tags: ["v*"]
19
+ workflow_dispatch:
20
+ inputs:
21
+ target:
22
+ description: "Index to publish to"
23
+ type: choice
24
+ options: [testpypi, pypi]
25
+ default: testpypi
26
+
27
+ permissions: {}
28
+
29
+ jobs:
30
+ build:
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - uses: actions/checkout@v7
34
+ - uses: astral-sh/setup-uv@v10.1.0
35
+ with:
36
+ python-version: "3.12"
37
+
38
+ - name: Build sdist and wheel
39
+ run: uv build
40
+
41
+ # Metadata that fails here fails AFTER upload otherwise, and by then the version is
42
+ # burned. Cheap gate: PyPI's own renderer/validator, run before anything is sent.
43
+ - name: Check metadata
44
+ run: uvx twine check --strict dist/*
45
+
46
+ # The wheel's version comes from src/kernel_fun/__init__.py. If someone tags v0.3.0
47
+ # without bumping it, the upload succeeds and ships 0.2.0 under a 0.3.0 tag -- an
48
+ # inconsistency that cannot be corrected in place. Refuse instead.
49
+ - name: Tag agrees with __version__
50
+ if: startsWith(github.ref, 'refs/tags/v')
51
+ run: |
52
+ tag="${GITHUB_REF_NAME#v}"
53
+ built=$(ls dist/*.whl | sed -E 's/.*kernel_fun-(.+)-py3-none-any\.whl/\1/')
54
+ echo "tag=$tag built=$built"
55
+ [ "$tag" = "$built" ] || { echo "::error::tag v$tag != __version__ $built"; exit 1; }
56
+
57
+ - uses: actions/upload-artifact@v7
58
+ with:
59
+ name: dist
60
+ path: dist/
61
+ if-no-files-found: error
62
+
63
+ testpypi:
64
+ needs: build
65
+ if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
66
+ runs-on: ubuntu-latest
67
+ environment:
68
+ name: testpypi
69
+ url: https://test.pypi.org/p/kernel-fun
70
+ permissions:
71
+ id-token: write # mint the OIDC token PyPI trades for a short-lived upload token
72
+ steps:
73
+ - uses: actions/download-artifact@v8
74
+ with:
75
+ name: dist
76
+ path: dist/
77
+ - uses: pypa/gh-action-pypi-publish@v1.14.2
78
+ with:
79
+ repository-url: https://test.pypi.org/legacy/
80
+
81
+ pypi:
82
+ needs: build
83
+ if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.target == 'pypi')
84
+ runs-on: ubuntu-latest
85
+ environment:
86
+ name: pypi
87
+ url: https://pypi.org/p/kernel-fun
88
+ permissions:
89
+ id-token: write
90
+ steps:
91
+ - uses: actions/download-artifact@v8
92
+ with:
93
+ name: dist
94
+ path: dist/
95
+ - uses: pypa/gh-action-pypi-publish@v1.14.2
@@ -0,0 +1,31 @@
1
+ # Build artifacts. This repo, unlike the research ladder, IS installable, so these are
2
+ # reachable here in a way they were not there.
3
+ dist/
4
+ build/
5
+ *.egg-info/
6
+
7
+ __pycache__/
8
+ *.py[cod]
9
+ .pytest_cache/
10
+ .ruff_cache/
11
+
12
+ # Generated Triton artifacts. The training image points TRITON_CACHE_DIR outside the tree;
13
+ # a native run on a workstation would land here.
14
+ .triton-cache/
15
+
16
+ # A .venv inside the tree is how the old repo lost a day: it shadowed the container's conda
17
+ # python and an image that definitely had torch reported "No module named torch". Install
18
+ # this package INTO the image's python (`pip install .`), or put its src on PYTHONPATH.
19
+ .venv/
20
+ .python-version
21
+
22
+ # macOS Finder droppings.
23
+ .DS_Store
24
+ *.swp
25
+
26
+ # Profiler captures: 5-50MB each and git history is forever. tools/prodtime.py and
27
+ # tools/graphprobe.py print their numbers; the captures leave a session by rsync.
28
+ *.ncu-rep
29
+ *.nsys-rep
30
+ *.qdrep
31
+ *.sqlite
@@ -0,0 +1,229 @@
1
+ # KDA — the math these kernels implement
2
+
3
+ Everything below is fla 0.5.2's own decomposition (`fla/ops/kda/{chunk_fwd.py,chunk_bwd.py,
4
+ chunk_intra.py,wy_fast.py}` and `fla/ops/common/chunk_delta_h.py`), stated precisely so each
5
+ port has one written contract. All logs are base-2 (fla multiplies the raw log-space gate by
6
+ `RCP_LN2 = 1/ln2` in the cumsum), all gates are ≤ 0 and decreasing down a chunk, and `BT` is
7
+ the chunk size (64), `BC = 16` the sub-chunk. Per (batch b, value-head hv); q/k live at head
8
+ `h = hv // (HV//H)`. `scale = K^-0.5`.
9
+
10
+ This package ports the forward scan+readout and four of the backward's seven stages out of
11
+ fla. Everything else is fla's, called stage by stage from `kda/chain.py` — which is the
12
+ authoritative table of who owns what; this file is the math each stage implements.
13
+
14
+ | where | module | replaces |
15
+ |---|---|---|
16
+ | fwd, Aqk zero-fill | `kda/_kernels/fwd_intra_triton.py` | a `masked_fill` over the whole tile |
17
+ | fwd, scan + o | `kda/_kernels/fwd_state.py` | `chunk_gated_delta_rule_fwd_h` + the o stage |
18
+ | bwd 2, re-scan (B1) | `kda/_kernels/bwd_scan.py` | `chunk_gated_delta_rule_fwd_h` |
19
+ | bwd 4, dhu (B2a) | `kda/_kernels/bwd_dhu.py` | `chunk_gated_delta_rule_bwd_dhu` |
20
+ | bwd 5, wy_dqkg (B2b) | `kda/_kernels/bwd_wy.py` | `chunk_kda_bwd_wy_dqkg_fused` |
21
+ | bwd 6, intra | `kda/_kernels/bwd_intra.py` (+`bwd_intra_triton.py`) | `chunk_kda_bwd_intra` |
22
+ | bwd 7, dg cumsum | — | folded into stage 6's epilogue |
23
+
24
+ The gate activation (`-exp(A_log)·softplus(g + dt_bias)`, when `use_gate_in_kernel`) is
25
+ fused into stage 1's cumsum via fla's `kda_gate_chunk_cumsum`, not run as eager fp32 torch
26
+ ops. At prod8192 the eager form is four passes over a [B,T,HV,K] fp32 tensor — ~2ms and
27
+ ~2GiB of saved activations *per layer*, against 1.9ms for the entire fused stage.
28
+
29
+ ---
30
+
31
+ # Forward
32
+
33
+ ## Stage 1 — cumsum (fla, stays Triton)
34
+
35
+ g2[t, d] = (1/ln2) * sum_{s <= t, s in chunk(t)} g[s, d] fp32 [B, T, HV, K]
36
+
37
+ Chunk-local inclusive cumsum, per dimension. `G[c, d] = g2[last row of chunk c, d]` is the
38
+ per-chunk total decay — a K-vector, where a scalar-gate op (gdn) would have a scalar.
39
+
40
+ ## Stage 2 — intra / WY (fla, stays Triton)
41
+
42
+ `chunk_kda_fwd_intra` produces, per chunk (row indices i, j within the chunk):
43
+
44
+ Aqk[i, j] = scale * sum_d q[i,d] * k[j,d] * exp2(g2[i,d] - g2[j,d]) for i >= j, else 0
45
+ Akk = the solved lower-triangular WY inverse, beta folded in
46
+ w[i, :] = (Akk @ (beta * exp2(g2) * k))[i, :] bf16 [B, T, HV, K]
47
+ u[i, :] = (Akk @ (beta * v))[i, :] bf16 [B, T, HV, V]
48
+ kg[i, d] = k[i,d] * exp2(G[d] - g2[i,d]) bf16 [B, T, HV, K]
49
+ qg = q * exp2(g2) (only materialized when disable_recompute; None on our path)
50
+
51
+ Two things to internalize:
52
+
53
+ - **`scale` ships inside Aqk** (the intra kernels multiply it in), while the `q @ h` readout
54
+ term gets `scale` applied in the o stage. Don't apply it twice.
55
+ - **The per-dim gate sits INSIDE Aqk's dot product.** `exp2(g2_i - g2_j)` would be a scalar
56
+ under a per-head gate, so it could be factored out of one MMA and applied afterwards in
57
+ SIMT. Per-dim, that factorization needs `exp2(+g2_j)` on one operand — unbounded — so fla
58
+ computes Aqk in sub-chunks (BC=16) where relative gates stay bounded. That is Triton work
59
+ worth keeping: the CuTe kernel LOADS Aqk instead of computing it.
60
+
61
+ ## Stage 3+4 — scan + o (`kda/_kernels/fwd_state.py`, fused)
62
+
63
+ Serial over chunks c, state h [K, V] fp32:
64
+
65
+ v'_c = u_c - w_c @ h_c (MMA "WH" + subtract)
66
+ o_c = scale * (q_c * exp2(g2_c)) @ h_c + Aqk_c @ v'_c (MMAs "OH" + "OI")
67
+ h_{c+1}[d, :] = exp2(G_c[d]) * h_c[d, :] + (kg_c^T @ v'_c)[d, :] (MMA "DH" + decay)
68
+
69
+ after the last chunk, `ht = h` (fp32, TMA store). fla stores v' (`v_new`) and per-chunk h to
70
+ HBM between its two kernels; the fusion keeps both in smem/tmem — that traffic is the win.
71
+
72
+ Notes that cost time to rediscover: the state decay multiply indexes the tmem fragment's row
73
+ (Ld16x256b lanes span rows r and r+8), and OH's q operand must be pre-gated in SIMT
74
+ (`qg = q·exp2(g2)`, per-dim) from the q tile plus a g2 tile (fp32 [BT,K], 32KB/stage smem).
75
+
76
+ ---
77
+
78
+ # Backward
79
+
80
+ Residuals from the forward: `(q, k, v, g2, beta, Aqk, Akk, h0)` — exactly what fla's own
81
+ autograd Function saves when `disable_recompute=False`. The forward must preserve that
82
+ rounding contract: Aqk/Akk stored bf16 by intra, g2 the fla cumsum, v_new/h recomputed by
83
+ the backward itself.
84
+
85
+ One deliberate deviation, and it is fla's own behaviour rather than ours: under
86
+ `use_gate_in_kernel`, **g2 is not saved**. The backward recomputes it from the raw (and
87
+ half-size) `g` with one kernel launch, which is what fla's recompute path does — saving it
88
+ would cost 1GiB per layer at prod8192 for something a single launch reproduces.
89
+
90
+ ## The seven launches
91
+
92
+ 1. **recompute** (`recompute_w_u_fwd`): w = Akk @ (β·exp2(g2)·k), u = Akk @ (β·v),
93
+ qg = q·exp2(g2), kg = k·exp2(G−g2). All bf16 [B,T,HV,K/V].
94
+ 2. **re-scan** (`chunk_gated_delta_rule_fwd_h`): the forward recurrence again, this time
95
+ materializing every per-chunk state: h_c [K,V] checkpoints (bf16, [B,NT,HV,K,V]) and
96
+ v'_c = u_c − w_c @ h_c (`v_new`). Serial over chunks.
97
+ 3. **dAv** (`chunk_kda_bwd_dAv`): dAqk[r,s] = scale·Σ_v do[r,v]·v'[s,v] (lower tri, fp32
98
+ [B,T,HV,BT]); dv[r,:] = Σ_{s≥r} Aqk[s,r]·do[s,:] (Aqk^T @ do, upper-masked).
99
+ 4. **dhu** (`chunk_gated_delta_rule_bwd_dhu`): the reverse scan. State dh [K,V] fp32; fla
100
+ folds the per-chunk decay on the K axis, updates dv (dv2 = dv + w-side term) per chunk,
101
+ and emits dh checkpoints (bf16 [B,NT,HV,K,V]) + dh0. Serial, reverse.
102
+ 5. **wy_dqkg** (`chunk_kda_bwd_wy_dqkg_fused`), per chunk, consuming h_c and dh_c:
103
+ - dq[r,d] = scale·exp2(g2[r,d])·Σ_v do[r,v]·h_c[d,v] (h consumer)
104
+ - dk[r,d] = exp2(G[d]−g2[r,d])·Σ_v v'[r,v]·dh_c[d,v] + WY corrections (dh consumer)
105
+ - dw[r,d] = −Σ_v dv[r,v]·h_c[d,v]; folded via Akk into dk/db/dAkk (h consumer)
106
+ - dgk[d] += exp2(G[d])·Σ_v h_c[d,v]·dh_c[d,v] (last row of chunk) (both)
107
+ - dv2 = β·(Akk^T @ dv); db += rowsum terms; dAkk (strict lower, fp32)
108
+ - dg[r,d] = q·dq − k·dk + last-row dgk + kg·(Akk@dw)·β (the inter-chunk part)
109
+ 6. **intra** (`chunk_kda_bwd_intra`): the intra-chunk dAqk/dAkk consumers — **the dominant
110
+ stage, half of fla's backward**; see below.
111
+ 7. **dg cumsum**: dg = chunk-local REVERSE cumsum of dg (fp32).
112
+
113
+ Ours are 2, 4, 5 and 6, and 7 disappears into 6's epilogue. Stages 2/4/5 are the "B1/B2"
114
+ fusions listed as standing headroom in the first version of this file: B1 re-runs the
115
+ forward scan with h checkpointed via TMA store off the critical path, B2a keeps dh^T
116
+ tmem-resident through the reverse scan, and B2b is a full-K restructure of fla's fused
117
+ kernel — the same math over one K slab instead of NK, so v_new/do/dv are read once each
118
+ instead of four times.
119
+
120
+ GVA (HV > H): dq/dk are produced at HV and reduced to H after stage 6.
121
+
122
+ ## Where the time and bytes go (prod8192 = B16 × T8192 × H16, K128/V256, b300)
123
+
124
+ Stage times measured 2026-08-18: 25.3ms total — intra 12.62, wy_dqkg 6.15, dhu 2.22,
125
+ fwd_h 1.88, recompute 1.18, dAv 0.77, dg cumsum 0.51. Inter-stage HBM: h 2.1GB + dh 2.1GB
126
+ + v_new 1.1GB + w/u/qg/kg 2.7GB + dv 2×1.1GB + fp32 dq/dk/dg 3.2GB + dAqk/dAkk 1.1GB.
127
+
128
+ ## Stage 6, bwd_intra — half the backward
129
+
130
+ ### What it computes
131
+
132
+ Adds the intra-chunk gradient parts, given dAqk and dAkk (both fp32, strictly-masked). With
133
+ r, s chunk-local rows, all per (b, hv, chunk), and every gate factor per-dim d:
134
+
135
+ dq[r,d] += Σ_{s ≤ r} dAqk[r,s] · k[s,d] · exp2(g2[r,d] − g2[s,d])
136
+ dwk[r,d] = Σ_{s ≤ r} dAkk[r,s] · k[s,d] · exp2(g2[r,d] − g2[s,d])
137
+ db[r] += Σ_d dwk[r,d] · k[r,d]
138
+ dk[s,d] += Σ_{r ≥ s} (dAqk[r,s]·q[r,d] + dAkk[r,s]·β_r·k[r,d]) · exp2(g2[r,d] − g2[s,d])
139
+ + β_s · dwk[s,d]
140
+ dg[r,d] += q[r,d]·dq_intra[r,d] + β_r·dwk[r,d]·k[r,d] − dkt[r,d]·k[r,d]
141
+ (dkt = the Σ_{r ≥ s} term above, i.e. the column-side accumulation)
142
+
143
+ ### The numerics law (do not relitigate)
144
+
145
+ Every exp2 argument above is `g2[r,d] − g2[s,d]` with r ≥ s — one-sided, ≤ 0, safe at any
146
+ gate magnitude. KDA's real init makes decay ~16 log2 units PER STEP (`exp(A_log) ∈ [1,16]`),
147
+ so any factorization through a reference row m — `exp2(g_r − g_m)·exp2(g_m − g_s)` — has one
148
+ factor of the pair unbounded unless m sits between r and s. On a diagonal block no single m
149
+ does, which is why fla's diagonals are scalar j-loops and why the SAFE_GATE midpoint trick
150
+ carries a bounded-gate contract this op does not satisfy. Cross-sub-chunk blocks ARE safe
151
+ (any boundary between the blocks separates all (r,s) pairs) — fla already exploits that with
152
+ MMAs there. Falsified before: two-sided midpoint/binary-tree diagonal forms — they overflow
153
+ (a production NaN, 2026-08-17) or don't win. `test_kimi_delta_attention_cute_extreme_decay`
154
+ in `src/test/nn/attention/kda_test.py` is the guard for exactly this class.
155
+
156
+ ### Why fla's version costs 12.6ms
157
+
158
+ Grid (NK·NC, NT, B·HV) = (16, 128, 256) → **524k CTAs of [BC=16, BK=32] work**:
159
+
160
+ - **Stream multiplicity.** Each of the NK=4 K-slabs re-reads the same [BC,BC] dAqk/dAkk
161
+ tiles (fp32 → 4× the 1.1GB), and each of the NC=4 sub-chunk owners re-reads its j-loop
162
+ partners' q/k/g tiles. db is written as an NK-slab and reduced afterwards.
163
+ - **exp2 volume.** ~1.1e10 exp2 at prod8192 → ~2.5ms of pure SFU even at perfect
164
+ utilization, and utilization is poor at [16,32] granularity. The *inherent* one-sided
165
+ minimum is ~4× smaller (~2.3e9, ~0.5–0.6ms) IF each factor is computed once and shared
166
+ between the dAqk/dAkk products AND between the row-side (dq) and column-side (dk/dg)
167
+ passes. fla shares the first pair but recomputes across passes — they live in different
168
+ CTAs.
169
+ - **Latency.** 16-iteration serial scalar loops of tiny loads with nothing to overlap them.
170
+
171
+ Floors: traffic ~7.5GB at one-read-one-write → ~1.1ms; exp2 ~0.6ms (overlappable). The
172
+ honest target is ~2.5–3ms; fla is 5× above it.
173
+
174
+ ### What `kda/_kernels/bwd_intra.py` does instead (6.34ms SIMT; ~5.0ms with the MMA path)
175
+
176
+ One CTA per (chunk, b·hv), 512 threads as (32 d-lanes) × (16 row-lanes) — one warp spans a
177
+ full row. Each thread walks one row per 16-block with mirror pairing (even blocks r0+rlane,
178
+ odd blocks r0+15−rlane) so diagonal work is uniform per thread, and owns 4 **strided**
179
+ columns d = lane + 32c (consecutive-column ownership was 4-way bank-conflicted on every
180
+ scalar load — 27ms). q/k/g2/dq/dk/dg and the [64,64] dA tiles are read once, dq2/dk2/dg2
181
+ written once, db completed in-CTA: the 4× multiplicities are gone.
182
+
183
+ Cross-block pairs factor through the **s-block end** boundary e(s) = 16(j+1):
184
+ `exp2(g_r − g_s) = exp2(g_r − g_e)·exp2(g_e − g_s)`, both exponents ≤ 0 at any gate
185
+ magnitude (r ≥ e > s on a decreasing g) — the same safety class as fla's own `kg` operand.
186
+ Diagonal pairs (same 16-block) keep exactly one one-sided exp2 per (r,s,d): nothing inside a
187
+ diagonal block is ever factorized. All three prescale arrays (kb, qb, kbb) are built in one
188
+ phase up front, so the two sweeps fuse per block with no inter-sweep barrier.
189
+
190
+ Outputs land ~1e-7..2e-6 abs of a fp64 reference, where fla's tf32 `tl.dot`s land 3e-4..1.3e-3.
191
+
192
+ The shipped kernel additionally runs the **cross-sub-chunk** blocks on tcgen05 MMAs rather
193
+ than SIMT (the diagonals stay scalar j-loops — see the numerics law above, which is not
194
+ negotiable), and folds two things into its epilogue that fla spends separate work on: the
195
+ chunk-local reverse cumsum of dg (deleting stage 7 entirely) and the bf16 cast of dq/dk,
196
+ which makes the wrapper's casts no-ops. The dg fold is unconditional; the bf16 emit is
197
+ gated on `HV == H`, because the GVA reduction that follows must sum in fp32.
198
+
199
+ Constraints: K=128, BT=64, fixed-length, no `safe_gate`, and a grid of at least 1024 CTAs
200
+ (smaller grids underfill a 148-SM box and the per-call marshaling isn't amortized — T512
201
+ regressed to 0.90× without the gate). Off that box the wrapper falls back to
202
+ `bwd_intra_triton.py` (the same math restructured in Triton, 9.26ms, K ≤ 128), which falls
203
+ back to fla's kernel for varlen/`safe_gate`. **That floor is per stage, not per call**: a
204
+ shape can clear `is_supported` and still run this one stage on Triton, which is why the
205
+ test arms in `src/test/nn/attention/kda_test.py` are sized to clear it explicitly.
206
+
207
+ ### Register-pressure notes for whoever edits the CuTe kernel
208
+
209
+ ptxas left alone targets 64 registers (dynamic smem hides the 1-CTA/SM cap from it) and
210
+ spills 1–2KB/thread. `min_blocks_per_mp=1` plus `--maxrregcount=128` (frozen as `_MAXREG`)
211
+ and *un*-unrolling the inner 16-iteration loops (`cutlass.range(unroll=4..8)`; full unroll
212
+ lets ptxas hoist whole load batches) gets `LOCAL_SIZE_BYTES=0`. The incoming-grad gmem reads
213
+ cost 3.25ms read-at-use; prefetching them a compute-section ahead is worth ~3ms. 1024
214
+ threads was falsified twice (0.45×, 64-reg spills), as was holding P1/P2 in smem (27.5ms).
215
+
216
+ ## Standing headroom
217
+
218
+ The B1/B2 fusions listed here originally are now stages 2, 4 and 5. What is left, in the
219
+ order the ladder is working on it:
220
+
221
+ - **recompute_w_u** (stage 1, 2 × 1.17ms) and the forward cumsum (0.32ms) — both fla's,
222
+ both counter-blocked on the profiling box.
223
+ - **wy_dqkg** at 4.99ms is now the largest single stage.
224
+ - Remaining intra headroom is ~4ms (latency-bound loads/prescale/epilogue at 16 warps/SM).
225
+
226
+ None of these are edited here. This directory is a vendored snapshot; kernel work happens
227
+ in the `kernel-fun-2` ladder, where a bench row can say whether an idea was worth keeping —
228
+ three of idea 004's did not survive that test.
229
+ - `recompute_w_u`/`dAv`/cumsum (2.4ms combined) are near their traffic floor — leave them.
@@ -0,0 +1,222 @@
1
+ # Landing this in OLMo-core
2
+
3
+ The last time kernels from this repo landed in the training repo, the ladder reported −3%
4
+ tokens/sec and it took two days to establish that there was no steady-state regression at
5
+ all (the ladder repo's `kernels/gnorm/LADDER-REGRESSION.md`). Almost none of that time went
6
+ into the kernels.
7
+ This checklist exists so that does not happen twice.
8
+
9
+ ## 1. The call site
10
+
11
+ **The unit of sharing is this package** (state as of 2026-09-08; the repo split below is
12
+ 2026-09-09). OLMo-core gets it one of two ways, on two branches that are otherwise
13
+ identical:
14
+
15
+ - `caleb/cute-kda-vendored` — the package tree copied to `src/olmo_core/kernel_fun/`,
16
+ byte-identical to this repo's `src/kernel_fun/` except the top-level `__init__.py`, which
17
+ carries `VENDORED_FROM`. No private dependency, so no tokens. scaling-ladders'
18
+ `caleb/cute-kda` points here.
19
+ - `caleb/cute-kda` (PR #837) — installs the package as a `kernel-fun` extra pinned to a sha.
20
+ Needs a GitHub token everywhere the lock resolves.
21
+
22
+ > **The shas in both places changed meaning on 2026-09-09**, when this package moved out of
23
+ > the ladder repo — which held the name `allenai/kernel-fun` until that day and is now
24
+ > `allenai/kernel-fun-dev`, and where this tree was `packages/kernel-fun/` — into the repo
25
+ > that took the name, **`allenai/kernel-fun`** (this one). `VENDORED_FROM` and the pinned
26
+ > extra now name a commit HERE, and the extra's URL drops
27
+ > `#subdirectory=packages/kernel-fun`. The last pre-split vendoring was ladder sha
28
+ > `7a6983b`, which exists only in `kernel-fun-dev`; the ladder sha a family's *kernels*
29
+ > were cut from lives on in `src/kernel_fun/<family>/_provenance.py::SOURCE_COMMIT`
30
+ > (`6fc6309` today) and is a different number. When re-vendoring, bump `VENDORED_FROM` to a
31
+ > full kernel-fun sha and say so in the commit message — a bare sha is now ambiguous.
32
+
33
+ The extra's URL, for reference:
34
+
35
+ ```
36
+ kernel-fun @ git+https://github.com/allenai/kernel-fun.git@<sha>
37
+ ```
38
+
39
+ Three rules for that extra, from reading OLMo-core's build (2026-09-09):
40
+
41
+ - **Bare `kernel-fun`, no `[cu13]`/`[cu12]`.** OLMo-core's images already carry the CuTe
42
+ DSL through the `fa4` extra (flash-attn-4 -> nvidia-cutlass-dsl), and the CUDA major is
43
+ decided once, in the Dockerfile (`CUDA_VERSION_PATH`, and `FLASH_ATTN_4_EXTRAS='[cu13]'`
44
+ on the B300 image). One extra then works on both the cu128 and cu130 images; the
45
+ package's own CUDA extras are for environments that have no DSL at all. Nothing in the
46
+ bare install replaces the image's torch/triton/fla — the floors are below both images.
47
+ - **The git URL cannot sit in the extra** if `ai2-olmo-core` is to keep publishing: PyPI
48
+ rejects metadata with a direct-URL requirement (the pyproject says so at `dion`). Put
49
+ the source in `[tool.uv.sources]` as `dion` does, or publish kernel-fun to PyPI and pin
50
+ a version.
51
+ - **fla must be 0.5.2 on the branch that merges.** The package requires
52
+ `fla-core>=0.5.2`; a branch still on flash-linear-attention 0.4.1 (the B300 image
53
+ branch, today) would end up with a mixed `fla/` tree.
54
+
55
+ If a cu130 image is ever built with FA4's bare requirement, the package logs a
56
+ `CUDA 12 one (nvidia-cutlass-dsl-libs-cu12 without -cu13)` warning at first use — see
57
+ `support.cute_cuda_mismatch`. It warns rather than falls back, because the wrong-build DSL
58
+ may still compile; treat the line as a build-arg bug.
59
+
60
+ Both route `flash_linear_attn_api.py::dispatch_chunk_kda` and `dispatch_causal_conv1d` to
61
+ the package when `KimiDeltaAttentionConfig.use_cute_kernel=True`; that one flag drives BOTH
62
+ families (it is plumbed to the three Q/K/V `CausalConv1d`s). There is no other copy of
63
+ these kernels in OLMo-core — the old frozen `nn/attention/kda_cute/` tree went three ideas
64
+ stale before it was deleted, which is why the package exists. Do not reintroduce one.
65
+
66
+ **To ship a release:** commit/push **kernel-fun** (this repo — tag it if the version
67
+ moved); on OLMo-core's vendored branch re-copy the changed files, bump `VENDORED_FROM` to
68
+ the full kernel-fun sha, bump the scaling-ladders submodule; on the package branch bump
69
+ the sha in the extra and re-lock. Then §2–§3 below. Nothing in this flow touches the ladder
70
+ repo — a ladder commit only ever enters through `tools/vendor.py` (README, "Two repos").
71
+
72
+ The kda call site, for reference:
73
+
74
+ ```python
75
+ # flash_linear_attn_api.py
76
+ if use_cute_kernel:
77
+ from kernel_fun.kda import chunk_kda as cute_chunk_kda
78
+
79
+ return cute_chunk_kda(
80
+ q=q, k=k, v=v, g=g, beta=beta, A_log=A_log, dt_bias=dt_bias,
81
+ scale=scale, initial_state=initial_state,
82
+ output_final_state=output_final_state,
83
+ use_qk_l2norm_in_kernel=use_qk_l2norm_in_kernel,
84
+ use_gate_in_kernel=use_gate_in_kernel,
85
+ cu_seqlens=cu_seqlens,
86
+ )
87
+ ```
88
+
89
+ The package decides internally and falls back to fla itself, so a packed-document batch or
90
+ a non-Blackwell node needs no branch in the caller (the old `cute_kda_supported` is gone).
91
+
92
+ **The conv call site**, same file, is the second family. `CausalConv1d.forward`
93
+ (`src/olmo_core/nn/convolution.py`) reaches the package through `dispatch_causal_conv1d`:
94
+
95
+ ```python
96
+ # flash_linear_attn_api.py
97
+ def dispatch_causal_conv1d(x, weight, bias, activation, backend="triton", cu_seqlens=None):
98
+ from kernel_fun.cconv import causal_conv1d
99
+
100
+ return causal_conv1d(
101
+ x=x, weight=weight, bias=bias, activation=activation, backend=backend,
102
+ cu_seqlens=cu_seqlens,
103
+ )
104
+ ```
105
+
106
+ No flag: the package forwards anything off its box (bias, cu_seqlens, `activation=None`,
107
+ `backend="cuda"`) to fla itself, and `KERNEL_FUN_CCONV_DISABLE=1` is the whole-family
108
+ switch. Like the kda entry point, this one IS `torch.compiler.disable`d — as of
109
+ 2026-09-04. It was not, on the theory that fla's conv is plain Python around an
110
+ autograd.Function and so is ours, so Dynamo would treat the two alike. The 30M mainline
111
+ ladder falsified that: Dynamo speculated OUR Function's backward (two tensor arguments is
112
+ the shape it agrees to trace; fla's eleven-argument one is not) and asserted inside
113
+ `cconv_bwd` on a symbolic `dy.stride(0)`. So this entry point costs ONE graph break that
114
+ fla does not — expect §3's break diff to show it, at three convs per KDA layer, and judge
115
+ it against the ~36 ms/step the kernels return rather than against zero.
116
+
117
+ Three things the caller should still do:
118
+
119
+ ```python
120
+ kernel_fun.kda.warmup(K=head_dim, V=head_v_dim, HV=n_v_heads) # before step 1
121
+ kernel_fun.cconv.warmup(B=microbatch, T=seq_len, D=(key_dim, value_dim)) # ditto
122
+ ```
123
+
124
+ Do NOT call `versions()` from the model's `forward` to log it. Both entry points log it
125
+ themselves, once, the first time either family engages — and they are
126
+ `torch.compiler.disable`d, so the line costs no graph break. The caller-side version of
127
+ this cost two breaks in the KDA layer's compiled block and raised
128
+ `TypeError: unhashable type: 'dict'` through olmo-core's `lru_cache`-based `log_once`
129
+ (2026-09-04). `versions()` stays public for a startup log outside any compiled region.
130
+
131
+ The cconv autotune key is `(D, W, B, T)`, so its warmup must see the REAL microbatch and
132
+ sequence length and every channel count the layer convolves (q/k at `n_heads*head_dim`,
133
+ v at `expand_v` times that). A shape it has not seen re-autotunes at first use: a few
134
+ seconds, once, per shape — fine for training, but it is what a step-1 "regression" looks
135
+ like.
136
+
137
+ The old wrapper computed the gate activation in eager torch. Do not reintroduce that: pass
138
+ `use_gate_in_kernel=True` and let the package fuse it into the cumsum. At prod8192 the
139
+ eager form costs ~2 ms and ~2 GiB of saved activations per layer, which is a third of what
140
+ these kernels win.
141
+
142
+ ## 2. Before the first real run
143
+
144
+ - [ ] `pytest -q` from this repo on a B300, green (`-m slow` adds the prod-shaped rows;
145
+ the cconv tests also run on an H100).
146
+ - [ ] `PYTHONPATH=src python tools/prodtime.py` on that B300 — the package's own numbers at
147
+ the production shapes, next to fla's, from the installed copy. (The ladder's
148
+ `python -m loop bench kda --impl 000,005 --case prod8192_gate` and
149
+ `... bench cconv --impl 000,001 --case prod8192_qk` are the recorded originals.)
150
+ - [ ] Confirm fla's Triton autotune cache directory is persistent and pre-warmed in the
151
+ training image. Otherwise every rank re-autotunes at step 1 and the startup cost you
152
+ just moved with `warmup()` comes back through another door.
153
+ - [ ] Grep the startup log for the `kernel-fun kda: engaged` line (and `cconv: engaged`).
154
+ If either says `falling back` instead, the reason is on the same line — read it
155
+ before looking at anything else. The `kernel-fun {...}` versions line appears just
156
+ above the first of them; if it is missing, neither family ever engaged.
157
+
158
+ ## 3. The A/B that settles it
159
+
160
+ Three arms, same GPU, same node, first step discarded, enough metric windows to be real
161
+ (the 1.4b gnorm run was killed after 7 windows, and half the "−3%" came from reading the
162
+ average anyway):
163
+
164
+ 1. fla (`use_cute_kernel=False`, conv routed to fla)
165
+ 2. kernel-fun, both families
166
+ 3. kernel-fun with `KERNEL_FUN_DISABLE=1`
167
+
168
+ Arm 3 is the control that separates *the wrapper* from *the kernels*: it runs the same
169
+ call path and the same fallback logic but fla's kernels underneath. If arm 3 is not within
170
+ noise of arm 1, the problem is integration, not numerics, and no amount of kernel work will
171
+ fix it. If arm 2 disappoints, `KERNEL_FUN_KDA_DISABLE=1` / `KERNEL_FUN_CCONV_DISABLE=1`
172
+ split it by family without a config change.
173
+
174
+ A fourth arm exists only for a model whose KDA grid is below the chain-level floor —
175
+ `B * HV * (V//64) < 256`, which the small OLMoE3 candidate's B4/HV8/V256 is, at 128.
176
+ `KERNEL_FUN_KDA_MIN_CTAS=128` moves that floor so those calls dispatch here instead of
177
+ reporting `grid too small` and going to fla. It is a measured opt-in for one shape on one
178
+ box, not a better default: the b1 scan and dhu backwards keep their own 256-CTA floors and
179
+ stay on fla at that grid, so the arm is worth running only against arm 1, on the model you
180
+ intend to train, and only after the log stops saying `grid too small`. Set it for
181
+ `warmup()` too — it warms one grid per floor in play, and the stages this arm adds are
182
+ autotuned in step 1 otherwise.
183
+
184
+ Then:
185
+
186
+ - [ ] **`TORCH_LOGS=recompiles,graph_breaks`, diff arm 1 against arm 2.** Free — the Beaker
187
+ launcher sets it already. At the kda call the counts must be EQUAL: fla's own
188
+ `chunk_kda` is `@torch.compiler.disable`d too, so that one MOVES a break rather than
189
+ adding one. At the conv, expect arm 2 to show exactly ONE more break per conv call
190
+ (three per KDA layer) — see §1; more than that, or a recompile loop, is a bug.
191
+ Do this first; it is the cheapest way to catch the failure mode that cost two days.
192
+ - [ ] **torch-profile ~20 steps, compare CPU gap time between GPU kernels** across arms.
193
+ That is where a host-side regression shows up and no benchmark in this repo can see
194
+ it. While you are in the profile, check that no `softplus` / `mul` / `to_copy` at
195
+ `[B,T,HV,K]` survives — if they do, the gate is not fused and you are paying twice.
196
+ And check the conv: `cconv_fwd_strip` / `cconv_bwd_strip` should be the only conv
197
+ kernels, and `causal_conv1d_fwd_kernel` should appear NOWHERE in the backward (fla
198
+ re-ran the forward there to rematerialize the pre-activation; ours does not).
199
+ - [ ] **`torch.cuda.memory_summary()` after ~5 steps**, both arms. Active memory should be
200
+ within a few hundred MiB. The call caches used to pin ~24 GiB/rank at these shapes;
201
+ that is fixed and tested, and this is the check that keeps it fixed.
202
+ - [ ] Report `throughput/device/TPS (actual avg)` with step 1 discarded.
203
+
204
+ ## 4. What to expect
205
+
206
+ **kda.** At the production call the op measures **1.540x** (23.74 ms vs fla's 36.56 at
207
+ B=16, T=8192, H=HV=16, K=128, V=256; 2026-09-02). The 1.288x port was worth 1.037x
208
+ end-to-end on the 1.4b mainline ladder, which puts KDA at ~16% of step time; the same
209
+ arithmetic gives **~1.06x** here, i.e. about +6% tokens/sec. Relative to the branch's
210
+ current 1.545x chain the new wy stage is +0.03x on the op — real, small, and the same
211
+ arithmetic says ~+0.3 points of TPS. Do not expect to see it in a noisy A/B.
212
+
213
+ **cconv.** The 810m/B300 trace (the ladder's `profiles/810m-b300-20260901.md`) put the three conv calls
214
+ at ~50 ms of a ~520 ms step: bwd 35.0 + fwd 7.7 + the backward's own forward re-run 6.9.
215
+ At the measured 4.65x / 1.62x that is ~13 ms — **~36 ms/step back, about 7% of the step,
216
+ ~1.075x tokens/sec at 810m**. The conv channel count is set by head count, which the
217
+ ladder pins from 810m up, so the fraction survives scale. This is the larger of the two
218
+ predictions and the easier one to see.
219
+
220
+ Both predictions assume the ops' step-time fractions are unchanged, and they are the
221
+ numbers to check against the A/B rather than to quote from. Judge the port on its fraction
222
+ of real step time — that is the one durable lesson from last time.