| from __future__ import annotations |
|
|
| from dataclasses import dataclass |
| from typing import Callable, Iterable |
|
|
| import torch |
|
|
|
|
| @dataclass |
| class GraphKVIndexWriter: |
| """Writes decode K/V values at the absolute token position provided by input_pos.""" |
|
|
| def write(self, k_cache: torch.Tensor, v_cache: torch.Tensor, k_val: torch.Tensor, v_val: torch.Tensor, input_pos: torch.Tensor) -> None: |
| index = input_pos.reshape(-1)[:1] |
| if index.dtype != torch.long: |
| index = index.to(dtype=torch.long) |
| if index.device != k_cache.device: |
| index = index.to(device=k_cache.device) |
| k_cache[: k_val.shape[0]].index_copy_(1, index, k_val) |
| v_cache[: v_val.shape[0]].index_copy_(1, index, v_val) |
|
|
|
|
| @dataclass |
| class GraphKVFlashState: |
| """Marks a cache as using flash_attn_with_kvcache-compatible graph decode.""" |
|
|
| flash_kvcache: bool = True |
|
|
|
|
| class CUDAGraphRunner: |
| """Captures and replays a fixed-shape CUDA callable.""" |
|
|
| def __init__(self, name: str, fn: Callable[..., object]) -> None: |
| self._name = name |
| self._fn = fn |
| self._graph: torch.cuda.CUDAGraph | None = None |
| self._static_inputs: list[torch.Tensor] = [] |
| self._output: object = None |
|
|
| def _capture(self, *inputs: torch.Tensor) -> object: |
| if not inputs: |
| raise ValueError(f"[{self._name}] CUDAGraphRunner requires at least one tensor input.") |
| for tensor in inputs: |
| if not torch.is_tensor(tensor): |
| raise TypeError(f"[{self._name}] CUDAGraphRunner accepts tensor inputs only.") |
| if tensor.device.type != "cuda": |
| raise ValueError(f"[{self._name}] CUDAGraphRunner requires CUDA tensors.") |
| self._static_inputs = [tensor.detach().clone() for tensor in inputs] |
| self._graph = torch.cuda.CUDAGraph() |
| try: |
| with torch.inference_mode(): |
| with torch.cuda.graph(self._graph, capture_error_mode="thread_local"): |
| self._output = self._fn(*self._static_inputs) |
| |
| |
| with torch.inference_mode(): |
| self._graph.replay() |
| except Exception: |
| self.release() |
| raise |
| return self._output |
|
|
| def _copy_inputs(self, *inputs: torch.Tensor) -> None: |
| if len(inputs) != len(self._static_inputs): |
| raise ValueError(f"[{self._name}] Expected {len(self._static_inputs)} inputs, got {len(inputs)}.") |
| for static_tensor, tensor in zip(self._static_inputs, inputs): |
| if tensor.shape != static_tensor.shape or tensor.dtype != static_tensor.dtype or tensor.device != static_tensor.device: |
| raise ValueError( |
| f"[{self._name}] Static shape mismatch: expected {tuple(static_tensor.shape)} {static_tensor.dtype} on {static_tensor.device}, " |
| f"got {tuple(tensor.shape)} {tensor.dtype} on {tensor.device}." |
| ) |
| static_tensor.copy_(tensor) |
|
|
| def run(self, *inputs: torch.Tensor) -> object: |
| if self._graph is None: |
| return self._capture(*inputs) |
| self._copy_inputs(*inputs) |
| with torch.inference_mode(): |
| self._graph.replay() |
| return self._output |
|
|
| def release(self) -> None: |
| self._graph = None |
| self._static_inputs = [] |
| self._output = None |
|
|
|
|
| class AutoRegressiveCudaGraphKit: |
| """Reusable helper to attach graph-safe KV writers and replay decode callables.""" |
|
|
| def __init__(self, name: str) -> None: |
| self._name = name |
| self._runners: dict[str, CUDAGraphRunner] = {} |
| self._attached_caches: list[object] = [] |
| self._attached_cache_ids: set[int] = set() |
|
|
| def attach_attention_modules(self, attention_modules: Iterable[object], max_tokens: int, graph_mode: str = "index") -> int: |
| if max_tokens <= 0: |
| raise ValueError("max_tokens must be > 0.") |
| if graph_mode not in ("index", "flash_kvcache"): |
| raise ValueError(f"Unsupported graph_mode '{graph_mode}'.") |
| final_capacity = 0 |
| for module in attention_modules: |
| kv_cache = getattr(module, "kv_cache", None) |
| if kv_cache is None: |
| raise RuntimeError("KV cache must be initialized before CUDA graph attachment.") |
| kv_cache.ensure_capacity(max_tokens) |
| final_capacity = max(final_capacity, int(kv_cache.capacity)) |
| cache_id = id(kv_cache) |
| if cache_id not in self._attached_cache_ids: |
| if graph_mode == "flash_kvcache": |
| kv_cache.graph_state = GraphKVFlashState() |
| else: |
| kv_cache.graph_state = GraphKVIndexWriter() |
| self._attached_cache_ids.add(cache_id) |
| self._attached_caches.append(kv_cache) |
| return final_capacity |
|
|
| def run(self, runner_name: str, fn: Callable[..., object], *inputs: torch.Tensor) -> object: |
| runner = self._runners.get(runner_name) |
| if runner is None: |
| runner = CUDAGraphRunner(f"{self._name}:{runner_name}", fn) |
| self._runners[runner_name] = runner |
| return runner.run(*inputs) |
|
|
| def release(self) -> None: |
| for runner in self._runners.values(): |
| runner.release() |
| self._runners.clear() |
| for kv_cache in self._attached_caches: |
| if hasattr(kv_cache, "graph_state"): |
| kv_cache.graph_state = None |
| self._attached_caches.clear() |
| self._attached_cache_ids.clear() |
|
|