kernel-fun 0.2.0.dev1__py3-none-any.whl
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.
- kernel_fun/__init__.py +67 -0
- kernel_fun/_common/__init__.py +11 -0
- kernel_fun/_common/cache.py +99 -0
- kernel_fun/_common/compat.py +168 -0
- kernel_fun/_common/support.py +267 -0
- kernel_fun/cconv/__init__.py +24 -0
- kernel_fun/cconv/_kernels/__init__.py +6 -0
- kernel_fun/cconv/_kernels/strip.py +450 -0
- kernel_fun/cconv/_provenance.py +11 -0
- kernel_fun/cconv/ops.py +256 -0
- kernel_fun/kda/__init__.py +23 -0
- kernel_fun/kda/_kernels/__init__.py +7 -0
- kernel_fun/kda/_kernels/bwd_dhu.py +994 -0
- kernel_fun/kda/_kernels/bwd_intra.py +1089 -0
- kernel_fun/kda/_kernels/bwd_intra_triton.py +328 -0
- kernel_fun/kda/_kernels/bwd_scan.py +1105 -0
- kernel_fun/kda/_kernels/bwd_wy.py +320 -0
- kernel_fun/kda/_kernels/bwd_wy_t.py +309 -0
- kernel_fun/kda/_kernels/fwd_intra_triton.py +101 -0
- kernel_fun/kda/_kernels/fwd_state.py +1065 -0
- kernel_fun/kda/_provenance.py +18 -0
- kernel_fun/kda/autograd.py +114 -0
- kernel_fun/kda/chain.py +150 -0
- kernel_fun/kda/ops.py +342 -0
- kernel_fun-0.2.0.dev1.dist-info/METADATA +347 -0
- kernel_fun-0.2.0.dev1.dist-info/RECORD +30 -0
- kernel_fun-0.2.0.dev1.dist-info/WHEEL +4 -0
- kernel_fun-0.2.0.dev1.dist-info/licenses/LICENSE +201 -0
- kernel_fun-0.2.0.dev1.dist-info/licenses/NOTICE +60 -0
- kernel_fun-0.2.0.dev1.dist-info/licenses/THIRD_PARTY_NOTICES.md +175 -0
kernel_fun/__init__.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""kernel-fun — CuTe/Triton kernels for linear-attention ops on Blackwell.
|
|
2
|
+
|
|
3
|
+
Each op family is a subpackage exporting a drop-in for its flash-linear-attention
|
|
4
|
+
counterpart, with the same signature and the same return contract:
|
|
5
|
+
|
|
6
|
+
from kernel_fun.kda import chunk_kda # the KDA chunk kernel (CuTe + Triton)
|
|
7
|
+
from kernel_fun.cconv import causal_conv1d # the KDA short conv + silu (Triton)
|
|
8
|
+
# gdn, gnorm to follow into the same shell
|
|
9
|
+
|
|
10
|
+
A call the package does not implement — wrong architecture, wrong shape, a flag we have
|
|
11
|
+
never seen, a CUDA graph capture of a shape that has not run eagerly yet — is forwarded to
|
|
12
|
+
fla verbatim, so installing this can change how fast a model trains but not what it
|
|
13
|
+
computes beyond kernel-level rounding.
|
|
14
|
+
|
|
15
|
+
Nothing is imported eagerly: `import kernel_fun` must stay cheap on a machine with no GPU,
|
|
16
|
+
so torch, triton and the CuTe DSL are pulled in by the family that needs them.
|
|
17
|
+
|
|
18
|
+
Two repos, and changes go to different ones: this package is developed at
|
|
19
|
+
github.com/allenai/kernel-fun (the release artifact — and the only route into
|
|
20
|
+
OLMo-core), while the kernels are researched in github.com/allenai/kernel-fun-dev (the ladder,
|
|
21
|
+
`kernels/<family>/ideas/*`) and cross over by `tools/vendor.py`. A `kernels/...` path in a
|
|
22
|
+
comment here is a path in the LADDER. See the pkg repo's README, "Two repos".
|
|
23
|
+
|
|
24
|
+
Two environment switches, read per call and documented in the README:
|
|
25
|
+
KERNEL_FUN_DISABLE=1 forward everything to fla (per family: _KDA_, _CCONV_, ...)
|
|
26
|
+
KERNEL_FUN_DEBUG=1 log why a call fell back
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
__all__ = ["__version__", "versions"]
|
|
32
|
+
|
|
33
|
+
__version__ = "0.2.0.dev1"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def versions() -> dict[str, str]:
|
|
37
|
+
"""Everything that decides what this package computes and how fast.
|
|
38
|
+
|
|
39
|
+
Worth logging once per training run: two of these (the CuTe DSL and cuda-python) are
|
|
40
|
+
not pinned by any requirement and arrive with the base image, and no bench row in the
|
|
41
|
+
research repo records them either. When a run is slower than the last one, this is the
|
|
42
|
+
first thing to diff.
|
|
43
|
+
"""
|
|
44
|
+
import importlib
|
|
45
|
+
|
|
46
|
+
out: dict[str, str] = {"kernel_fun": __version__}
|
|
47
|
+
for name, mod in (
|
|
48
|
+
("torch", "torch"),
|
|
49
|
+
("triton", "triton"),
|
|
50
|
+
("fla", "fla"),
|
|
51
|
+
("cutlass", "cutlass"),
|
|
52
|
+
("cuda-python", "cuda.bindings"),
|
|
53
|
+
):
|
|
54
|
+
try:
|
|
55
|
+
m = importlib.import_module(mod)
|
|
56
|
+
out[name] = str(getattr(m, "__version__", "unknown"))
|
|
57
|
+
except Exception:
|
|
58
|
+
out[name] = "not installed"
|
|
59
|
+
try:
|
|
60
|
+
import torch
|
|
61
|
+
|
|
62
|
+
if torch.cuda.is_available():
|
|
63
|
+
cap = torch.cuda.get_device_capability()
|
|
64
|
+
out["device"] = f"{torch.cuda.get_device_name()} sm{cap[0]}{cap[1]}"
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
return out
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Machinery every op family in this package shares.
|
|
2
|
+
|
|
3
|
+
Deliberately small, and deliberately not a place for op logic: the call cache, the support
|
|
4
|
+
predicate, the fla compatibility probe, and once-per-process logging. Those four are what
|
|
5
|
+
the kda / gdn / gnorm chains all needed independently — and each had its own copy in the
|
|
6
|
+
research tree, which is how one leak fix ended up written for gnorm and missing from the
|
|
7
|
+
other two for two weeks.
|
|
8
|
+
|
|
9
|
+
Nothing here imports torch or cutlass at module scope beyond what it must, so importing
|
|
10
|
+
`kernel_fun` on a machine without a GPU stays cheap.
|
|
11
|
+
"""
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""The CuTe call cache: marshal once, poke the pointer, launch.
|
|
2
|
+
|
|
3
|
+
Every CuTe kernel in this package drives its compiled object through the same three
|
|
4
|
+
mechanisms, and they live here once rather than copy-pasted per kernel (which is how they
|
|
5
|
+
exist in the research tree, and how the keepalive bug below survived in four places at
|
|
6
|
+
once):
|
|
7
|
+
|
|
8
|
+
cute_view — a torch tensor as a cute.Tensor with the mode order cute wants and the
|
|
9
|
+
batch-ish modes marked dynamic, so one compile serves every B/T.
|
|
10
|
+
retarget — rewrite the descriptor's data pointer for this call. Marshaling costs
|
|
11
|
+
~0.07ms per kernel; a ctypes word-write costs nothing.
|
|
12
|
+
release_keepalives — drop the DLPack reference to the tensor that exported the view.
|
|
13
|
+
|
|
14
|
+
The last one is not an optimization, it is a leak fix: `from_dlpack` requires the consumer
|
|
15
|
+
to keep the exporter alive, so a cached view pins the FIRST call's tensor for the life of
|
|
16
|
+
the process. Measured on the kda chain at B8/T1024/H8/HV8/K128/V256: 778 MiB pinned across
|
|
17
|
+
its four cache entries, ~24 GiB at prod8192 shapes, never freed — which is exactly the
|
|
18
|
+
condition that pushes the caching allocator into device-synchronizing cudaMalloc retries.
|
|
19
|
+
gnorm hit the same bug at +3.5 GiB/rank on the 1.4b ladder.
|
|
20
|
+
|
|
21
|
+
IMPORTANT for anything cached here: outputs are allocated fresh per call and retargeted,
|
|
22
|
+
never reused. A cache-owned output silently aliases across calls, which a bench that makes
|
|
23
|
+
one call per iteration cannot see.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import ctypes
|
|
29
|
+
|
|
30
|
+
import torch
|
|
31
|
+
from cutlass.cute.runtime import from_dlpack
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def cute_view(t: torch.Tensor, perm: tuple[int, ...], dyn_modes: tuple[int, ...]):
|
|
35
|
+
"""A cute.Tensor view of `t`, permuted to `perm`, with `dyn_modes` dynamic.
|
|
36
|
+
|
|
37
|
+
cute wants modes ordered outermost-to-innermost and checks that against the strides.
|
|
38
|
+
`tt.dim_order()` is not usable: it places size-1 modes by contiguity heuristics, so e.g.
|
|
39
|
+
(512,128,2,1):(256,1,128,131072) comes back as (0,2,3,1) and cute rejects it. Sorting
|
|
40
|
+
the permuted tensor's strides has the mirror problem — size-1 modes tie with their
|
|
41
|
+
neighbour and land on the wrong side. Take the order from the UNPERMUTED tensor, where
|
|
42
|
+
descending stride is unambiguous, and map it through perm.
|
|
43
|
+
|
|
44
|
+
detach: inside an autograd.Function the incoming leaves still carry requires_grad and
|
|
45
|
+
dlpack refuses to export those. Every gradient path here is hand-written, so only the
|
|
46
|
+
storage is wanted.
|
|
47
|
+
"""
|
|
48
|
+
t = t.detach()
|
|
49
|
+
base_order = sorted(range(t.dim()), key=lambda i: -t.stride(i))
|
|
50
|
+
new_of_old = {old: new for new, old in enumerate(perm)}
|
|
51
|
+
stride_order = tuple(new_of_old[d] for d in base_order)
|
|
52
|
+
ct = from_dlpack(t.permute(*perm), assumed_align=16)
|
|
53
|
+
for m in dyn_modes:
|
|
54
|
+
ct = ct.mark_compact_shape_dynamic(mode=m, stride_order=stride_order)
|
|
55
|
+
return ct
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def retarget(ct, t: torch.Tensor) -> None:
|
|
59
|
+
"""Point a cached view at this call's tensor. One 64-bit store."""
|
|
60
|
+
ptr = t.data_ptr()
|
|
61
|
+
assert ptr & 15 == 0, "kernel views assume 16B alignment"
|
|
62
|
+
ctypes.c_uint64.from_address(ct.__c_pointers__()[0]).value = ptr
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def release_keepalives(*cts) -> None:
|
|
66
|
+
"""Drop cached views' DLPack references to the tensors that exported them.
|
|
67
|
+
|
|
68
|
+
Call this after `cute.compile` and before the entry lands in a call cache, on EVERY
|
|
69
|
+
view including the outputs' — a cached entry needs only the C memref descriptor, which
|
|
70
|
+
the view owns, since `retarget` rewrites the data word before every launch.
|
|
71
|
+
|
|
72
|
+
Populate the pointer cache first, then null the two reference fields. Those names are
|
|
73
|
+
cutlass internals: fail loudly if an upgrade renames them rather than silently going
|
|
74
|
+
back to pinning gigabytes.
|
|
75
|
+
"""
|
|
76
|
+
for ct in cts:
|
|
77
|
+
ct.__c_pointers__() # materialize _c_pointers_cache before the source goes away
|
|
78
|
+
d = ct.__dict__
|
|
79
|
+
assert "_dlpack_data" in d and "_dltensor_wrapper" in d, (
|
|
80
|
+
"cutlass.cute.runtime._Tensor changed its keepalive fields; re-derive "
|
|
81
|
+
"release_keepalives against the new version (see kernel_fun._common.cache)"
|
|
82
|
+
)
|
|
83
|
+
d["_dlpack_data"] = None
|
|
84
|
+
d["_dltensor_wrapper"] = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def out_specs(*tensors: torch.Tensor) -> tuple:
|
|
88
|
+
"""(shape, dtype) per output — what a cache entry keeps instead of the tensors."""
|
|
89
|
+
return tuple((tuple(t.shape), t.dtype) for t in tensors)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def alloc_outs(specs: tuple, device) -> tuple:
|
|
93
|
+
return tuple(torch.empty(shape, device=device, dtype=dtype) for shape, dtype in specs)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def stream_handle() -> int:
|
|
97
|
+
"""The current stream's raw handle — part of every call key, so a stream switch makes a
|
|
98
|
+
new entry rather than launching a stale one."""
|
|
99
|
+
return torch.cuda.current_stream().cuda_stream
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Are the flash-linear-attention internals we call still the ones we were built against?
|
|
2
|
+
|
|
3
|
+
This package is a partial replacement: several stages of every chain are fla's own kernels,
|
|
4
|
+
called by private path, and one of them (`chunk_kda_fwd_kernel_inter_solve_fused`) is a raw
|
|
5
|
+
`triton.jit` kernel we launch with our own grid and constexpr list. That is a deliberate
|
|
6
|
+
trade — reusing fla's kernels is why our numbers are comparable to fla's launch for launch —
|
|
7
|
+
but it means an fla upgrade can change our results without changing our code.
|
|
8
|
+
|
|
9
|
+
So: check at warmup, not at import. A raise here happens before a training run spends any
|
|
10
|
+
time; the same drift discovered at step 40,000 costs a day.
|
|
11
|
+
|
|
12
|
+
Policy: warn on a version we have not tested, raise on a symbol or signature that moved.
|
|
13
|
+
KERNEL_FUN_FALLBACK=1 downgrades the raise to a warning plus a fallback, for whoever needs
|
|
14
|
+
the cluster running now rather than correct attribution.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import inspect
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
from functools import cache
|
|
23
|
+
|
|
24
|
+
log = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
# Versions this package has actually been run against, not a guess at compatibility.
|
|
27
|
+
TESTED_FLA = {"0.5.2"}
|
|
28
|
+
|
|
29
|
+
# module path -> symbols we import from it. Kept as data so the check and the docs cannot
|
|
30
|
+
# drift apart, and so a new family only has to extend this table.
|
|
31
|
+
FLA_SYMBOLS: dict[str, tuple[str, ...]] = {
|
|
32
|
+
"fla.ops.kda.chunk_intra": (
|
|
33
|
+
"chunk_kda_fwd_intra",
|
|
34
|
+
"chunk_kda_bwd_intra",
|
|
35
|
+
"chunk_kda_fwd_kernel_inter_solve_fused",
|
|
36
|
+
),
|
|
37
|
+
"fla.ops.kda.chunk_intra_token_parallel": ("chunk_kda_fwd_intra_token_parallel",),
|
|
38
|
+
"fla.ops.kda.chunk_bwd": ("chunk_kda_bwd_dAv", "chunk_kda_bwd_wy_dqkg_fused"),
|
|
39
|
+
"fla.ops.kda.wy_fast": ("recompute_w_u_fwd",),
|
|
40
|
+
"fla.ops.kda.gate": ("kda_gate_chunk_cumsum", "kda_gate_bwd"),
|
|
41
|
+
"fla.ops.common.chunk_delta_h": (
|
|
42
|
+
"chunk_gated_delta_rule_fwd_h",
|
|
43
|
+
"chunk_gated_delta_rule_bwd_dhu",
|
|
44
|
+
),
|
|
45
|
+
"fla.ops.common.gate": ("fused_beta_sigmoid", "fused_beta_sigmoid_bwd"),
|
|
46
|
+
"fla.ops.gla.chunk": ("chunk_gla_fwd_o_gk",),
|
|
47
|
+
"fla.ops.utils": ("chunk_local_cumsum",),
|
|
48
|
+
"fla.ops.utils.constant": ("RCP_LN2",),
|
|
49
|
+
"fla.ops.utils.cache": ("fla_cache_autotune",),
|
|
50
|
+
"fla.ops.utils.op": ("exp2",),
|
|
51
|
+
"fla.utils": (
|
|
52
|
+
"autotune_cache_kwargs",
|
|
53
|
+
"check_shared_mem",
|
|
54
|
+
"input_guard",
|
|
55
|
+
"autocast_custom_fwd",
|
|
56
|
+
"autocast_custom_bwd",
|
|
57
|
+
),
|
|
58
|
+
"fla.modules.l2norm": ("l2norm_fwd", "l2norm_bwd"),
|
|
59
|
+
# cconv: the public entry point is also the fallback, and its parameter list is what
|
|
60
|
+
# the whitelist in kernel_fun.cconv.ops classifies.
|
|
61
|
+
"fla.modules.convolution": ("causal_conv1d",),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Keywords we pass by name. A rename upstream is a TypeError at the worst possible moment
|
|
65
|
+
# otherwise; a reorder of the raw triton kernel's args would be worse — wrong answers.
|
|
66
|
+
FLA_KWARGS: dict[tuple[str, str], tuple[str, ...]] = {
|
|
67
|
+
("fla.ops.kda.wy_fast", "recompute_w_u_fwd"): ("q", "k", "v", "beta", "A", "gk"),
|
|
68
|
+
("fla.ops.kda.gate", "kda_gate_chunk_cumsum"): (
|
|
69
|
+
"g", "A_log", "dt_bias", "scale", "chunk_size",
|
|
70
|
+
),
|
|
71
|
+
("fla.ops.kda.gate", "kda_gate_bwd"): ("g", "A_log", "dt_bias", "dyg"),
|
|
72
|
+
("fla.ops.kda.chunk_bwd", "chunk_kda_bwd_dAv"): (
|
|
73
|
+
"q", "k", "v", "do", "A", "scale", "chunk_size",
|
|
74
|
+
),
|
|
75
|
+
("fla.ops.utils", "chunk_local_cumsum"): ("g", "chunk_size", "reverse"),
|
|
76
|
+
("fla.modules.convolution", "causal_conv1d"): (
|
|
77
|
+
"x", "weight", "bias", "residual", "initial_state", "output_final_state",
|
|
78
|
+
"activation", "backend", "cu_seqlens", "cu_seqlens_cpu", "chunk_indices",
|
|
79
|
+
"cp_context",
|
|
80
|
+
),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class FlaDriftError(RuntimeError):
|
|
85
|
+
"""An fla internal we depend on moved. See the message for which one."""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _fail(msg: str) -> None:
|
|
89
|
+
if os.environ.get("KERNEL_FUN_FALLBACK", "0") == "1":
|
|
90
|
+
log.warning("kernel-fun: %s (KERNEL_FUN_FALLBACK=1: falling back to fla)", msg)
|
|
91
|
+
return
|
|
92
|
+
raise FlaDriftError(
|
|
93
|
+
f"{msg}\n"
|
|
94
|
+
f"kernel-fun calls flash-linear-attention internals directly; tested against "
|
|
95
|
+
f"{sorted(TESTED_FLA)}. Set KERNEL_FUN_FALLBACK=1 to degrade to fla's own kernels "
|
|
96
|
+
f"instead of raising."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@cache
|
|
101
|
+
def check_fla() -> bool:
|
|
102
|
+
"""Verify every fla symbol and keyword this package uses. Cached; call from warmup.
|
|
103
|
+
|
|
104
|
+
Returns False when drift was found and KERNEL_FUN_FALLBACK downgraded it, so callers
|
|
105
|
+
can route to fla wholesale.
|
|
106
|
+
"""
|
|
107
|
+
import importlib
|
|
108
|
+
|
|
109
|
+
import fla
|
|
110
|
+
|
|
111
|
+
version = getattr(fla, "__version__", "unknown")
|
|
112
|
+
if version not in TESTED_FLA:
|
|
113
|
+
log.warning(
|
|
114
|
+
"kernel-fun: flash-linear-attention %s is untested here (tested: %s); "
|
|
115
|
+
"verify numerics before trusting a training run",
|
|
116
|
+
version, sorted(TESTED_FLA),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
ok = True
|
|
120
|
+
for mod_path, names in FLA_SYMBOLS.items():
|
|
121
|
+
try:
|
|
122
|
+
mod = importlib.import_module(mod_path)
|
|
123
|
+
except ImportError as e:
|
|
124
|
+
_fail(f"cannot import {mod_path}: {e}")
|
|
125
|
+
ok = False
|
|
126
|
+
continue
|
|
127
|
+
for name in names:
|
|
128
|
+
if not hasattr(mod, name):
|
|
129
|
+
_fail(f"{mod_path}.{name} is gone in fla {version}")
|
|
130
|
+
ok = False
|
|
131
|
+
|
|
132
|
+
for (mod_path, name), kwargs in FLA_KWARGS.items():
|
|
133
|
+
try:
|
|
134
|
+
fn = getattr(importlib.import_module(mod_path), name)
|
|
135
|
+
except (ImportError, AttributeError):
|
|
136
|
+
continue # already reported above
|
|
137
|
+
params = _parameters(fn)
|
|
138
|
+
if params is None:
|
|
139
|
+
continue
|
|
140
|
+
missing = [kw for kw in kwargs if kw not in params]
|
|
141
|
+
if missing:
|
|
142
|
+
_fail(f"{mod_path}.{name} no longer accepts {missing} in fla {version}")
|
|
143
|
+
ok = False
|
|
144
|
+
return ok
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _parameters(fn) -> set[str] | None:
|
|
148
|
+
"""Parameter names of a python function or a triton.jit kernel.
|
|
149
|
+
|
|
150
|
+
A JITFunction is not introspectable by `inspect.signature`; it carries `arg_names`.
|
|
151
|
+
That list is also ORDERED, which matters for the one kernel we launch ourselves — a
|
|
152
|
+
reorder upstream would silently feed our arguments to the wrong parameters.
|
|
153
|
+
"""
|
|
154
|
+
arg_names = getattr(fn, "arg_names", None)
|
|
155
|
+
if arg_names is not None:
|
|
156
|
+
return set(arg_names)
|
|
157
|
+
try:
|
|
158
|
+
return set(inspect.signature(fn).parameters)
|
|
159
|
+
except (TypeError, ValueError): # pragma: no cover - exotic callables
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def kernel_arg_names(mod_path: str, name: str) -> tuple[str, ...]:
|
|
164
|
+
"""Ordered parameter names of a raw triton kernel, for an exact-match assertion."""
|
|
165
|
+
import importlib
|
|
166
|
+
|
|
167
|
+
fn = getattr(importlib.import_module(mod_path), name)
|
|
168
|
+
return tuple(getattr(fn, "arg_names", ()))
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Can this call use our kernels — and if not, exactly why?
|
|
2
|
+
|
|
3
|
+
Every public entry point in this package answers that question before doing anything, and
|
|
4
|
+
delegates to flash-linear-attention when the answer is no. The reason string is the
|
|
5
|
+
important half: a silent fallback reads as a correct 1.00x result, and the last time
|
|
6
|
+
kernels from this repo landed in training the single most expensive question was "did they
|
|
7
|
+
actually run?".
|
|
8
|
+
|
|
9
|
+
Probes that touch the driver or import cutlass are cached — the previous port called
|
|
10
|
+
`import cutlass.cute` on every forward. Probe functions are module-level and cached so a
|
|
11
|
+
test can monkeypatch one and clear its cache to exercise the fallback on any GPU.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
from functools import cache
|
|
19
|
+
|
|
20
|
+
import torch
|
|
21
|
+
|
|
22
|
+
log = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Serial-scan kernels need a full GPU: a one-CTA-per-(chunk, b*hv) kernel at a 64-CTA grid
|
|
25
|
+
# lost 1.4ms to fla in the gdn ladder. Below this the fla path is genuinely faster, so the
|
|
26
|
+
# gate is a performance decision, not a capability one.
|
|
27
|
+
MIN_CTAS = 256
|
|
28
|
+
|
|
29
|
+
_logged: set[str] = set()
|
|
30
|
+
_VERSIONS_KEY = "kernel-fun versions" # sentinel in _logged, so a test's reset re-arms it
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def log_once(message: str, level: int = logging.INFO) -> None:
|
|
34
|
+
if message in _logged:
|
|
35
|
+
return
|
|
36
|
+
_logged.add(message)
|
|
37
|
+
log.log(level, message)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def log_versions_once() -> None:
|
|
41
|
+
"""Log `versions()` the first time any family engages.
|
|
42
|
+
|
|
43
|
+
This belongs HERE, not in the caller's module: every entry point that calls it is
|
|
44
|
+
already `torch.compiler.disable`d, so the line costs no graph break, whereas the same
|
|
45
|
+
call from a compiled `forward` splits the block (and, in olmo-core, hit `lru_cache` on
|
|
46
|
+
a dict return and raised `TypeError: unhashable type: 'dict'`). Two of these versions
|
|
47
|
+
— the CuTe DSL and cuda-python — are pinned by nothing and ride in with the image,
|
|
48
|
+
so when a run is slower than the last one this is the first thing to diff.
|
|
49
|
+
"""
|
|
50
|
+
if _VERSIONS_KEY in _logged:
|
|
51
|
+
return # before versions(): it queries device properties, ~14 us, and this is per call
|
|
52
|
+
_logged.add(_VERSIONS_KEY)
|
|
53
|
+
from .. import versions
|
|
54
|
+
|
|
55
|
+
log.info(f"kernel-fun {versions()}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@cache
|
|
59
|
+
def has_cute() -> bool:
|
|
60
|
+
"""Is the CuTe DSL importable? Cached: the import pulls MLIR and costs seconds.
|
|
61
|
+
|
|
62
|
+
Also warns, once, when the DSL's CUDA build visibly disagrees with torch's — see
|
|
63
|
+
`cute_cuda_mismatch`. A warning and not a gate: the wrong-build DSL may well still
|
|
64
|
+
compile (a CUDA 12 toolchain on a CUDA 13 driver is a supported combination), and
|
|
65
|
+
silently routing a working install to fla is the failure this package exists to avoid.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
import cuda.bindings.driver # noqa: F401
|
|
69
|
+
import cutlass # noqa: F401
|
|
70
|
+
import cutlass.cute # noqa: F401
|
|
71
|
+
except Exception: # pragma: no cover - environment-dependent
|
|
72
|
+
return False
|
|
73
|
+
reason = cute_cuda_mismatch(torch.version.cuda, _installed_dists())
|
|
74
|
+
if reason is not None:
|
|
75
|
+
log_once(f"kernel-fun: {reason}", logging.WARNING)
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _installed_dists() -> frozenset[str]:
|
|
80
|
+
import importlib.metadata as md
|
|
81
|
+
|
|
82
|
+
return frozenset(
|
|
83
|
+
d.metadata["Name"].lower() for d in md.distributions() if d.metadata["Name"]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def cute_cuda_mismatch(torch_cuda: str | None, installed: frozenset[str]) -> str | None:
|
|
88
|
+
"""Is the installed CuTe DSL the CUDA build torch was built for? Pure, for the test.
|
|
89
|
+
|
|
90
|
+
On PyPI `nvidia-cutlass-dsl` >= 4.6 always pulls `nvidia-cutlass-dsl-libs-cu12` and
|
|
91
|
+
ships the CUDA 13 libraries only through its `[cu13]` extra, so a CUDA 13 torch next to
|
|
92
|
+
a DSL whose cu13 libs are absent means somebody wrote the bare requirement (this
|
|
93
|
+
package's own extra is `kernel-fun[cu13]`; OLMo-core's is FA4's `[cu13]`). Only that
|
|
94
|
+
one direction is detectable: before 4.6 the CUDA 12 libraries live inside the base
|
|
95
|
+
wheel, and a cu13-only install on a CUDA 12 torch cannot be told from a pre-split one.
|
|
96
|
+
"""
|
|
97
|
+
if not torch_cuda or "nvidia-cutlass-dsl" not in installed:
|
|
98
|
+
return None
|
|
99
|
+
major = torch_cuda.split(".")[0]
|
|
100
|
+
has_cu12 = "nvidia-cutlass-dsl-libs-cu12" in installed
|
|
101
|
+
has_cu13 = "nvidia-cutlass-dsl-libs-cu13" in installed
|
|
102
|
+
if major == "13" and has_cu12 and not has_cu13:
|
|
103
|
+
return (
|
|
104
|
+
f"torch is a CUDA {torch_cuda} build but the CuTe DSL installed is the CUDA 12 "
|
|
105
|
+
f"one (nvidia-cutlass-dsl-libs-cu12 without -cu13). Install "
|
|
106
|
+
f"nvidia-cutlass-dsl[cu13] — kernel-fun[cu13] does — or expect cute.compile "
|
|
107
|
+
f"to fail rather than fall back"
|
|
108
|
+
)
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@cache
|
|
113
|
+
def arch_at_least(device_index: int, major: int) -> bool:
|
|
114
|
+
"""For the Triton-only families: any Blackwell-or-newer datacenter part, or Hopper.
|
|
115
|
+
|
|
116
|
+
Those kernels need no tcgen05 and no CuTe DSL, so the sm100 gate below would deny them
|
|
117
|
+
for no reason; but nobody has timed them below sm100, so callers state their floor.
|
|
118
|
+
"""
|
|
119
|
+
return torch.cuda.get_device_capability(device_index)[0] >= major
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@cache
|
|
123
|
+
def arch_ok(device_index: int) -> bool:
|
|
124
|
+
"""sm100 exactly — Blackwell datacenter (B200/B300).
|
|
125
|
+
|
|
126
|
+
Not `major >= 10`: sm_120 is consumer Blackwell and has no tcgen05, so the MMA kernels
|
|
127
|
+
would fail inside cute.compile rather than fall back. Not `major == 10 and minor == 0`
|
|
128
|
+
either: B300 reports (10, 3).
|
|
129
|
+
"""
|
|
130
|
+
return torch.cuda.get_device_capability(device_index)[0] == 10
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def disabled(family: str) -> bool:
|
|
134
|
+
"""KERNEL_FUN_DISABLE=1 kills every family; KERNEL_FUN_<FAMILY>_DISABLE=1 kills one.
|
|
135
|
+
|
|
136
|
+
Read per call, never at import: the point of a kill switch is that someone can set it
|
|
137
|
+
on a run that is already failing, and an import-time read would depend on which module
|
|
138
|
+
got imported first.
|
|
139
|
+
"""
|
|
140
|
+
return (
|
|
141
|
+
os.environ.get("KERNEL_FUN_DISABLE", "0") == "1"
|
|
142
|
+
or os.environ.get(f"KERNEL_FUN_{family.upper()}_DISABLE", "0") == "1"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def debug() -> bool:
|
|
147
|
+
return os.environ.get("KERNEL_FUN_DEBUG", "0") == "1"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def min_ctas(family: str) -> int:
|
|
151
|
+
"""`family`'s dispatch floor: `MIN_CTAS`, or `KERNEL_FUN_<FAMILY>_MIN_CTAS` if set.
|
|
152
|
+
|
|
153
|
+
A performance heuristic, not a capability — the kernels run below it, they just lose to
|
|
154
|
+
fla where the CuTe scans underfill the GPU — and the crossover belongs to the workload,
|
|
155
|
+
not to the hardware. So a shape that has been *measured* below the default can opt in
|
|
156
|
+
from outside, instead of a training script assigning to `MIN_CTAS` and moving whichever
|
|
157
|
+
per-stage gates happened to bind it at import.
|
|
158
|
+
|
|
159
|
+
Only the chain-level gate moves. Every kernel keeps its own floor, so a lowered value
|
|
160
|
+
changes which stages are ours, never what any of them computes.
|
|
161
|
+
|
|
162
|
+
Read per call, like `disabled`. A value that is not a positive integer is ignored with a
|
|
163
|
+
warning: falling back is this package's job, ending someone's run at step 1 is not.
|
|
164
|
+
"""
|
|
165
|
+
raw = os.environ.get(f"KERNEL_FUN_{family.upper()}_MIN_CTAS")
|
|
166
|
+
if raw is None:
|
|
167
|
+
return MIN_CTAS
|
|
168
|
+
try:
|
|
169
|
+
floor = int(raw)
|
|
170
|
+
except ValueError:
|
|
171
|
+
floor = 0
|
|
172
|
+
if floor < 1:
|
|
173
|
+
log_once(
|
|
174
|
+
f"kernel-fun {family}: ignoring KERNEL_FUN_{family.upper()}_MIN_CTAS={raw!r} "
|
|
175
|
+
f"(want a positive integer); using the default floor of {MIN_CTAS} CTAs",
|
|
176
|
+
logging.WARNING,
|
|
177
|
+
)
|
|
178
|
+
return MIN_CTAS
|
|
179
|
+
return floor
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def capturing() -> bool:
|
|
183
|
+
"""Is a CUDA graph being captured on this stream?"""
|
|
184
|
+
try:
|
|
185
|
+
return torch.cuda.is_current_stream_capturing()
|
|
186
|
+
except Exception: # pragma: no cover - older torch without the query
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# Call signatures (per family: shapes, dtypes, device, kernel-selecting flags — see the
|
|
191
|
+
# family's `_warm_sig`) that have completed an eager forward / backward through our kernels.
|
|
192
|
+
_WARM: set[tuple] = set()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def mark_warm(sig: tuple, phase: str) -> None:
|
|
196
|
+
"""Record that `sig` just ran `phase` ("fwd" or "bwd") outside any capture."""
|
|
197
|
+
if not capturing():
|
|
198
|
+
_WARM.add((*sig, phase))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def capture_unsupported_reason(sig: tuple, needs_grad: bool) -> str | None:
|
|
202
|
+
"""Under CUDA graph capture, our kernels run only for a shape that has already run eagerly.
|
|
203
|
+
|
|
204
|
+
Capture bakes every pointer the region touches; the call cache's launch-time pointer
|
|
205
|
+
poke is no different from what cuBLAS does, and with the static buffers a graphed
|
|
206
|
+
callable owns it replays correctly (verified bit-identical fwd+bwd, 2026-09-08). What
|
|
207
|
+
CANNOT happen inside a capture is a first call at a shape: `cute.compile`, and the
|
|
208
|
+
Triton autotuners' trial launches (ours and fla's both synchronize). Both are keyed on
|
|
209
|
+
shapes and dtypes, never on the stream, so a call-cache miss on a new stream for a warm
|
|
210
|
+
shape is only `from_dlpack` plus an allocation, and captures fine. Hence the stream is
|
|
211
|
+
deliberately NOT part of the signature: `make_graphed_callables` warms on one side
|
|
212
|
+
stream and captures on another, and keying on it would send every graphed call to fla
|
|
213
|
+
with nothing but a log line to say so.
|
|
214
|
+
|
|
215
|
+
The fallback for a cold shape is fla, which under capture has the same autotune
|
|
216
|
+
problem — but that is fla's failure to report, loudly, not ours to hide.
|
|
217
|
+
"""
|
|
218
|
+
if not capturing():
|
|
219
|
+
return None
|
|
220
|
+
if (*sig, "fwd") not in _WARM:
|
|
221
|
+
return (
|
|
222
|
+
"CUDA graph capture of a shape that has not run eagerly yet (run one forward "
|
|
223
|
+
"at this shape before capturing — the compile and autotune steps cannot be "
|
|
224
|
+
"captured)"
|
|
225
|
+
)
|
|
226
|
+
if needs_grad and (*sig, "bwd") not in _WARM:
|
|
227
|
+
return (
|
|
228
|
+
"CUDA graph capture of a shape whose backward has not run eagerly yet (run one "
|
|
229
|
+
"fwd+bwd at this shape before capturing)"
|
|
230
|
+
)
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _basic_unsupported_reason(t: torch.Tensor, family: str) -> str | None:
|
|
235
|
+
if disabled(family):
|
|
236
|
+
return f"KERNEL_FUN_{family.upper()}_DISABLE / KERNEL_FUN_DISABLE is set"
|
|
237
|
+
if not t.is_cuda:
|
|
238
|
+
return "not a CUDA tensor"
|
|
239
|
+
# CUDA graph capture is gated per shape, after the shape checks: capture_unsupported_reason.
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def common_unsupported_reason(t: torch.Tensor, family: str) -> str | None:
|
|
244
|
+
"""The gates every CuTe family shares. Family-specific shape checks live with the family."""
|
|
245
|
+
reason = _basic_unsupported_reason(t, family)
|
|
246
|
+
if reason is not None:
|
|
247
|
+
return reason
|
|
248
|
+
if not arch_ok(t.device.index or 0):
|
|
249
|
+
cap = torch.cuda.get_device_capability(t.device.index or 0)
|
|
250
|
+
return f"device capability sm{cap[0]}{cap[1]} is not sm100 (B200/B300)"
|
|
251
|
+
if not has_cute():
|
|
252
|
+
return "the CUTLASS CuTe DSL is not installed"
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def triton_unsupported_reason(t: torch.Tensor, family: str, min_major: int = 9) -> str | None:
|
|
257
|
+
"""The same, for a family whose kernels are all Triton: no CuTe, a lower arch floor."""
|
|
258
|
+
reason = _basic_unsupported_reason(t, family)
|
|
259
|
+
if reason is not None:
|
|
260
|
+
return reason
|
|
261
|
+
if not arch_at_least(t.device.index or 0, min_major):
|
|
262
|
+
cap = torch.cuda.get_device_capability(t.device.index or 0)
|
|
263
|
+
return (
|
|
264
|
+
f"device capability sm{cap[0]}{cap[1]} is below sm{min_major}0 "
|
|
265
|
+
f"(measured on sm100; untimed below it)"
|
|
266
|
+
)
|
|
267
|
+
return None
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""cconv — the KDA short convolution: width-4 depthwise causal conv + silu.
|
|
2
|
+
|
|
3
|
+
from kernel_fun.cconv import causal_conv1d
|
|
4
|
+
|
|
5
|
+
Same signature and return contract as `fla.modules.convolution.causal_conv1d`; anything this
|
|
6
|
+
package does not implement is forwarded to fla, so the swap is one import line and
|
|
7
|
+
reverting it is one line back.
|
|
8
|
+
|
|
9
|
+
A KDA layer makes three of these calls per forward (q, k at D = n_heads*128, v at 2x that),
|
|
10
|
+
and the 810m/B300 step trace put them at ~50 ms of a ~520 ms step — the largest non-GEMM
|
|
11
|
+
item in the step, against an ~8-10 ms bandwidth roofline. Measured isolated on B300 at the
|
|
12
|
+
production call (B=16, T=8192, D=2048, W=4, bf16 x / fp32 weight; 2026-09-01, the ladder's
|
|
13
|
+
cconv/001 record 002):
|
|
14
|
+
|
|
15
|
+
backward (dx, dw; no forward re-run) 1.597 ms -> 0.344 ms 4.65x (4.7 TB/s)
|
|
16
|
+
forward (silu fused) 0.307 ms -> 0.190 ms 1.62x (5.6 TB/s)
|
|
17
|
+
|
|
18
|
+
The same ratios hold at D=1024 and D=4096. Both kernels are Triton; see ops.py for the
|
|
19
|
+
supported box and _kernels/strip.py for the design.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .ops import causal_conv1d, is_supported, warmup
|
|
23
|
+
|
|
24
|
+
__all__ = ["causal_conv1d", "is_supported", "warmup"]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Vendored kernels: copied from the research ladder at the commit in _provenance.py.
|
|
2
|
+
|
|
3
|
+
These files are edited on arrival (env knobs frozen to the branch that won) and are
|
|
4
|
+
otherwise the measured code. Edit them HERE only for packaging concerns; kernel work
|
|
5
|
+
happens in the ladder, and comes back through tools/vendor.py.
|
|
6
|
+
"""
|