Skip to content

Commit ede065d

Browse files
committed
Add persistent CuTeDSL compile cache
Human Note Agent note CuTeDSL call sites currently rely on process-local dictionaries or the compiler's implicit cache, which cannot safely coordinate concurrent tuning workers. Add a source- and target-aware artifact cache with per-key file locking, atomic object publication, corrupt-entry recovery, and process-local loaded-callable reuse. Keep runtime tensors outside the key and centralize the fake-stream, typed TVM-FFI compile contract in `compile_tvm_ffi`. The cache directory has one Attention Gym override, while the upstream `CUTE_DSL_NO_CACHE` switch remains authoritative. Tests use an isolated temporary cache and fake exported objects to cover thread/process contention, warm reloads, failure cleanup, target inheritance, and the TVM-FFI ABI without requiring a GPU. Test Plan: ```bash ~/.venvs/nightly/bin/python -m pytest -q test/test_cute_cache.py ~/.venvs/dev/bin/ruff check attn_gym/_backends/cute/{__init__,_key,cache,target,utils}.py test/test_cute_cache.py ```
1 parent 1f5c714 commit ede065d

6 files changed

Lines changed: 1064 additions & 0 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Infrastructure shared by CuTeDSL attention backends."""
2+
3+
from .cache import jit_cache
4+
from .utils import compile_tvm_ffi
5+
6+
__all__ = ["compile_tvm_ffi", "jit_cache"]

attn_gym/_backends/cute/_key.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
"""Deterministic source and invocation keys for the CuTeDSL cache."""
2+
3+
from __future__ import annotations
4+
5+
import dataclasses
6+
import enum
7+
import functools
8+
import hashlib
9+
import os
10+
import pickle
11+
import platform
12+
import sys
13+
from collections.abc import Callable
14+
from pathlib import Path
15+
from typing import Any
16+
17+
from .target import CompileTarget
18+
19+
CACHE_FORMAT_VERSION = 4
20+
21+
22+
def _hash_file(hasher: Any, label: str, source: Path) -> None:
23+
content = source.read_bytes()
24+
encoded_label = label.encode()
25+
hasher.update(len(encoded_label).to_bytes(8, "little"))
26+
hasher.update(encoded_label)
27+
hasher.update(len(content).to_bytes(8, "little"))
28+
hasher.update(content)
29+
30+
31+
def _hash_source_tree(hasher: Any, root: Path) -> None:
32+
for source in sorted(root.rglob("*.py")):
33+
if source.is_file():
34+
_hash_file(hasher, source.relative_to(root).as_posix(), source)
35+
36+
37+
@functools.cache
38+
def source_fingerprint(
39+
fn: Callable[..., Any],
40+
extra_sources: tuple[str, ...] = (),
41+
) -> str:
42+
"""Fingerprint the compile function, CuTe sources, and host/runtime ABI."""
43+
import cutlass
44+
import torch
45+
import tvm_ffi
46+
47+
hasher = hashlib.sha256()
48+
stamps = (
49+
CACHE_FORMAT_VERSION,
50+
sys.implementation.cache_tag,
51+
platform.system(),
52+
platform.machine(),
53+
cutlass.__version__,
54+
tvm_ffi.__version__,
55+
torch.__version__,
56+
torch.version.cuda,
57+
)
58+
hasher.update(pickle.dumps(stamps, protocol=pickle.HIGHEST_PROTOCOL))
59+
60+
module = sys.modules.get(fn.__module__)
61+
module_file = getattr(module, "__file__", None)
62+
if module_file is not None:
63+
source = Path(module_file).resolve()
64+
if source.suffix == ".py" and source.is_file():
65+
_hash_file(hasher, fn.__module__, source)
66+
67+
package_root = Path(__file__).resolve().parents[2]
68+
for source in sorted(package_root.rglob("*.py")):
69+
relative_path = source.relative_to(package_root)
70+
if "cute" in relative_path.parts:
71+
_hash_file(hasher, relative_path.as_posix(), source)
72+
73+
for extra_source in extra_sources:
74+
path = Path(extra_source).expanduser().resolve()
75+
if path.is_dir():
76+
_hash_source_tree(hasher, path)
77+
elif path.is_file():
78+
_hash_file(hasher, str(path), path)
79+
else:
80+
raise FileNotFoundError(f"extra CuTeDSL cache source does not exist: {path}")
81+
82+
cutlass_root = Path(cutlass.__file__).resolve().parent
83+
for relative_path in (
84+
"__init__.py",
85+
"base_dsl/compiler.py",
86+
"base_dsl/dsl.py",
87+
"cutlass_dsl/tvm_ffi_provider.py",
88+
"cute/runtime.py",
89+
):
90+
source = cutlass_root / relative_path
91+
if source.is_file():
92+
_hash_file(hasher, f"cutlass/{relative_path}", source)
93+
94+
codegen_environment = tuple(
95+
(name, os.getenv(name))
96+
for name in (
97+
"CUTE_DSL_ARCH",
98+
"CUTE_DSL_COMPILER_OPT",
99+
"CUTE_DSL_ENABLE_ASSERTIONS",
100+
"CUTE_DSL_ENABLE_TVM_FFI",
101+
"CUTE_DSL_LIBS",
102+
"CUTE_DSL_LINEINFO",
103+
)
104+
)
105+
hasher.update(pickle.dumps(codegen_environment, protocol=pickle.HIGHEST_PROTOCOL))
106+
return hasher.hexdigest()
107+
108+
109+
def _is_named_tuple(item: Any) -> bool:
110+
fields = getattr(type(item), "_fields", None)
111+
return isinstance(item, tuple) and isinstance(fields, tuple)
112+
113+
114+
def _pickle_sort_key(item: Any) -> bytes:
115+
return pickle.dumps(item, protocol=pickle.HIGHEST_PROTOCOL)
116+
117+
118+
def _canonicalize(item: Any) -> Any:
119+
custom_key = getattr(item, "__attention_gym_cache_key__", None)
120+
if custom_key is not None:
121+
value = custom_key() if callable(custom_key) else custom_key
122+
return (
123+
"custom",
124+
type(item).__module__,
125+
type(item).__qualname__,
126+
_canonicalize(value),
127+
)
128+
if _is_named_tuple(item):
129+
return (
130+
"named_tuple",
131+
type(item).__module__,
132+
type(item).__qualname__,
133+
tuple((name, _canonicalize(getattr(item, name))) for name in type(item)._fields),
134+
)
135+
if dataclasses.is_dataclass(item) and not isinstance(item, type):
136+
return (
137+
"dataclass",
138+
type(item).__module__,
139+
type(item).__qualname__,
140+
tuple(
141+
(field.name, _canonicalize(getattr(item, field.name)))
142+
for field in dataclasses.fields(item)
143+
),
144+
)
145+
if isinstance(item, enum.Enum):
146+
return ("enum", type(item).__module__, type(item).__qualname__, item.name)
147+
if isinstance(item, type):
148+
return ("type", item.__module__, item.__qualname__)
149+
if isinstance(item, Path):
150+
return ("path", str(item))
151+
if isinstance(item, tuple):
152+
return ("tuple", tuple(_canonicalize(value) for value in item))
153+
if isinstance(item, list):
154+
return ("list", tuple(_canonicalize(value) for value in item))
155+
if isinstance(item, dict):
156+
entries = [(_canonicalize(key), _canonicalize(value)) for key, value in item.items()]
157+
return ("dict", tuple(sorted(entries, key=lambda entry: _pickle_sort_key(entry[0]))))
158+
if isinstance(item, set):
159+
values = [_canonicalize(value) for value in item]
160+
return ("set", tuple(sorted(values, key=_pickle_sort_key)))
161+
if isinstance(item, frozenset):
162+
values = [_canonicalize(value) for value in item]
163+
return ("frozenset", tuple(sorted(values, key=_pickle_sort_key)))
164+
try:
165+
pickle.dumps(item, protocol=pickle.HIGHEST_PROTOCOL)
166+
except (pickle.PickleError, TypeError) as error:
167+
raise TypeError(
168+
f"CuTeDSL cache argument of type {type(item).__qualname__} has no stable key; "
169+
"pass static pickleable values or define __attention_gym_cache_key__"
170+
) from error
171+
return item
172+
173+
174+
def make_key(
175+
fn: Callable[..., Any],
176+
args: tuple[Any, ...],
177+
kwargs: dict[str, Any],
178+
target: CompileTarget,
179+
) -> str:
180+
"""Hash one compile invocation and its complete target contract."""
181+
key_data = (
182+
CACHE_FORMAT_VERSION,
183+
fn.__module__,
184+
fn.__qualname__,
185+
_canonicalize(args),
186+
_canonicalize(kwargs),
187+
target,
188+
)
189+
encoded = pickle.dumps(key_data, protocol=pickle.HIGHEST_PROTOCOL)
190+
return hashlib.sha256(encoded).hexdigest()

0 commit comments

Comments
 (0)