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 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
+ """