Mike0021 commited on
Commit
a63d266
·
verified ·
1 Parent(s): 6407319

Initial ZONOS2 ZeroGPU Space

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. .gitignore +7 -0
  3. README.md +13 -8
  4. app.py +317 -0
  5. requirements.txt +19 -0
  6. zonos2/__main__.py +5 -0
  7. zonos2/attention/__init__.py +94 -0
  8. zonos2/attention/base.py +69 -0
  9. zonos2/attention/fa.py +199 -0
  10. zonos2/attention/fi.py +360 -0
  11. zonos2/attention/utils.py +56 -0
  12. zonos2/benchmark/client.py +501 -0
  13. zonos2/benchmark/perf.py +74 -0
  14. zonos2/core.py +218 -0
  15. zonos2/distributed/__init__.py +12 -0
  16. zonos2/distributed/impl.py +97 -0
  17. zonos2/distributed/info.py +38 -0
  18. zonos2/engine/__init__.py +4 -0
  19. zonos2/engine/config.py +55 -0
  20. zonos2/engine/engine.py +336 -0
  21. zonos2/engine/graph.py +157 -0
  22. zonos2/engine/sample.py +170 -0
  23. zonos2/env.py +87 -0
  24. zonos2/kernel/__init__.py +14 -0
  25. zonos2/kernel/__main__.py +47 -0
  26. zonos2/kernel/csrc/include/zonos2/nccl227.h +571 -0
  27. zonos2/kernel/csrc/include/zonos2/tensor.h +496 -0
  28. zonos2/kernel/csrc/include/zonos2/utils.cuh +144 -0
  29. zonos2/kernel/csrc/include/zonos2/utils.h +122 -0
  30. zonos2/kernel/csrc/include/zonos2/warp.cuh +80 -0
  31. zonos2/kernel/csrc/jit/index.cu +172 -0
  32. zonos2/kernel/csrc/jit/store.cu +123 -0
  33. zonos2/kernel/csrc/src/pynccl.cu +188 -0
  34. zonos2/kernel/csrc/src/radix.cpp +44 -0
  35. zonos2/kernel/csrc/src/tensor.cpp +31 -0
  36. zonos2/kernel/index.py +50 -0
  37. zonos2/kernel/moe_impl.py +59 -0
  38. zonos2/kernel/pynccl.py +78 -0
  39. zonos2/kernel/radix.py +20 -0
  40. zonos2/kernel/store.py +42 -0
  41. zonos2/kernel/tensor.py +19 -0
  42. zonos2/kernel/triton/fused_moe.py +230 -0
  43. zonos2/kernel/utils.py +129 -0
  44. zonos2/kvcache/__init__.py +74 -0
  45. zonos2/kvcache/base.py +130 -0
  46. zonos2/kvcache/mha_pool.py +79 -0
  47. zonos2/kvcache/naive_manager.py +44 -0
  48. zonos2/kvcache/radix_manager.py +221 -0
  49. zonos2/layers/__init__.py +32 -0
  50. zonos2/layers/activation.py +15 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ zonos2/vendor/nemo_text_processing/text_normalization/en/data/number/cardinal_number_name.far filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ .gradio/
4
+ .venv/
5
+ venv/
6
+ *.wav
7
+ .DS_Store
README.md CHANGED
@@ -1,13 +1,18 @@
1
  ---
2
- title: Zonos2
3
- emoji: 📚
4
- colorFrom: purple
5
- colorTo: gray
6
  sdk: gradio
7
- sdk_version: 6.18.0
8
- python_version: '3.13'
9
  app_file: app.py
10
- pinned: false
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
  ---
2
+ title: ZONOS2
3
+ emoji: 🔊
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 6.10.0
 
8
  app_file: app.py
9
+ short_description: ZeroGPU text-to-speech with ZONOS2
10
+ python_version: "3.10"
11
+ startup_duration_timeout: 1h
12
+ preload_from_hub:
13
+ - Zyphra/ZONOS2
14
  ---
15
 
16
+ # ZONOS2 on ZeroGPU
17
+
18
+ Gradio Space for [Zyphra/ZONOS2](https://huggingface.co/Zyphra/ZONOS2), using the upstream [ZONOS2](https://github.com/Zyphra/ZONOS2) inference code with small ZeroGPU compatibility patches.
app.py ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ import threading
4
+ import time
5
+ import wave
6
+ from pathlib import Path
7
+
8
+ os.environ.setdefault("HF_HOME", "/data/.cache/huggingface")
9
+ os.environ.setdefault("HF_MODULES_CACHE", "/tmp/hf_modules")
10
+ os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
11
+ os.environ.setdefault("ZONOS2_TTS_NORM_CACHE_DIR", "/tmp/zonos2-tts-norm")
12
+ os.environ.setdefault("GRADIO_SSR_MODE", "false")
13
+ os.environ.setdefault("NUMBA_DISABLE_CUDA", "1")
14
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
15
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
16
+
17
+ for cache_dir in (
18
+ os.environ["HF_HOME"],
19
+ os.environ["HF_MODULES_CACHE"],
20
+ os.environ["MPLCONFIGDIR"],
21
+ os.environ["ZONOS2_TTS_NORM_CACHE_DIR"],
22
+ ):
23
+ Path(cache_dir).mkdir(parents=True, exist_ok=True)
24
+
25
+ import spaces
26
+ import gradio as gr
27
+ import numpy as np
28
+ import torch
29
+
30
+ from zonos2.message import TTSSamplingParams
31
+ from zonos2.tokenizer.textnorm import SERVER_TO_NEMO_LANG, TTSTextNormalizer
32
+ from zonos2.tts import TTSLLM
33
+
34
+ MODEL_ID = "Zyphra/ZONOS2"
35
+ SAMPLE_RATE = 44100
36
+ LANGUAGES = [
37
+ ("English (US)", "en_us"),
38
+ ("English (UK)", "en_gb"),
39
+ ("French", "fr_fr"),
40
+ ("German", "de"),
41
+ ("Spanish", "es"),
42
+ ("Italian", "it"),
43
+ ("Portuguese (Brazil)", "pt_br"),
44
+ ("Japanese", "ja"),
45
+ ("Mandarin Chinese", "cmn"),
46
+ ("Korean", "ko"),
47
+ ]
48
+ SPEAKING_RATE_BUCKETS = [
49
+ ("Default", "default"),
50
+ ("Very slow", "0"),
51
+ ("Slow", "1"),
52
+ ("Relaxed", "2"),
53
+ ("Natural", "3"),
54
+ ("Bright", "4"),
55
+ ("Fast", "5"),
56
+ ("Very fast", "6"),
57
+ ("Extreme", "7"),
58
+ ]
59
+
60
+ torch.backends.cuda.matmul.allow_tf32 = True
61
+
62
+
63
+ def _load_model() -> TTSLLM:
64
+ print(f"Loading {MODEL_ID} for ZeroGPU inference...", flush=True)
65
+ started = time.perf_counter()
66
+ model = TTSLLM(
67
+ model_path=MODEL_ID,
68
+ decode_audio=True,
69
+ cuda_graph_max_bs=0,
70
+ max_running_req=4,
71
+ max_extend_tokens=4096,
72
+ memory_ratio=0.75,
73
+ use_pynccl=False,
74
+ )
75
+ elapsed = time.perf_counter() - started
76
+ print(f"Loaded {MODEL_ID} in {elapsed:.1f}s", flush=True)
77
+ return model
78
+
79
+
80
+ TTS = _load_model()
81
+ TEXT_NORMALIZER = TTSTextNormalizer()
82
+ TTS_LOCK = threading.Lock()
83
+
84
+
85
+ def _estimate_duration(*args, **kwargs) -> int:
86
+ max_tokens = kwargs.get("max_tokens")
87
+ if max_tokens is None and len(args) > 4:
88
+ max_tokens = args[4]
89
+ try:
90
+ max_tokens = int(max_tokens)
91
+ except (TypeError, ValueError):
92
+ max_tokens = 768
93
+ return min(180, max(60, 45 + max_tokens // 12))
94
+
95
+
96
+ def _pcm_float32_to_wav(audio_bytes: bytes, sample_rate: int = SAMPLE_RATE) -> str:
97
+ audio = np.frombuffer(audio_bytes, dtype=np.float32)
98
+ if audio.size == 0:
99
+ raise gr.Error("The model returned no audio. Try increasing max tokens.")
100
+ audio = np.nan_to_num(audio, nan=0.0, posinf=0.0, neginf=0.0)
101
+ audio = np.clip(audio, -1.0, 1.0)
102
+ audio_i16 = (audio * 32767.0).astype(np.int16)
103
+
104
+ handle = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
105
+ handle.close()
106
+ with wave.open(handle.name, "wb") as wav:
107
+ wav.setnchannels(1)
108
+ wav.setsampwidth(2)
109
+ wav.setframerate(sample_rate)
110
+ wav.writeframes(audio_i16.tobytes())
111
+ return handle.name
112
+
113
+
114
+ def _normalize_text(text: str, language: str, enabled: bool) -> str:
115
+ if not enabled:
116
+ return text
117
+ if language not in SERVER_TO_NEMO_LANG:
118
+ return text
119
+ return TEXT_NORMALIZER.normalize(text, language)
120
+
121
+
122
+ def _speaking_rate_bucket(value: str) -> int | None:
123
+ if value == "default":
124
+ return None
125
+ return int(value)
126
+
127
+
128
+ @spaces.GPU(duration=_estimate_duration)
129
+ def synthesize(
130
+ text: str,
131
+ language: str,
132
+ text_normalization: bool,
133
+ speaking_rate: str,
134
+ max_tokens: int,
135
+ temperature: float,
136
+ topk: int,
137
+ top_p: float,
138
+ min_p: float,
139
+ repetition_penalty: float,
140
+ seed: int,
141
+ ):
142
+ text = (text or "").strip()
143
+ if not text:
144
+ raise gr.Error("Enter text to synthesize.")
145
+ if len(text) > 1200:
146
+ raise gr.Error("Keep the prompt under 1200 characters for this Space.")
147
+
148
+ normalized = _normalize_text(text, language, text_normalization)
149
+ params = TTSSamplingParams(
150
+ temperature=float(temperature),
151
+ topk=int(topk),
152
+ top_p=float(top_p),
153
+ min_p=float(min_p),
154
+ max_tokens=int(max_tokens),
155
+ repetition_window=50,
156
+ repetition_penalty=float(repetition_penalty),
157
+ repetition_codebooks=8,
158
+ seed=None if seed is None or int(seed) < 0 else int(seed),
159
+ )
160
+
161
+ started = time.perf_counter()
162
+ with TTS_LOCK:
163
+ torch.cuda.set_stream(TTS.stream)
164
+ result = TTS.generate_one(
165
+ normalized,
166
+ params,
167
+ decode_audio=True,
168
+ speaking_rate_bucket=_speaking_rate_bucket(speaking_rate),
169
+ quality_buckets=None,
170
+ )
171
+ elapsed = time.perf_counter() - started
172
+
173
+ wav_path = _pcm_float32_to_wav(result["audio"], result.get("sample_rate", SAMPLE_RATE))
174
+ frames = len(result.get("audio_tokens") or [])
175
+ eos_frame = result.get("eos_frame")
176
+ status = f"Generated {frames} frames in {elapsed:.1f}s"
177
+ if eos_frame is not None:
178
+ status += f" (EOS frame {eos_frame})"
179
+ if normalized != text:
180
+ status += f"\n\nNormalized text: {normalized}"
181
+ return wav_path, status
182
+
183
+
184
+ CSS = """
185
+ main, .gradio-container, .gradio-container > .fillable {
186
+ max-width: 1180px !important;
187
+ margin-inline: auto !important;
188
+ }
189
+ .compact-status textarea {
190
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
191
+ }
192
+ """
193
+
194
+
195
+ with gr.Blocks(title="ZONOS2", css=CSS) as demo:
196
+ gr.Markdown("# ZONOS2")
197
+ with gr.Row():
198
+ with gr.Column(scale=5):
199
+ text = gr.Textbox(
200
+ label="Text",
201
+ value="In the quiet hum of the studio, ZONOS2 turns written words into natural speech.",
202
+ lines=6,
203
+ max_length=1200,
204
+ )
205
+ with gr.Row():
206
+ language = gr.Dropdown(
207
+ choices=LANGUAGES,
208
+ value="en_us",
209
+ label="Language",
210
+ )
211
+ speaking_rate = gr.Dropdown(
212
+ choices=SPEAKING_RATE_BUCKETS,
213
+ value="default",
214
+ label="Speaking rate",
215
+ )
216
+ text_normalization = gr.Checkbox(value=True, label="Text normalization")
217
+ generate = gr.Button("Generate", variant="primary")
218
+ with gr.Column(scale=4):
219
+ audio = gr.Audio(label="Audio", type="filepath", format="wav")
220
+ status = gr.Textbox(
221
+ label="Status",
222
+ lines=5,
223
+ interactive=False,
224
+ elem_classes=["compact-status"],
225
+ )
226
+
227
+ with gr.Accordion("Sampling", open=False):
228
+ with gr.Row():
229
+ max_tokens = gr.Slider(
230
+ minimum=128,
231
+ maximum=2048,
232
+ step=64,
233
+ value=768,
234
+ label="Max audio tokens",
235
+ )
236
+ seed = gr.Number(value=-1, precision=0, label="Seed (-1 random)")
237
+ with gr.Row():
238
+ temperature = gr.Slider(0.1, 2.0, value=1.15, step=0.05, label="Temperature")
239
+ topk = gr.Slider(1, 512, value=106, step=1, label="Top-k")
240
+ with gr.Row():
241
+ top_p = gr.Slider(0.0, 1.0, value=0.0, step=0.01, label="Top-p")
242
+ min_p = gr.Slider(0.0, 0.5, value=0.18, step=0.01, label="Min-p")
243
+ repetition_penalty = gr.Slider(
244
+ 1.0,
245
+ 2.0,
246
+ value=1.2,
247
+ step=0.05,
248
+ label="Repetition penalty",
249
+ )
250
+
251
+ gr.Examples(
252
+ examples=[
253
+ [
254
+ "The first explorers landed just after sunrise, carrying maps, coffee, and impossible optimism.",
255
+ "en_us",
256
+ True,
257
+ "default",
258
+ 512,
259
+ 1.15,
260
+ 106,
261
+ 0.0,
262
+ 0.18,
263
+ 1.2,
264
+ -1,
265
+ ],
266
+ [
267
+ "Le modèle parle avec une voix claire, expressive et naturellement rythmée.",
268
+ "fr_fr",
269
+ True,
270
+ "default",
271
+ 512,
272
+ 1.15,
273
+ 106,
274
+ 0.0,
275
+ 0.18,
276
+ 1.2,
277
+ -1,
278
+ ],
279
+ ],
280
+ inputs=[
281
+ text,
282
+ language,
283
+ text_normalization,
284
+ speaking_rate,
285
+ max_tokens,
286
+ temperature,
287
+ topk,
288
+ top_p,
289
+ min_p,
290
+ repetition_penalty,
291
+ seed,
292
+ ],
293
+ )
294
+
295
+ generate.click(
296
+ fn=synthesize,
297
+ inputs=[
298
+ text,
299
+ language,
300
+ text_normalization,
301
+ speaking_rate,
302
+ max_tokens,
303
+ temperature,
304
+ topk,
305
+ top_p,
306
+ min_p,
307
+ repetition_penalty,
308
+ seed,
309
+ ],
310
+ outputs=[audio, status],
311
+ api_name="generate",
312
+ concurrency_limit=1,
313
+ )
314
+
315
+
316
+ if __name__ == "__main__":
317
+ demo.queue(max_size=8, default_concurrency_limit=1).launch()
requirements.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=6.10.0
2
+ spaces>=0.41.0
3
+ numpy
4
+ tqdm
5
+ msgpack
6
+ sgl_kernel>=0.3.17.post1
7
+ descript-audio-codec==1.0.0
8
+ transformers>=4.56.0,<=4.57.3
9
+ huggingface_hub
10
+ pyzmq
11
+ apache-tvm-ffi>=0.1.4
12
+ nvidia-cutlass-dsl==4.3.1
13
+ flashinfer-cubin==0.5.3
14
+ flashinfer-python>=0.5.3
15
+ flashinfer-jit-cache @ https://github.com/flashinfer-ai/flashinfer/releases/download/v0.5.3/flashinfer_jit_cache-0.5.3+cu128-cp39-abi3-manylinux_2_28_x86_64.whl
16
+ ninja>=1.13.0
17
+ kernels>=0.12.1
18
+ pynini==2.1.6
19
+ sacremoses>=0.1.1
zonos2/__main__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from .server import launch_server
2
+
3
+ assert __name__ == "__main__"
4
+
5
+ launch_server()
zonos2/attention/__init__.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Protocol
4
+
5
+ import torch
6
+ from zonos2.utils import Registry, init_logger, is_sm90_supported, is_sm100_supported
7
+
8
+ from .base import BaseAttnBackend, BaseAttnMetadata, HybridBackend
9
+
10
+ if TYPE_CHECKING:
11
+ from zonos2.kvcache import BaseKVCache
12
+ from zonos2.models import ModelConfig
13
+
14
+ logger = init_logger(__name__)
15
+
16
+
17
+ class BackendCreator(Protocol):
18
+ def __call__(
19
+ self, config: ModelConfig, kvcache: BaseKVCache, page_table: torch.Tensor
20
+ ) -> BaseAttnBackend: ...
21
+
22
+
23
+ SUPPORTED_ATTENTION_BACKENDS = Registry[BackendCreator]("Attention Backend")
24
+
25
+
26
+ def resolve_auto_backend(config: ModelConfig) -> str:
27
+ """Determine the best attention backend based on the GPU architecture and model."""
28
+ if is_sm100_supported(): # blackwell
29
+ return "fi"
30
+ elif is_sm90_supported(): # hopper
31
+ return "fa,fi"
32
+ else: # pre-hopper
33
+ return "fi"
34
+
35
+
36
+ @SUPPORTED_ATTENTION_BACKENDS.register("fi")
37
+ def create_fi_backend(config: ModelConfig, kvcache: BaseKVCache, page_table: torch.Tensor):
38
+ from .fi import FlashInferBackend
39
+
40
+ return FlashInferBackend(config, kvcache, page_table)
41
+
42
+
43
+ @SUPPORTED_ATTENTION_BACKENDS.register("fa")
44
+ def create_fa_backend(config: ModelConfig, kvcache: BaseKVCache, page_table: torch.Tensor):
45
+ from .fa import FlashAttentionBackend
46
+
47
+ return FlashAttentionBackend(config, kvcache, page_table)
48
+
49
+
50
+ def validate_backend(backend: str):
51
+ if backend != "auto":
52
+ required_backends = backend.split(",") if "," in backend else [backend]
53
+ supported = SUPPORTED_ATTENTION_BACKENDS.supported_names()
54
+ for b in required_backends:
55
+ if b not in supported:
56
+ from argparse import ArgumentTypeError
57
+
58
+ raise ArgumentTypeError(
59
+ f"Unsupported attention backend: {b}. Supported backends: {supported}"
60
+ )
61
+ return backend
62
+
63
+
64
+ def create_attention_backend(
65
+ backend: str,
66
+ config: ModelConfig,
67
+ kvcache: BaseKVCache,
68
+ page_table: torch.Tensor,
69
+ ) -> BaseAttnBackend:
70
+ if backend == "auto":
71
+ backend = resolve_auto_backend(config)
72
+ logger.info(f"Auto-selected attention backend: {backend}")
73
+
74
+ if "," in backend:
75
+ assert backend.count(",") == 1, "Only one comma is allowed in hybrid backend"
76
+ p_backend, d_backend = backend.split(",", 1)
77
+ if p_backend != d_backend:
78
+ logger.info(f"Using hybrid attention backend: prefill={p_backend}, decode={d_backend}")
79
+ p_backend = create_attention_backend(p_backend, config, kvcache, page_table)
80
+ d_backend = create_attention_backend(d_backend, config, kvcache, page_table)
81
+ return HybridBackend(p_backend, d_backend)
82
+ backend = p_backend # both are the same, fall through to single backend
83
+ logger.warning(f"P/D attention backends are the same: {backend}, using single backend.")
84
+
85
+ return SUPPORTED_ATTENTION_BACKENDS[backend](config, kvcache, page_table)
86
+
87
+
88
+ __all__ = [
89
+ "BaseAttnMetadata",
90
+ "BaseAttnBackend",
91
+ "create_attention_backend",
92
+ "SUPPORTED_ATTENTION_BACKENDS",
93
+ "validate_backend",
94
+ ]
zonos2/attention/base.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, List
6
+
7
+ if TYPE_CHECKING:
8
+ import torch
9
+ from zonos2.core import TTSBatch
10
+
11
+
12
+ @dataclass
13
+ class BaseAttnMetadata(ABC):
14
+ positions: torch.Tensor
15
+
16
+ @abstractmethod
17
+ def get_last_indices(self, bs: int) -> torch.Tensor: ...
18
+
19
+
20
+ class BaseAttnBackend(ABC):
21
+ @abstractmethod
22
+ def forward(
23
+ self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, layer_id: int, batch: TTSBatch
24
+ ) -> torch.Tensor: ...
25
+
26
+ @abstractmethod
27
+ def prepare_metadata(self, batch: TTSBatch) -> None: ...
28
+
29
+ @abstractmethod
30
+ def init_capture_graph(
31
+ self, max_seq_len: int, bs_list: List[int], frame_width: int = 1
32
+ ) -> None: ...
33
+
34
+ @abstractmethod
35
+ def prepare_for_capture(self, batch: TTSBatch) -> None: ...
36
+
37
+ @abstractmethod
38
+ def prepare_for_replay(self, batch: TTSBatch) -> None: ...
39
+
40
+
41
+ class HybridBackend(BaseAttnBackend):
42
+ def __init__(
43
+ self,
44
+ prefill_backend: BaseAttnBackend,
45
+ decode_backend: BaseAttnBackend,
46
+ ) -> None:
47
+ self.prefill_backend = prefill_backend
48
+ self.decode_backend = decode_backend
49
+
50
+ def forward(
51
+ self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, layer_id: int, batch: TTSBatch
52
+ ) -> torch.Tensor:
53
+ backend = self.prefill_backend if batch.is_prefill else self.decode_backend
54
+ return backend.forward(q, k, v, layer_id, batch)
55
+
56
+ def prepare_metadata(self, batch: TTSBatch) -> None:
57
+ backend = self.prefill_backend if batch.is_prefill else self.decode_backend
58
+ return backend.prepare_metadata(batch)
59
+
60
+ def init_capture_graph(
61
+ self, max_seq_len: int, bs_list: List[int], frame_width: int = 1
62
+ ) -> None:
63
+ self.decode_backend.init_capture_graph(max_seq_len, bs_list, frame_width)
64
+
65
+ def prepare_for_capture(self, batch: TTSBatch) -> None:
66
+ self.decode_backend.prepare_for_capture(batch)
67
+
68
+ def prepare_for_replay(self, batch: TTSBatch) -> None:
69
+ self.decode_backend.prepare_for_replay(batch)
zonos2/attention/fa.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, List, Tuple
5
+
6
+ import torch
7
+
8
+ from .base import BaseAttnBackend, BaseAttnMetadata
9
+ from .utils import BaseCaptureData, make_positions
10
+
11
+ if TYPE_CHECKING:
12
+ from zonos2.core import TTSBatch
13
+ from zonos2.kvcache import BaseKVCache
14
+ from zonos2.models import ModelConfig
15
+
16
+
17
+ @dataclass
18
+ class FACaptureData(BaseCaptureData):
19
+ pass
20
+
21
+
22
+ @dataclass
23
+ class FAMetadata(BaseAttnMetadata):
24
+ cu_seqlens_k: torch.Tensor
25
+ cu_seqlens_q: torch.Tensor
26
+ cache_seqlens: torch.Tensor
27
+ max_seqlen_k: int
28
+ max_seqlen_q: int
29
+
30
+ page_table: torch.Tensor
31
+
32
+ def get_positions(self) -> torch.Tensor:
33
+ return self.positions
34
+
35
+ def get_last_indices(self, bs: int) -> torch.Tensor:
36
+ return self.cu_seqlens_q[1 : 1 + bs] - 1
37
+
38
+
39
+ class FlashAttentionBackend(BaseAttnBackend):
40
+ def __init__(self, config: ModelConfig, kvcache: BaseKVCache, page_table: torch.Tensor):
41
+ self.config = config
42
+ self.kvcache = kvcache
43
+ self.capture: FACaptureData | None = None
44
+ self.max_graph_bs = 0
45
+ self.capture_bs: List[int] = []
46
+ self.scale = config.head_dim**-0.5
47
+ self.page_table = page_table
48
+
49
+ def forward(
50
+ self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, layer_id: int, batch: TTSBatch
51
+ ) -> torch.Tensor:
52
+ metadata = batch.attn_metadata
53
+ assert isinstance(metadata, FAMetadata)
54
+ self.kvcache.store_kv(k, v, batch.out_loc, layer_id)
55
+ return _fa_sgl_impl(
56
+ q=q,
57
+ k_cache=self.kvcache.k_cache(layer_id),
58
+ v_cache=self.kvcache.v_cache(layer_id),
59
+ page_table=metadata.page_table,
60
+ cache_seqlens=metadata.cache_seqlens,
61
+ cu_seqlens_q=metadata.cu_seqlens_q,
62
+ cu_seqlens_k_new=metadata.cu_seqlens_k,
63
+ max_seqlen_q=metadata.max_seqlen_q,
64
+ softmax_scale=self.scale,
65
+ )
66
+
67
+ def prepare_metadata(self, batch: TTSBatch) -> None:
68
+ reqs = batch.padded_reqs
69
+
70
+ padded_size = len(reqs)
71
+ seqlens_q = [req.extend_len for req in reqs]
72
+ seqlens_k = [req.device_len for req in reqs]
73
+ cached_lens = [req.cached_len for req in reqs]
74
+ max_seqlen_k = max(seqlens_k)
75
+ max_seqlen_q = max(seqlens_q)
76
+ cpu_kwargs = {"device": "cpu", "dtype": torch.int32, "pin_memory": True}
77
+
78
+ device = self.kvcache.device
79
+ cache_seqlens = torch.tensor(seqlens_k, **cpu_kwargs)
80
+ cache_seqlens = cache_seqlens.to(device, non_blocking=True)
81
+ cu_seqlens_k = torch.tensor([0] + seqlens_k, **cpu_kwargs).cumsum_(dim=0)
82
+ cu_seqlens_k = cu_seqlens_k.to(device, non_blocking=True)
83
+
84
+ if max_seqlen_q == 1:
85
+ cu_seqlens_q = torch.arange(0, padded_size + 1, device=device, dtype=torch.int32)
86
+ elif all(l == 0 for l in cached_lens): # prefill with no cache hit
87
+ cu_seqlens_q = cu_seqlens_k
88
+ else: # normal extend prefill, with partial cache hit
89
+ cu_seqlens_q = torch.tensor([0] + seqlens_q, **cpu_kwargs).cumsum_(dim=0)
90
+ cu_seqlens_q = cu_seqlens_q.to(self.kvcache.device, non_blocking=True)
91
+
92
+ positions = make_positions(device, reqs)
93
+ page_table = self.page_table
94
+ new_page_table = torch.stack([page_table[req.table_idx, :max_seqlen_k] for req in reqs])
95
+
96
+ # copy from CPU to GPU
97
+ batch.attn_metadata = FAMetadata(
98
+ cu_seqlens_k=cu_seqlens_k,
99
+ cu_seqlens_q=cu_seqlens_q,
100
+ positions=positions,
101
+ cache_seqlens=cache_seqlens,
102
+ max_seqlen_k=max_seqlen_k,
103
+ max_seqlen_q=max_seqlen_q,
104
+ page_table=new_page_table,
105
+ )
106
+
107
+ def init_capture_graph(
108
+ self, max_seq_len: int, bs_list: List[int], frame_width: int = 1
109
+ ) -> None:
110
+ assert self.capture is None, "Capture already initialized."
111
+ max_bs = max(bs_list)
112
+ capture = FACaptureData.create(max_bs, max_seq_len, self.kvcache.device, frame_width)
113
+ self.max_graph_bs = max_bs
114
+ self.capture = capture
115
+ self.capture_bs = sorted(bs_list)
116
+
117
+ def prepare_for_capture(self, batch: TTSBatch) -> None:
118
+ assert (bs := batch.size) in self.capture_bs and self.capture
119
+ capture = self.capture
120
+ metadata = FAMetadata(
121
+ cu_seqlens_k=capture.cu_seqlens_k[: bs + 1],
122
+ cu_seqlens_q=capture.cu_seqlens_q[: bs + 1],
123
+ positions=capture.positions[:bs],
124
+ cache_seqlens=capture.seq_lens[:bs],
125
+ max_seqlen_k=capture.page_table.size(1),
126
+ max_seqlen_q=1, # decode only
127
+ page_table=capture.page_table[:bs, :],
128
+ )
129
+ batch.attn_metadata = metadata
130
+ batch.input_ids = capture.input_ids[:bs]
131
+ batch.out_loc = capture.out_loc[:bs]
132
+
133
+ def prepare_for_replay(self, batch: TTSBatch) -> None:
134
+ metadata, bs = batch.attn_metadata, batch.padded_size
135
+ assert isinstance(metadata, FAMetadata)
136
+ assert self.capture is not None and bs in self.capture_bs
137
+ capture = self.capture
138
+
139
+ # Copy all dynamic tensors to capture buffers (fixed memory addresses for CUDA graph)
140
+ capture.input_ids[:bs].copy_(batch.input_ids)
141
+ capture.out_loc[:bs].copy_(batch.out_loc)
142
+ capture.positions[:bs].copy_(metadata.positions)
143
+ capture.cu_seqlens_k[: bs + 1].copy_(metadata.cu_seqlens_k)
144
+ capture.seq_lens[:bs].copy_(metadata.cache_seqlens)
145
+ capture.page_table[:bs, : metadata.max_seqlen_k].copy_(metadata.page_table)
146
+
147
+ # Update metadata and batch to point to capture buffers
148
+ # This ensures CUDA graph uses fixed memory addresses for all inputs
149
+ metadata.positions = capture.positions[:bs]
150
+ metadata.cu_seqlens_k = capture.cu_seqlens_k[: bs + 1]
151
+ metadata.cache_seqlens = capture.seq_lens[:bs]
152
+ metadata.page_table = capture.page_table[:bs, :]
153
+ batch.input_ids = capture.input_ids[:bs]
154
+ batch.out_loc = capture.out_loc[:bs]
155
+
156
+
157
+ def _fa_sgl_impl(
158
+ q: torch.Tensor,
159
+ k_cache: torch.Tensor,
160
+ v_cache: torch.Tensor,
161
+ page_table: torch.Tensor,
162
+ cache_seqlens: torch.Tensor,
163
+ cu_seqlens_q: torch.Tensor,
164
+ cu_seqlens_k_new: torch.Tensor,
165
+ max_seqlen_q: int,
166
+ softmax_scale: float,
167
+ sm_margin: int = 0,
168
+ window_size: Tuple[int, int] = (-1, -1), # -1 means infinite context window
169
+ softcap: float = 0.0, # 0.0 means deactivated
170
+ num_splits: int = 0, # Can be tuned for speed
171
+ pack_gqa: bool | None = None, # Can be tuned for speed
172
+ causal: bool = True,
173
+ ) -> torch.Tensor:
174
+ try:
175
+ from sgl_kernel.flash_attn import flash_attn_with_kvcache
176
+ except ImportError as e:
177
+ raise ImportError(
178
+ "sgl_kernel.flash_attn is not found. Please install it with `pip install sgl-kernel`.\n"
179
+ "If you're sure it's correctly installed, try `apt update && apt install libnuma1`."
180
+ ) from e
181
+
182
+ return flash_attn_with_kvcache( # type: ignore
183
+ q=q,
184
+ k_cache=k_cache,
185
+ v_cache=v_cache,
186
+ page_table=page_table,
187
+ cache_seqlens=cache_seqlens,
188
+ cu_seqlens_q=cu_seqlens_q,
189
+ cu_seqlens_k_new=cu_seqlens_k_new,
190
+ max_seqlen_q=max_seqlen_q,
191
+ softmax_scale=softmax_scale,
192
+ sm_margin=sm_margin,
193
+ window_size=window_size,
194
+ softcap=softcap,
195
+ num_splits=num_splits,
196
+ pack_gqa=pack_gqa,
197
+ causal=causal,
198
+ ver=3, # TODO: support FA4 on blackwell
199
+ )
zonos2/attention/fi.py ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import math
5
+ from dataclasses import dataclass
6
+ from functools import cached_property
7
+ from typing import TYPE_CHECKING, Dict, List, Literal
8
+
9
+ import torch
10
+ from zonos2.distributed import get_tp_info
11
+ from zonos2.env import ENV
12
+ from zonos2.utils import divide_even
13
+ from zonos2.utils.logger import init_logger
14
+
15
+ from .base import BaseAttnBackend, BaseAttnMetadata
16
+ from .utils import BaseCaptureData, make_positions
17
+
18
+ if TYPE_CHECKING:
19
+ from flashinfer import (
20
+ BatchDecodeWithPagedKVCacheWrapper,
21
+ BatchPrefillWithPagedKVCacheWrapper,
22
+ CUDAGraphBatchDecodeWithPagedKVCacheWrapper,
23
+ )
24
+ from zonos2.core import TTSBatch
25
+ from zonos2.kvcache import BaseKVCache
26
+ from zonos2.models import ModelConfig
27
+
28
+
29
+ def _next_power_of_2(n: int) -> int:
30
+ if n <= 1:
31
+ return 1
32
+ return 1 << math.ceil(math.log2(n))
33
+
34
+
35
+ logger = init_logger(__name__)
36
+
37
+ _fi_debug_counter = 0
38
+
39
+
40
+ @dataclass
41
+ class FICaptureData(BaseCaptureData):
42
+ @property
43
+ def one_tensor(self) -> torch.Tensor:
44
+ return self.seq_lens
45
+
46
+ @property
47
+ def indices(self) -> torch.Tensor:
48
+ return self.page_table
49
+
50
+
51
+ @dataclass
52
+ class FIMetadata(BaseAttnMetadata):
53
+ # fmt: off
54
+ cu_seqlens_q_cpu: torch.Tensor # on cpu
55
+ cu_seqlens_k_cpu: torch.Tensor # on cpu
56
+ cu_seqlens_q_gpu: torch.Tensor # on gpu
57
+ indices: torch.Tensor # on gpu
58
+ last_page_len_cpu: torch.Tensor # on cpu
59
+ num_qo_heads: int
60
+ num_kv_heads: int
61
+ head_dim: int
62
+ page_size: Literal[1] # currently only support page_size=1
63
+ pos_encoding_mode: str
64
+ seq_lens_cpu: torch.Tensor # on cpu
65
+ dtype: torch.dtype
66
+ wrapper: BatchPrefillWithPagedKVCacheWrapper | BatchDecodeWithPagedKVCacheWrapper
67
+ initialized: bool = False
68
+ sm_scale: float = 0.0 # softmax scale (1/sqrt(head_dim)), set during prepare_metadata
69
+ # fmt: on
70
+
71
+ def __post_init__(self) -> None:
72
+ assert self.page_size == 1, "Currently only page_size=1 is supported."
73
+ assert (
74
+ self.positions.is_cuda
75
+ and self.cu_seqlens_k_cpu.is_cpu
76
+ and self.cu_seqlens_q_cpu.is_cpu
77
+ and self.cu_seqlens_q_gpu.is_cuda
78
+ and self.indices.is_cuda
79
+ and self.last_page_len_cpu.is_cpu
80
+ and self.seq_lens_cpu.is_cpu
81
+ )
82
+
83
+ def get_positions(self) -> torch.Tensor:
84
+ return self.positions
85
+
86
+ def get_last_indices(self, bs: int) -> torch.Tensor:
87
+ return self.cu_seqlens_q_gpu[1 : 1 + bs] - 1
88
+
89
+
90
+ class FlashInferBackend(BaseAttnBackend):
91
+ def __init__(
92
+ self,
93
+ config: ModelConfig,
94
+ kvcache: BaseKVCache,
95
+ page_table: torch.Tensor,
96
+ ) -> None:
97
+ from flashinfer import (
98
+ BatchDecodeWithPagedKVCacheWrapper,
99
+ BatchPrefillWithPagedKVCacheWrapper,
100
+ )
101
+
102
+ self.config = config
103
+ self.kvcache = kvcache
104
+ self.device = kvcache.device
105
+ self.float_workspace_buffer = torch.empty(
106
+ 128 * 1024 * 1024, dtype=torch.uint8, device=self.device
107
+ )
108
+ self.prefill_wrapper = BatchPrefillWithPagedKVCacheWrapper(
109
+ self.float_workspace_buffer,
110
+ kv_layout="NHD",
111
+ backend="fa2", # flashinfer fa3 is buggy, use fa2 instead
112
+ )
113
+ self.decode_wrappers = BatchDecodeWithPagedKVCacheWrapper(
114
+ self.float_workspace_buffer,
115
+ kv_layout="NHD",
116
+ )
117
+
118
+ # NOTE: some hack to reuse the int_workspace_buffer
119
+ self.int_workspace_buffer = self.prefill_wrapper._int_workspace_buffer
120
+ self.decode_wrappers._int_workspace_buffer = self.int_workspace_buffer
121
+
122
+ # initialize some data members
123
+ tp_size = get_tp_info().size
124
+ self.qo_head_local = divide_even(self.config.num_qo_heads, tp_size)
125
+ self.kv_head_local = divide_even(self.config.num_kv_heads, tp_size)
126
+
127
+ self.cached_ones_cpu: torch.Tensor = torch.tensor([], dtype=torch.int32, pin_memory=True)
128
+ # for cuda graph
129
+ self.capture_bs: List[int] = []
130
+ self.max_graph_bs = 0
131
+ self.graph_wrappers: Dict[int, CUDAGraphBatchDecodeWithPagedKVCacheWrapper] = {}
132
+ self.capture: FICaptureData | None = None
133
+ self.page_table = page_table
134
+
135
+ @staticmethod
136
+ def _initialize_metadata_once(metadata: FIMetadata) -> None:
137
+ if metadata.initialized:
138
+ return
139
+
140
+ from flashinfer import (
141
+ BatchDecodeWithPagedKVCacheWrapper,
142
+ CUDAGraphBatchDecodeWithPagedKVCacheWrapper,
143
+ )
144
+
145
+ metadata.initialized = True
146
+
147
+ is_cuda_graph = isinstance(metadata.wrapper, CUDAGraphBatchDecodeWithPagedKVCacheWrapper)
148
+ is_decode = isinstance(metadata.wrapper, BatchDecodeWithPagedKVCacheWrapper) or is_cuda_graph
149
+
150
+ if is_cuda_graph and logger.isEnabledFor(logging.DEBUG):
151
+ global _fi_debug_counter
152
+ if _fi_debug_counter <= 5:
153
+ logger.debug("plan() for CUDAGraphWrapper: indices shape=%s", metadata.indices.shape)
154
+ logger.debug(" cu_seqlens_k_cpu=%s", metadata.cu_seqlens_k_cpu.tolist())
155
+
156
+ if is_decode:
157
+ # Decode wrapper (both regular and CUDA graph variants)
158
+ # sm_scale must be passed to plan() for decode, not run()
159
+ metadata.wrapper.plan(
160
+ indptr=metadata.cu_seqlens_k_cpu,
161
+ indices=metadata.indices,
162
+ last_page_len=metadata.last_page_len_cpu,
163
+ num_qo_heads=metadata.num_qo_heads,
164
+ num_kv_heads=metadata.num_kv_heads,
165
+ head_dim=metadata.head_dim,
166
+ page_size=metadata.page_size,
167
+ pos_encoding_mode=metadata.pos_encoding_mode,
168
+ seq_lens=metadata.seq_lens_cpu,
169
+ data_type=metadata.dtype,
170
+ q_data_type=metadata.dtype,
171
+ kv_data_type=metadata.dtype,
172
+ sm_scale=metadata.sm_scale,
173
+ non_blocking=True,
174
+ )
175
+ else:
176
+ # Prefill wrapper - sm_scale passed to plan()
177
+ metadata.wrapper.plan(
178
+ qo_indptr=metadata.cu_seqlens_q_cpu,
179
+ paged_kv_indptr=metadata.cu_seqlens_k_cpu,
180
+ paged_kv_indices=metadata.indices,
181
+ paged_kv_last_page_len=metadata.last_page_len_cpu,
182
+ num_qo_heads=metadata.num_qo_heads,
183
+ num_kv_heads=metadata.num_kv_heads,
184
+ head_dim_qk=metadata.head_dim,
185
+ page_size=metadata.page_size,
186
+ pos_encoding_mode=metadata.pos_encoding_mode,
187
+ seq_lens=metadata.seq_lens_cpu,
188
+ q_data_type=metadata.dtype,
189
+ kv_data_type=metadata.dtype,
190
+ sm_scale=metadata.sm_scale,
191
+ non_blocking=True,
192
+ causal=True,
193
+ )
194
+
195
+ def _get_ones_cpu(self, bs: int) -> torch.Tensor:
196
+ if bs <= len(self.cached_ones_cpu):
197
+ return self.cached_ones_cpu[:bs]
198
+ # padding to next pow of 2
199
+ next_len = _next_power_of_2(bs)
200
+ self.cached_ones_cpu = torch.ones(next_len, dtype=torch.int32, pin_memory=True)
201
+ return self.cached_ones_cpu[:bs]
202
+
203
+ def forward(
204
+ self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, layer_id: int, batch: TTSBatch
205
+ ) -> torch.Tensor:
206
+ metadata = batch.attn_metadata
207
+ assert isinstance(metadata, FIMetadata)
208
+ self._initialize_metadata_once(metadata)
209
+ self.kvcache.store_kv(k, v, batch.out_loc, layer_id)
210
+ kv_cache = (self.kvcache.k_cache(layer_id), self.kvcache.v_cache(layer_id))
211
+
212
+ # Debug: check attention inputs/outputs for first few steps at layer 0
213
+ global _fi_debug_counter
214
+ is_capturing = torch.cuda.is_current_stream_capturing()
215
+ if (logger.isEnabledFor(logging.DEBUG)
216
+ and layer_id == 0 and batch.is_decode and not is_capturing
217
+ and _fi_debug_counter < 10):
218
+ _fi_debug_counter += 1
219
+ step = _fi_debug_counter
220
+ logger.debug("ATTN Step %d Layer 0", step)
221
+ logger.debug(" Q shape: %s, norm: %.4f, mean: %.6f", q.shape, q.norm().item(), q.mean().item())
222
+ logger.debug(" K shape: %s, norm: %.4f, mean: %.6f", k.shape, k.norm().item(), k.mean().item())
223
+ logger.debug(" V shape: %s, norm: %.4f, mean: %.6f", v.shape, v.norm().item(), v.mean().item())
224
+ logger.debug(" out_loc: %s", batch.out_loc.tolist())
225
+ logger.debug(" positions: %s", metadata.positions.tolist())
226
+ logger.debug(" indices shape: %s, first 10: %s", metadata.indices.shape, metadata.indices[:10].tolist())
227
+ logger.debug(" cu_seqlens_k: %s", metadata.cu_seqlens_k_cpu.tolist())
228
+ k_cache, v_cache = kv_cache
229
+ if batch.out_loc.numel() > 0:
230
+ slot = batch.out_loc[0].item()
231
+ logger.debug(" KV cache slot %d: k_norm=%.4f", slot, k_cache.view(-1, k_cache.shape[-1])[slot].norm().item())
232
+
233
+ output = metadata.wrapper.run(q=q, paged_kv_cache=kv_cache)
234
+
235
+ if (logger.isEnabledFor(logging.DEBUG)
236
+ and layer_id == 0 and batch.is_decode and not is_capturing
237
+ and _fi_debug_counter <= 10):
238
+ logger.debug(" Output shape: %s, norm: %.4f, mean: %.6f", output.shape, output.norm().item(), output.mean().item())
239
+
240
+ return output
241
+
242
+ def prepare_metadata(self, batch: TTSBatch) -> None:
243
+ reqs = batch.padded_reqs
244
+
245
+ padded_size = len(reqs)
246
+ seqlens_q = [req.extend_len for req in reqs]
247
+ seqlens_k = [req.device_len for req in reqs]
248
+ cached_lens = [req.cached_len for req in reqs]
249
+ max_seqlen_q = max(seqlens_q)
250
+ cpu_kwargs = {"device": "cpu", "dtype": torch.int32, "pin_memory": True}
251
+
252
+ device = self.device
253
+ seq_len_cpu = torch.tensor(seqlens_k, **cpu_kwargs)
254
+ cu_seqlens_k_cpu = torch.tensor([0] + seqlens_k, **cpu_kwargs).cumsum_(dim=0)
255
+ if max_seqlen_q == 1: # decode with all extend_len = 1
256
+ cu_seqlens_q_cpu = torch.arange(0, padded_size + 1, **cpu_kwargs)
257
+ elif all(l == 0 for l in cached_lens): # prefill with no cache hit
258
+ cu_seqlens_q_cpu = cu_seqlens_k_cpu
259
+ else: # normal extend prefill, with partial cache hit
260
+ cu_seqlens_q_cpu = torch.tensor([0] + seqlens_q, **cpu_kwargs).cumsum_(dim=0)
261
+ batch.attn_metadata = FIMetadata(
262
+ positions=make_positions(device, reqs),
263
+ cu_seqlens_q_cpu=cu_seqlens_q_cpu,
264
+ cu_seqlens_k_cpu=cu_seqlens_k_cpu,
265
+ cu_seqlens_q_gpu=cu_seqlens_q_cpu.to(device, non_blocking=True),
266
+ indices=torch.cat([self.page_table[req.table_idx, : req.device_len] for req in reqs]),
267
+ last_page_len_cpu=self._get_ones_cpu(padded_size),
268
+ num_qo_heads=self.qo_head_local,
269
+ num_kv_heads=self.kv_head_local,
270
+ head_dim=self.config.head_dim,
271
+ page_size=1,
272
+ pos_encoding_mode="NONE",
273
+ seq_lens_cpu=seq_len_cpu,
274
+ dtype=self.kvcache.dtype,
275
+ wrapper=self.decode_wrappers if batch.is_decode else self.prefill_wrapper,
276
+ sm_scale=1.0 / math.sqrt(self.config.head_dim),
277
+ )
278
+
279
+ def init_capture_graph(
280
+ self, max_seq_len: int, bs_list: List[int], frame_width: int = 1
281
+ ) -> None:
282
+ assert self.capture is None, "Capture already initialized."
283
+ max_bs = max(bs_list)
284
+ capture = FICaptureData.create(max_bs, max_seq_len, self.kvcache.device, frame_width)
285
+ capture.page_table = capture.page_table.view(-1) # use 1D as ragged indices
286
+ self.max_graph_bs = max_bs
287
+ self.capture = capture
288
+ self.capture_bs = sorted(bs_list)
289
+
290
+ @cached_property
291
+ def use_tensor_cores(self) -> bool:
292
+ if (overriden_value := ENV.FLASHINFER_USE_TENSOR_CORES.value) is not None:
293
+ logger.warning(f"Overriding FlashInfer tensor core usage to {overriden_value}")
294
+ return overriden_value
295
+ GQA = self.config.num_qo_heads // self.config.num_kv_heads
296
+ return GQA >= 4
297
+
298
+ def prepare_for_capture(self, batch: TTSBatch) -> None:
299
+ from flashinfer import CUDAGraphBatchDecodeWithPagedKVCacheWrapper
300
+
301
+ bs = batch.size
302
+ assert bs in self.capture_bs and bs not in self.graph_wrappers and self.capture
303
+ batch.padded_reqs = batch.reqs
304
+ capture = self.capture
305
+ self.graph_wrappers[bs] = CUDAGraphBatchDecodeWithPagedKVCacheWrapper(
306
+ self.float_workspace_buffer,
307
+ kv_layout="NHD",
308
+ use_tensor_cores=self.use_tensor_cores,
309
+ indptr_buffer=capture.cu_seqlens_k[: bs + 1],
310
+ indices_buffer=capture.indices,
311
+ last_page_len_buffer=capture.one_tensor[:bs],
312
+ )
313
+ self.graph_wrappers[bs]._int_workspace_buffer = self.int_workspace_buffer
314
+ self.prepare_metadata(batch)
315
+ metadata = batch.attn_metadata
316
+ assert isinstance(metadata, FIMetadata)
317
+ # Copy metadata tensors to capture buffers and update metadata to use capture buffers
318
+ # This ensures CUDA graph uses fixed memory addresses
319
+ indices_len = metadata.indices.shape[0]
320
+ capture.indices[:indices_len].copy_(metadata.indices)
321
+ capture.cu_seqlens_k[: bs + 1].copy_(metadata.cu_seqlens_k_cpu.to(capture.indices.device))
322
+ capture.seq_lens[:bs].copy_(metadata.seq_lens_cpu.to(capture.indices.device))
323
+ # Update metadata to point to capture buffers (critical for CUDA graph)
324
+ metadata.indices = capture.indices[:indices_len]
325
+ metadata.wrapper = self.graph_wrappers[bs]
326
+ metadata.positions = capture.positions[:bs]
327
+ batch.input_ids = capture.input_ids[:bs]
328
+ batch.out_loc = capture.out_loc[:bs]
329
+ self._initialize_metadata_once(metadata)
330
+
331
+ def prepare_for_replay(self, batch: TTSBatch) -> None:
332
+ metadata, bs = batch.attn_metadata, batch.padded_size
333
+ assert isinstance(metadata, FIMetadata) and not metadata.initialized
334
+ assert self.capture is not None and bs in self.capture_bs
335
+ capture = self.capture
336
+
337
+ if logger.isEnabledFor(logging.DEBUG) and _fi_debug_counter < 5:
338
+ logger.debug("prepare_for_replay: bs=%d, indices_len=%d", bs, metadata.indices.shape[0])
339
+ logger.debug(" cu_seqlens_k_cpu=%s", metadata.cu_seqlens_k_cpu.tolist())
340
+
341
+ # Copy all dynamic tensors to capture buffers (fixed memory addresses for CUDA graph)
342
+ capture.input_ids[:bs].copy_(batch.input_ids)
343
+ capture.out_loc[:bs].copy_(batch.out_loc)
344
+ capture.positions[:bs].copy_(metadata.positions)
345
+ # Copy indices (page table entries) - critical for KV cache lookups
346
+ indices_len = metadata.indices.shape[0]
347
+ capture.indices[:indices_len].copy_(metadata.indices)
348
+ # Copy cu_seqlens_k for the wrapper's indptr
349
+ capture.cu_seqlens_k[: bs + 1].copy_(metadata.cu_seqlens_k_cpu.to(capture.indices.device))
350
+ # Copy seq_lens for the wrapper
351
+ capture.seq_lens[:bs].copy_(metadata.seq_lens_cpu.to(capture.indices.device))
352
+ # Update metadata to point to capture buffers (CRITICAL: must match prepare_for_capture)
353
+ # This ensures CUDA graph uses fixed memory addresses for all inputs
354
+ metadata.indices = capture.indices[:indices_len]
355
+ metadata.positions = capture.positions[:bs]
356
+ metadata.wrapper = self.graph_wrappers[bs]
357
+ # Update batch input_ids and out_loc to point to capture buffers
358
+ batch.input_ids = capture.input_ids[:bs]
359
+ batch.out_loc = capture.out_loc[:bs]
360
+ self._initialize_metadata_once(metadata)
zonos2/attention/utils.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, List
5
+
6
+ import torch
7
+
8
+ if TYPE_CHECKING:
9
+ from zonos2.core import TTSReq
10
+
11
+
12
+ @dataclass
13
+ class BaseCaptureData:
14
+ input_ids: torch.Tensor
15
+ seq_lens: torch.Tensor
16
+ positions: torch.Tensor
17
+ cu_seqlens_k: torch.Tensor
18
+ cu_seqlens_q: torch.Tensor
19
+ page_table: torch.Tensor
20
+ out_loc: torch.Tensor
21
+
22
+ @classmethod
23
+ def create(
24
+ cls, max_bs: int, max_seq_len: int, device: torch.device, frame_width: int = 1, **kwargs
25
+ ):
26
+ # Audio token frames are 2D (batch, frame_width); frame_width == 1 keeps a 1D layout
27
+ if frame_width > 1:
28
+ input_ids = torch.zeros((max_bs, frame_width), dtype=torch.int32, device=device)
29
+ else:
30
+ input_ids = torch.zeros((max_bs,), dtype=torch.int32, device=device)
31
+ return cls(
32
+ input_ids=input_ids,
33
+ seq_lens=torch.ones((max_bs,), dtype=torch.int32, device=device),
34
+ positions=torch.zeros((max_bs,), dtype=torch.int32, device=device),
35
+ cu_seqlens_k=torch.arange(0, max_bs + 1, dtype=torch.int32, device=device),
36
+ cu_seqlens_q=torch.arange(0, max_bs + 1, dtype=torch.int32, device=device),
37
+ page_table=torch.zeros((max_bs, max_seq_len), dtype=torch.int32, device=device),
38
+ out_loc=torch.zeros((max_bs,), dtype=torch.int32, device=device),
39
+ **kwargs,
40
+ )
41
+
42
+
43
+ def make_positions(device: torch.device, reqs: List[TTSReq]) -> torch.Tensor:
44
+ needed_size = sum(req.extend_len for req in reqs)
45
+ indices_host = torch.empty(needed_size, dtype=torch.int32, pin_memory=True)
46
+ offset = 0
47
+ for req in reqs:
48
+ length = req.extend_len
49
+ torch.arange(
50
+ req.cached_len,
51
+ req.device_len,
52
+ dtype=torch.int32,
53
+ out=indices_host[offset : offset + length],
54
+ )
55
+ offset += length
56
+ return indices_host.to(device, non_blocking=True)
zonos2/benchmark/client.py ADDED
@@ -0,0 +1,501 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import random
5
+ import time
6
+ from contextlib import contextmanager
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Dict, List, Tuple, overload
9
+
10
+ from openai import AsyncOpenAI as OpenAI
11
+ from pydantic import BaseModel
12
+ from tqdm.asyncio import tqdm
13
+ from zonos2.utils import UNSET, Unset, init_logger
14
+
15
+ logger = init_logger(__name__)
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class BenchmarkTrace:
20
+ timestamp: float
21
+ message: str # unit (second)
22
+ output_length: int # output length in tokens
23
+ input_length: int | None = None # input length in tokens, optional
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class BenchOneResult:
28
+ tics: List[float]
29
+ input_len: int
30
+ output_len: int
31
+
32
+ def as_json(self) -> List[float]:
33
+ return [self.input_len, self.output_len] + self.tics
34
+
35
+ @staticmethod
36
+ def from_json(raw: List[float]) -> BenchOneResult:
37
+ # check raw[0] and raw[1] are integers
38
+ assert raw[0].is_integer() and raw[1].is_integer()
39
+ return BenchOneResult(tics=raw[2:], input_len=int(raw[0]), output_len=int(raw[1]))
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class RawResult:
44
+ input_len: int | None
45
+ output_len: int
46
+ message: str
47
+ tics: List[float]
48
+
49
+
50
+ @dataclass
51
+ class Counter:
52
+ current: int = 0
53
+ history_max: int = 0
54
+
55
+ def inc(self, n=1):
56
+ self.current += n
57
+ self.history_max = max(self.history_max, self.current)
58
+
59
+ def dec(self, n=1):
60
+ self.current -= n
61
+ assert self.current >= 0
62
+
63
+
64
+ @dataclass
65
+ class Console:
66
+ input_pbar: tqdm
67
+ output_pbar: tqdm
68
+ prefill_pbar: tqdm
69
+ decode_pbar: tqdm
70
+ disabled: bool
71
+ inflight_counter: Counter = field(default_factory=Counter)
72
+ queue_counter: Counter = field(default_factory=Counter)
73
+
74
+ def update_input(self, n=1):
75
+ self.input_pbar.update(n)
76
+ self.input_pbar.refresh()
77
+ self.inflight_counter.inc(n)
78
+ self.queue_counter.inc(n)
79
+
80
+ def update_output(self, n=1):
81
+ self.output_pbar.update(n)
82
+ self.output_pbar.refresh()
83
+ self.inflight_counter.dec(n)
84
+
85
+ def update_prefill(self, n=1):
86
+ self.prefill_pbar.update(n)
87
+ self.prefill_pbar.refresh()
88
+ self.queue_counter.dec(n)
89
+
90
+ def update_decode(self, n=1):
91
+ self.decode_pbar.update(n)
92
+
93
+ @contextmanager
94
+ def inflight(self, n=1):
95
+ self.update_input(n)
96
+ yield
97
+ self.update_output(n)
98
+
99
+ @contextmanager
100
+ def log_stats(self):
101
+ yield
102
+ self.input_pbar.close()
103
+ self.output_pbar.close()
104
+ self.prefill_pbar.close()
105
+ self.decode_pbar.close()
106
+ if not self.disabled:
107
+ max_inflight = self.inflight_counter.history_max
108
+ max_queue = self.queue_counter.history_max
109
+ logger.info(f"Max inflight requests: {max_inflight}, Max queued requests: {max_queue}")
110
+
111
+
112
+ @dataclass(frozen=True)
113
+ class BenchmarkResult:
114
+ raw_data: List[BenchOneResult]
115
+
116
+ def as_json(self) -> List[List[float]]:
117
+ return [r.as_json() for r in self.raw_data]
118
+
119
+ @staticmethod
120
+ def from_json(raw: List[List[float]]) -> BenchmarkResult:
121
+ return BenchmarkResult(raw_data=[BenchOneResult.from_json(r) for r in raw])
122
+
123
+
124
+ def make_console(num_requests: int, sum_output_length: int, use_pbar: bool = True) -> Console:
125
+ BAR_FORMAT_0 = (
126
+ "{desc:<10} {percentage:3.0f}%|{bar}|"
127
+ " {n_fmt:>5}/{total_fmt} "
128
+ "[{rate_fmt:>12} {elapsed:>8}/{remaining:<8}]"
129
+ )
130
+ BAR_FORMAT_1 = BAR_FORMAT_0
131
+ n_fmt_align = 5
132
+ prefill_tokens = num_requests
133
+ decode_tokens = sum_output_length - prefill_tokens
134
+
135
+ if len(str(decode_tokens)) > n_fmt_align:
136
+ n_fmt_align = len(str(decode_tokens))
137
+ BAR_FORMAT_0 = BAR_FORMAT_0.replace("{n_fmt:>5}", "{n_fmt:>" + str(n_fmt_align) + "}")
138
+ BAR_FORMAT_1 = BAR_FORMAT_0
139
+
140
+ if len(str(prefill_tokens)) < len(str(decode_tokens)):
141
+ old_align_str = "{n_fmt:>" + str(n_fmt_align) + "}"
142
+ n_fmt_align += len(str(decode_tokens)) - len(str(prefill_tokens))
143
+ BAR_FORMAT_0 = BAR_FORMAT_0.replace(old_align_str, "{n_fmt:>" + str(n_fmt_align) + "}")
144
+
145
+ disabled = not use_pbar
146
+ input_pbar = tqdm(
147
+ total=num_requests,
148
+ desc="Requests sent",
149
+ position=0,
150
+ bar_format=BAR_FORMAT_0,
151
+ disable=disabled,
152
+ )
153
+ output_pbar = tqdm(
154
+ total=num_requests,
155
+ desc="Requests done",
156
+ position=1,
157
+ bar_format=BAR_FORMAT_0,
158
+ disable=disabled,
159
+ )
160
+ prefill_pbar = tqdm(
161
+ total=prefill_tokens,
162
+ desc="Prefill token",
163
+ position=2,
164
+ bar_format=BAR_FORMAT_0,
165
+ disable=disabled,
166
+ )
167
+ decode_pbar = tqdm(
168
+ total=decode_tokens,
169
+ desc="Decode token ",
170
+ position=3,
171
+ bar_format=BAR_FORMAT_1,
172
+ disable=disabled,
173
+ )
174
+ return Console(
175
+ input_pbar=input_pbar,
176
+ output_pbar=output_pbar,
177
+ prefill_pbar=prefill_pbar,
178
+ decode_pbar=decode_pbar,
179
+ disabled=disabled,
180
+ )
181
+
182
+
183
+ def generate_prompt(tokenizer: Any, n: int) -> str:
184
+ """Generate a prompt of approximately `n` tokens using the provided tokenizer."""
185
+ vocab_size = tokenizer.vocab_size // 2
186
+ token_ids = [random.randint(0, vocab_size) for _ in range(n - 1)]
187
+
188
+ for _ in range(64):
189
+ prompt = tokenizer.decode(token_ids)
190
+ token_ids = tokenizer.encode(prompt, add_special_tokens=False)
191
+ if len(token_ids) == n:
192
+ return prompt
193
+ if len(token_ids) < n:
194
+ need = n - len(token_ids)
195
+ token_ids.extend([random.randint(0, vocab_size) for _ in range(need)])
196
+ else:
197
+ token_ids = token_ids[:n]
198
+
199
+ raise ValueError("Failed to generate a message of the desired length.")
200
+
201
+
202
+ async def benchmark_one(
203
+ client: OpenAI,
204
+ prompt: str,
205
+ output_length: int,
206
+ model: str,
207
+ *,
208
+ pbar: Console | bool = True,
209
+ extra_body: Dict[str, Any] | None = None,
210
+ input_length: int | None = None, # a hack to force input length
211
+ ) -> RawResult:
212
+ if isinstance(pbar, bool):
213
+ pbar = make_console(1, output_length, use_pbar=pbar)
214
+ with pbar.inflight(1):
215
+ kwargs = {
216
+ "ignore_eos": True,
217
+ "top_k": 1,
218
+ }
219
+ # this is an internal kwargs that might work for our system
220
+ if input_length is not None:
221
+ kwargs["input_length_override"] = input_length
222
+ kwargs.update(extra_body or {}) # can override kwargs
223
+ response = await client.chat.completions.create(
224
+ model=model,
225
+ stream=True,
226
+ messages=[
227
+ {
228
+ "role": "user",
229
+ "content": prompt,
230
+ },
231
+ ],
232
+ max_tokens=output_length,
233
+ temperature=0.0,
234
+ extra_body=kwargs,
235
+ )
236
+ tics = [time.perf_counter()]
237
+ async for _ in response:
238
+ tics.append(time.perf_counter())
239
+ if len(tics) == 2:
240
+ pbar.update_prefill()
241
+ elif len(tics) <= output_length + 1:
242
+ pbar.update_decode()
243
+ return RawResult(
244
+ input_len=input_length,
245
+ output_len=output_length,
246
+ message=prompt,
247
+ tics=tics,
248
+ )
249
+
250
+
251
+ async def benchmark_one_batch(
252
+ client: OpenAI,
253
+ prompts: List[str],
254
+ output_lengths: List[int] | int,
255
+ model: str,
256
+ *,
257
+ extra_body: Dict[str, Any] | None = None,
258
+ input_lengths: List[int | None] | None = None,
259
+ pbar: Console | bool = True,
260
+ ) -> List[RawResult]:
261
+ if isinstance(output_lengths, int):
262
+ output_lengths = [output_lengths] * len(prompts)
263
+ if isinstance(pbar, bool):
264
+ pbar = make_console(len(prompts), sum(output_lengths), use_pbar=pbar)
265
+ if input_lengths is None:
266
+ l: List[int | None] = [None] * len(prompts)
267
+ input_lengths = l # work-around for typing bug
268
+
269
+ tasks = [
270
+ benchmark_one(
271
+ client=client,
272
+ prompt=prompt,
273
+ output_length=output_length,
274
+ model=model,
275
+ pbar=pbar,
276
+ extra_body=extra_body,
277
+ input_length=input_length,
278
+ )
279
+ for prompt, output_length, input_length in zip(
280
+ prompts, output_lengths, input_lengths, strict=True
281
+ )
282
+ ]
283
+ with pbar.log_stats():
284
+ return await asyncio.gather(*tasks)
285
+
286
+
287
+ async def benchmark_trace(
288
+ client: OpenAI,
289
+ msgs: List[BenchmarkTrace],
290
+ model: str,
291
+ *,
292
+ pbar: Console | bool = True,
293
+ ) -> List[RawResult]:
294
+ if isinstance(pbar, bool):
295
+ sum_output_len = sum(msg.output_length for msg in msgs)
296
+ pbar = make_console(len(msgs), sum_output_len, use_pbar=pbar)
297
+ start = time.perf_counter()
298
+ offset = min(msg.timestamp for msg in msgs) - 1
299
+
300
+ async def benchmark_timed(msg: BenchmarkTrace):
301
+ target = start + msg.timestamp - offset
302
+ await asyncio.sleep(max(0, target - time.perf_counter()))
303
+ return await benchmark_one(
304
+ client, msg.message, msg.output_length, model, pbar=pbar, input_length=msg.input_length
305
+ )
306
+
307
+ tasks = [benchmark_timed(msg) for msg in msgs]
308
+ with pbar.log_stats():
309
+ return await asyncio.gather(*tasks)
310
+
311
+
312
+ @overload
313
+ def process_benchmark_results(raw_data: List[RawResult], tokenizer: Any) -> BenchmarkResult: ...
314
+
315
+
316
+ @overload
317
+ def process_benchmark_results(raw_data: List[RawResult]) -> None: ...
318
+
319
+
320
+ def process_benchmark_results(
321
+ raw_data: List[RawResult],
322
+ tokenizer: Any = UNSET,
323
+ ) -> BenchmarkResult | None:
324
+ accum_times: List[float] = []
325
+ first_times: List[float] = []
326
+ results = [r.tics for r in raw_data]
327
+ for tics in results:
328
+ deltas: List[float] = []
329
+ for i in range(len(tics) - 1):
330
+ diff = tics[i + 1] - tics[i]
331
+ deltas.append(diff)
332
+ first_times.append(deltas[0])
333
+ accum_times.extend(deltas[1:])
334
+
335
+ e2e_times = [tics[-1] - tics[0] for tics in results]
336
+ first_times.sort()
337
+ accum_times.sort()
338
+ e2e_times.sort()
339
+
340
+ def _print_stats(times: List[float], scale: float = 1.0) -> Tuple[float, ...]:
341
+ assert len(times) > 0
342
+ return (
343
+ scale * sum(times) / len(times), # avg
344
+ scale * times[int(len(times) * 0.5)], # p50
345
+ scale * times[int(len(times) * 0.9)], # p90
346
+ scale * times[int(len(times) * 0.99)], # p99
347
+ scale * max(times), # max
348
+ )
349
+
350
+ def _fmt(x: float) -> str:
351
+ if x >= 1000:
352
+ return f"{int(x):>6}"
353
+ elif x >= 10:
354
+ return f"{x:>6.2f}"
355
+ else:
356
+ return f"{x:>6.4f}"
357
+
358
+ avg_ttft, p50_ttft, p90_ttft, p99_ttft, max_ttft = _print_stats(first_times, 1000)
359
+ avg_tpot, p50_tpot, p90_tpot, p99_tpot, max_tpot = _print_stats(accum_times, 1000)
360
+ avg_e2e, p50_e2e, p90_e2e, p99_e2e, max_e2e = _print_stats(e2e_times)
361
+
362
+ min_time = min(min(r) for r in results)
363
+ max_time = max(max(r) for r in results)
364
+ dur = max_time - min_time
365
+ assert dur > 0, "Duration must be positive"
366
+
367
+ num_tokens = sum(len(tic) for tic in results)
368
+ num_requests = len(results)
369
+
370
+ logger.info(f"Num requests: #{num_requests}, Num tokens: #{num_tokens}")
371
+ logger.info(
372
+ f"TTFT: {_fmt(avg_ttft)} ms (p50: {_fmt(p50_ttft)} ms, p90: {_fmt(p90_ttft)} ms,"
373
+ f" p99: {_fmt(p99_ttft)} ms, max: {_fmt(max_ttft)} ms)"
374
+ )
375
+ logger.info(
376
+ f"TPOT: {_fmt(avg_tpot)} ms (p50: {_fmt(p50_tpot)} ms, p90: {_fmt(p90_tpot)} ms,"
377
+ f" p99: {_fmt(p99_tpot)} ms, max: {_fmt(max_tpot)} ms)"
378
+ )
379
+ logger.info(
380
+ f"E2E: {_fmt(avg_e2e) } s (p50: {_fmt(p50_e2e) } s, p90: {_fmt(p90_e2e) } s,"
381
+ f" p99: {_fmt(p99_e2e) } s, max: {_fmt(max_e2e) } s)"
382
+ )
383
+ logger.info(f"Duration: {_fmt(dur)} s")
384
+ logger.info(f"Throughput: {_fmt(num_tokens / dur)} token/s, {_fmt(num_requests / dur)} req/s")
385
+
386
+ # normalize the time to start from zero
387
+ results = [[r - min_time for r in tics] for tics in results]
388
+ if isinstance(tokenizer, Unset):
389
+ return None
390
+
391
+ return BenchmarkResult(
392
+ raw_data=[
393
+ BenchOneResult(
394
+ tics=r.tics,
395
+ input_len=(
396
+ r.input_len
397
+ if r.input_len is not None
398
+ else len(tokenizer.encode(r.message, add_special_tokens=False))
399
+ ),
400
+ output_len=r.output_len,
401
+ )
402
+ for r in raw_data
403
+ ]
404
+ )
405
+
406
+
407
+ def read_qwen_trace(
408
+ file_path: str,
409
+ tokenizer: Any,
410
+ n: int | None = None,
411
+ dummy: bool = False,
412
+ ) -> List[BenchmarkTrace]:
413
+ class JSONInput(BaseModel):
414
+ chat_id: int
415
+ parent_chat_id: int
416
+ timestamp: float
417
+ input_length: int
418
+ output_length: int
419
+ type: str # unused
420
+ turn: int # unused
421
+ hash_ids: List[int] # unused
422
+
423
+ with open(file_path, "r") as f:
424
+ lines = f.readlines()
425
+ if n is not None:
426
+ lines = lines[:n]
427
+ objs = [JSONInput.model_validate_json(line) for line in lines]
428
+ if dummy:
429
+ prompt = generate_prompt(tokenizer, max(obj.input_length for obj in objs))
430
+ ids = tokenizer.encode(prompt, add_special_tokens=False)
431
+ _get_prompt = lambda obj: tokenizer.decode(ids[: obj.input_length])
432
+ else:
433
+ _get_prompt = lambda obj: generate_prompt(tokenizer, obj.input_length)
434
+ return [
435
+ BenchmarkTrace(
436
+ timestamp=obj.timestamp,
437
+ message=_get_prompt(obj),
438
+ input_length=obj.input_length,
439
+ output_length=obj.output_length,
440
+ )
441
+ for obj in objs
442
+ ]
443
+
444
+
445
+ def read_mooncake_trace(
446
+ file_path: str,
447
+ tokenizer: Any,
448
+ n: int | None = None,
449
+ dummy: bool = False,
450
+ ) -> List[BenchmarkTrace]:
451
+ class JSONInput(BaseModel):
452
+ timestamp: int
453
+ input_length: int
454
+ output_length: int
455
+ hash_ids: List[int] # unused for now
456
+
457
+ with open(file_path, "r") as f:
458
+ lines = f.readlines()
459
+ if n is not None:
460
+ lines = lines[:n]
461
+ objs = [JSONInput.model_validate_json(line) for line in lines]
462
+ if dummy:
463
+ prompt = generate_prompt(tokenizer, max(obj.input_length for obj in objs))
464
+ ids = tokenizer.encode(prompt, add_special_tokens=False)
465
+ _get_prompt = lambda obj: tokenizer.decode(ids[: obj.input_length])
466
+ else:
467
+ _get_prompt = lambda obj: generate_prompt(tokenizer, obj.input_length)
468
+ return [
469
+ BenchmarkTrace(
470
+ timestamp=obj.timestamp / 1000,
471
+ message=_get_prompt(obj),
472
+ input_length=obj.input_length,
473
+ output_length=obj.output_length,
474
+ )
475
+ for obj in objs
476
+ ]
477
+
478
+
479
+ def scale_traces(
480
+ traces: List[BenchmarkTrace],
481
+ scale: float,
482
+ ) -> List[BenchmarkTrace]:
483
+ min_tic = min(trace.timestamp for trace in traces)
484
+ return sorted(
485
+ [
486
+ BenchmarkTrace(
487
+ timestamp=(trace.timestamp - min_tic) * scale,
488
+ message=trace.message,
489
+ input_length=trace.input_length,
490
+ output_length=trace.output_length,
491
+ )
492
+ for trace in traces
493
+ ],
494
+ key=lambda x: x.timestamp,
495
+ )
496
+
497
+
498
+ async def get_model_name(client: OpenAI) -> str:
499
+ async for model in client.models.list():
500
+ return model.id
501
+ raise ValueError("No models available")
zonos2/benchmark/perf.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Dict, Tuple
4
+
5
+ from zonos2.utils import init_logger
6
+
7
+ logger = init_logger(__name__)
8
+
9
+
10
+ def perf_cuda(
11
+ f: Callable[[], Any],
12
+ *,
13
+ init_stream: bool = True,
14
+ repetitions: int = 10,
15
+ cuda_graph_repetitions: int | None = 10,
16
+ ) -> float:
17
+ import torch
18
+
19
+ assert repetitions > 0
20
+ tic = torch.cuda.Event(enable_timing=True)
21
+ toc = torch.cuda.Event(enable_timing=True)
22
+ stream = torch.cuda.Stream()
23
+ torch.cuda.synchronize()
24
+ if init_stream:
25
+ stream = torch.cuda.Stream()
26
+ else:
27
+ stream = torch.cuda.current_stream()
28
+
29
+ with torch.cuda.stream(stream):
30
+ f()
31
+ if N := cuda_graph_repetitions:
32
+ g = torch.cuda.CUDAGraph()
33
+ with torch.cuda.graph(g):
34
+ for _ in range(N):
35
+ f()
36
+ replay = g.replay
37
+ del g
38
+ else:
39
+ replay = f
40
+ N = 1
41
+
42
+ torch.cuda.synchronize()
43
+
44
+ replay()
45
+ tic.record()
46
+ for _ in range(repetitions):
47
+ replay()
48
+ toc.record()
49
+ toc.synchronize()
50
+ dur = tic.elapsed_time(toc)
51
+ return dur / (N * repetitions)
52
+
53
+
54
+ def compare_memory_kernel_perf(
55
+ *,
56
+ baseline: Callable[[], Any],
57
+ our_impl: Callable[[], Any],
58
+ memory_footprint: int, # in bytes
59
+ description: str = " ",
60
+ extra_kwargs: Dict[str, Any] | None = None,
61
+ need_latency: bool = True,
62
+ ) -> Tuple[float, float]:
63
+ extra_kwargs = extra_kwargs or {}
64
+
65
+ dur = perf_cuda(baseline, **extra_kwargs)
66
+ bandwidth_0 = memory_footprint / (dur * 1e6) # GB/s
67
+ latency_msg = f"{dur:8.3f} ms | " if need_latency else ""
68
+ message_0 = f"Baseline: {latency_msg}{bandwidth_0:8.3f} GB/s"
69
+
70
+ dur = perf_cuda(our_impl, **extra_kwargs)
71
+ bandwidth_1 = memory_footprint / (dur * 1e6) # GB/s
72
+ latency_msg = f"{dur:8.3f} ms | " if need_latency else ""
73
+ logger.info(f"{description}{message_0} | Our Impl: {latency_msg}{bandwidth_1:8.3f} GB/s")
74
+ return bandwidth_0, bandwidth_1
zonos2/core.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager
4
+ from dataclasses import dataclass, field
5
+ from typing import TYPE_CHECKING, List, Literal
6
+
7
+ import torch
8
+
9
+ if TYPE_CHECKING:
10
+ from zonos2.attention import BaseAttnBackend, BaseAttnMetadata
11
+ from zonos2.kvcache import BaseCacheHandle
12
+
13
+
14
+ @dataclass
15
+ class Context:
16
+ page_size: int
17
+ attn_backend: BaseAttnBackend
18
+ _batch: TTSBatch | None = field(default=None, init=False)
19
+
20
+ @property
21
+ def batch(self) -> TTSBatch:
22
+ assert self._batch is not None, "No active batch in context"
23
+ return self._batch
24
+
25
+ @contextmanager
26
+ def forward_batch(self, batch: TTSBatch):
27
+ assert self._batch is None, "Nested forward_batch is not allowed"
28
+ try:
29
+ self._batch = batch
30
+ yield
31
+ finally:
32
+ self._batch = None
33
+
34
+
35
+ _GLOBAL_CTX: Context | None = None
36
+
37
+
38
+ def set_global_ctx(ctx: Context):
39
+ global _GLOBAL_CTX
40
+ assert _GLOBAL_CTX is None, "Global context is already set"
41
+ _GLOBAL_CTX = ctx
42
+
43
+
44
+ def get_global_ctx() -> Context:
45
+ assert _GLOBAL_CTX is not None, "Global context is not set"
46
+ return _GLOBAL_CTX
47
+
48
+
49
+ # =============================================================================
50
+ # TTS-specific data structures
51
+ # =============================================================================
52
+
53
+
54
+ @dataclass
55
+ class TTSSamplingParams:
56
+ """Sampling parameters for TTS generation."""
57
+
58
+ temperature: float = 1.15
59
+ topk: int = 106
60
+ top_p: float = 0.0
61
+ min_p: float = 0.18
62
+ max_tokens: int = 1024
63
+ ignore_eos: bool = False
64
+ repetition_window: int = 50
65
+ repetition_penalty: float = 1.2
66
+ repetition_codebooks: int = 8
67
+ seed: int | None = None
68
+
69
+
70
+ @dataclass(eq=False)
71
+ class TTSReq:
72
+ """Request class for TTS generation with 2D token format.
73
+
74
+ Tokens are in unpacked format: [cb0, cb1, ..., cb8, text_token] per frame.
75
+ """
76
+
77
+ input_ids: torch.Tensor # 2D CPU tensor (seq_len, frame_width)
78
+ table_idx: int
79
+ cached_len: int
80
+ output_len: int
81
+ uid: int
82
+ sampling_params: TTSSamplingParams
83
+ cache_handle: BaseCacheHandle
84
+ n_codebooks: int = 9
85
+ eoa_id: int = 1024
86
+ eos_frame: int = -1 # Aligned frame where EOS first appeared (-1 = not seen)
87
+ eos_countdown: int = -1 # Steps remaining after EOS (-1 = not in countdown)
88
+ total_generated: int = 0 # Total frames generated (for logging)
89
+ rng: torch.Generator | None = None # Per-request RNG for deterministic sampling
90
+ speaker_embedding: torch.Tensor | None = None # 1D CPU float32 tensor
91
+ speaker_token_position: int = -1 # Injection position within the prompt sequence
92
+
93
+ def __post_init__(self) -> None:
94
+ assert self.input_ids.is_cpu
95
+ assert self.input_ids.dim() == 2, "TTS input_ids must be 2D (seq_len, frame_width)"
96
+ self.device_len = len(self.input_ids)
97
+ self.max_device_len = len(self.input_ids) + self.output_len
98
+ assert 0 <= self.cached_len < self.device_len <= self.max_device_len
99
+
100
+ if self.speaker_embedding is not None:
101
+ emb = self.speaker_embedding
102
+ if emb.dim() == 2 and emb.shape[0] == 1:
103
+ emb = emb.squeeze(0)
104
+ if emb.dim() != 1:
105
+ raise ValueError(
106
+ f"speaker_embedding must be 1D or (1, D), got shape {tuple(emb.shape)}"
107
+ )
108
+ self.speaker_embedding = emb.to(dtype=torch.float32, device="cpu")
109
+
110
+ if self.speaker_token_position < 0:
111
+ # Training convention: reserved speaker slot is at prompt position 0.
112
+ self.speaker_token_position = 0
113
+ if self.speaker_token_position >= self.device_len:
114
+ self.speaker_token_position = 0
115
+
116
+ @property
117
+ def frame_width(self) -> int:
118
+ """Number of elements per frame (n_codebooks + extras)."""
119
+ return self.input_ids.shape[-1]
120
+
121
+ @property
122
+ def remain_len(self) -> int:
123
+ return self.max_device_len - self.device_len
124
+
125
+ @property
126
+ def extend_len(self) -> int:
127
+ return self.device_len - self.cached_len
128
+
129
+ @property
130
+ def num_completion_tokens(self) -> int:
131
+ """Number of generated tokens (frames)."""
132
+ return self.device_len - self.cached_len
133
+
134
+ def complete_one(self) -> None:
135
+ self.cached_len = self.device_len
136
+ self.device_len += 1
137
+ self.total_generated += 1
138
+
139
+ def append_host(self, next_token: torch.Tensor) -> None:
140
+ """Append a single frame (unpacked token) to input_ids."""
141
+ assert next_token.dim() == 1, "next_token must be 1D (frame_width,)"
142
+ self.input_ids = torch.cat([self.input_ids, next_token.unsqueeze(0)], dim=0)
143
+
144
+ def can_decode(self) -> bool:
145
+ return self.remain_len > 0 and self.eos_countdown != 0
146
+
147
+ def check_eos(self, audio_codes: List[int]) -> bool:
148
+ """Check for EOS and update countdown state.
149
+
150
+ Args:
151
+ audio_codes: List of audio codebook values for one frame
152
+
153
+ Returns:
154
+ True if sequence is finished (countdown reached 0)
155
+ """
156
+ if self.sampling_params.ignore_eos:
157
+ return False
158
+
159
+ # Match Zonos2 reference inference: any sampled EOA codebook starts the
160
+ # delayed stop countdown. The aligned frame is shifted back by the
161
+ # highest EOA codebook index and clamped at zero.
162
+ # Use total_generated because this request only sees one decode frame at a time.
163
+ if self.eos_frame < 0:
164
+ step = self.total_generated - 1
165
+ eos_cols = [c == self.eoa_id for c in audio_codes[: self.n_codebooks]]
166
+ if any(eos_cols):
167
+ # First EOS: compute aligned frame
168
+ max_eos_cb = max(i for i, is_eos in enumerate(eos_cols) if is_eos)
169
+ self.eos_frame = max(0, step - max_eos_cb)
170
+ self.eos_countdown = self.n_codebooks + 1
171
+
172
+ # Decrement countdown
173
+ if self.eos_countdown > 0:
174
+ self.eos_countdown -= 1
175
+ if self.eos_countdown == 0:
176
+ return True
177
+
178
+ return False
179
+
180
+ def __repr__(self) -> str:
181
+ return (
182
+ f"{type(self).__name__}(table_idx={self.table_idx}, "
183
+ f"cached_len={self.cached_len}, device_len={self.device_len}, "
184
+ f"max_device_len={self.max_device_len}, eos_frame={self.eos_frame})"
185
+ )
186
+
187
+
188
+ @dataclass
189
+ class TTSBatch:
190
+ """Batch of TTS requests with 2D token format."""
191
+
192
+ reqs: List[TTSReq]
193
+ phase: Literal["prefill", "decode"]
194
+ # these fields should be set by scheduler
195
+ input_ids: torch.Tensor = field(init=False) # (total_tokens, frame_width)
196
+ out_loc: torch.Tensor = field(init=False)
197
+ padded_reqs: List[TTSReq] = field(init=False)
198
+ # this field should be set by attention backend
199
+ attn_metadata: BaseAttnMetadata = field(init=False)
200
+ # Optional per-batch speaker conditioning data (set by TTS scheduler).
201
+ speaker_emb_values: torch.Tensor | None = field(default=None, init=False)
202
+ speaker_token_positions: torch.Tensor | None = field(default=None, init=False)
203
+
204
+ @property
205
+ def is_prefill(self) -> bool:
206
+ return self.phase == "prefill"
207
+
208
+ @property
209
+ def is_decode(self) -> bool:
210
+ return self.phase == "decode"
211
+
212
+ @property
213
+ def size(self) -> int:
214
+ return len(self.reqs)
215
+
216
+ @property
217
+ def padded_size(self) -> int:
218
+ return len(self.padded_reqs)
zonos2/distributed/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .impl import DistributedCommunicator, destroy_distributed, enable_pynccl_distributed
2
+ from .info import DistributedInfo, get_tp_info, set_tp_info, try_get_tp_info
3
+
4
+ __all__ = [
5
+ "DistributedInfo",
6
+ "get_tp_info",
7
+ "set_tp_info",
8
+ "enable_pynccl_distributed",
9
+ "DistributedCommunicator",
10
+ "try_get_tp_info",
11
+ "destroy_distributed",
12
+ ]
zonos2/distributed/impl.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, List
6
+
7
+ import torch
8
+ import torch.distributed as dist
9
+
10
+ if TYPE_CHECKING:
11
+ from zonos2.distributed import DistributedInfo
12
+ from zonos2.kernel import PyNCCLCommunicator
13
+
14
+
15
+ @dataclass
16
+ class DistributedImpl(ABC):
17
+ @abstractmethod
18
+ def all_reduce(self, x: torch.Tensor) -> torch.Tensor: ...
19
+
20
+ @abstractmethod
21
+ def all_gather(self, x: torch.Tensor) -> torch.Tensor: ...
22
+
23
+
24
+ @dataclass
25
+ class TorchDistributedImpl(DistributedImpl):
26
+ def all_reduce(self, x: torch.Tensor) -> torch.Tensor:
27
+ tp_size = dist.get_world_size()
28
+ if tp_size == 1:
29
+ return x
30
+ dist.all_reduce(x, op=dist.ReduceOp.SUM)
31
+ return x
32
+
33
+ def all_gather(self, x: torch.Tensor) -> torch.Tensor:
34
+ tp_size = dist.get_world_size()
35
+ if tp_size == 1:
36
+ return x
37
+ shape = list(x.shape)
38
+ shape[0] = shape[0] * tp_size
39
+ out = torch.empty(shape, dtype=x.dtype, device=x.device)
40
+ dist.all_gather_into_tensor(out, x)
41
+ return out
42
+
43
+
44
+ @dataclass
45
+ class PyNCCLDistributedImpl(DistributedImpl):
46
+ comm: PyNCCLCommunicator
47
+
48
+ def all_reduce(self, x: torch.Tensor) -> torch.Tensor:
49
+ self.comm.all_reduce(x, "sum")
50
+ return x
51
+
52
+ def all_gather(self, x: torch.Tensor) -> torch.Tensor:
53
+ from .info import get_tp_info
54
+
55
+ world_size = get_tp_info().size
56
+ output_shape = list(x.shape)
57
+ output_shape[0] *= world_size
58
+ result = x.new_empty(output_shape)
59
+ self.comm.all_gather(result, x)
60
+ return result
61
+
62
+
63
+ class DistributedCommunicator:
64
+ plugins: List[DistributedImpl] = [TorchDistributedImpl()]
65
+
66
+ def all_reduce(self, x: torch.Tensor) -> torch.Tensor:
67
+ return self.plugins[-1].all_reduce(x)
68
+
69
+ def all_gather(self, x: torch.Tensor) -> torch.Tensor:
70
+ return self.plugins[-1].all_gather(x)
71
+
72
+
73
+ def enable_pynccl_distributed(
74
+ tp_info: DistributedInfo, tp_cpu_group: torch.distributed.ProcessGroup, max_bytes: int
75
+ ) -> None:
76
+ """
77
+ Enable PyNCCL-based distributed communication for tensor parallelism.
78
+ """
79
+ if tp_info.size == 1:
80
+ return
81
+ from zonos2.kernel import init_pynccl
82
+
83
+ comm = init_pynccl(
84
+ tp_rank=tp_info.rank,
85
+ tp_size=tp_info.size,
86
+ tp_cpu_group=tp_cpu_group,
87
+ max_size_bytes=max_bytes,
88
+ )
89
+
90
+ DistributedCommunicator.plugins.append(PyNCCLDistributedImpl(comm))
91
+
92
+
93
+ def destroy_distributed() -> None:
94
+ """
95
+ Destroy all the distributed communication plugins.
96
+ """
97
+ DistributedCommunicator.plugins = []
zonos2/distributed/info.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class DistributedInfo: # should not export from here
8
+ rank: int
9
+ size: int
10
+
11
+ def __post_init__(self):
12
+ assert 0 <= self.rank < self.size
13
+
14
+ def is_primary(self) -> bool:
15
+ return self.rank == 0
16
+
17
+
18
+ _TP_INFO: DistributedInfo | None = None
19
+
20
+
21
+ def set_tp_info(rank: int, size: int) -> None:
22
+ global _TP_INFO
23
+ if _TP_INFO is not None:
24
+ raise RuntimeError("TP info has been set")
25
+ _TP_INFO = DistributedInfo(rank, size)
26
+
27
+
28
+ def get_tp_info() -> DistributedInfo:
29
+ if _TP_INFO is None:
30
+ raise RuntimeError("TP info has not been set")
31
+ return _TP_INFO
32
+
33
+
34
+ def try_get_tp_info() -> DistributedInfo | None:
35
+ return _TP_INFO
36
+
37
+
38
+ __all__ = ["DistributedInfo", "set_tp_info", "get_tp_info", "try_get_tp_info"]
zonos2/engine/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from .config import EngineConfig
2
+ from .engine import Engine
3
+
4
+ __all__ = ["Engine", "EngineConfig"]
zonos2/engine/config.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from functools import cached_property
5
+ from typing import TYPE_CHECKING, List
6
+
7
+ import torch
8
+ from zonos2.distributed import DistributedInfo
9
+ from zonos2.utils import cached_load_checkpoint_config
10
+
11
+ if TYPE_CHECKING:
12
+ from zonos2.models import ModelConfig
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class EngineConfig:
17
+ model_path: str
18
+ tp_info: DistributedInfo
19
+ dtype: torch.dtype
20
+ max_running_req: int = 256
21
+ attention_backend: str = "auto"
22
+ moe_backend: str = "fused_moe"
23
+ cuda_graph_bs: List[int] | None = None
24
+ cuda_graph_max_bs: int | None = None
25
+ page_size: int = 1
26
+ memory_ratio: float = 0.9
27
+ distributed_timeout: float = 60.0
28
+ use_dummy_weight: bool = False
29
+ use_pynccl: bool = True
30
+ max_seq_len_override: int | None = None
31
+ num_page_override: int | None = None # if not None, will override the number of pages
32
+
33
+ @cached_property
34
+ def checkpoint_config(self):
35
+ return cached_load_checkpoint_config(self.model_path)
36
+
37
+ @cached_property
38
+ def model_config(self) -> ModelConfig:
39
+ from zonos2.models import ModelConfig
40
+
41
+ return ModelConfig.from_checkpoint_config(self.checkpoint_config)
42
+
43
+ @property
44
+ def max_seq_len(self) -> int:
45
+ if self.max_seq_len_override is not None:
46
+ return self.max_seq_len_override
47
+ return self.model_config.rotary_config.max_position
48
+
49
+ @property
50
+ def max_forward_len(self) -> int:
51
+ return self.max_seq_len
52
+
53
+ @property
54
+ def distributed_addr(self) -> str:
55
+ return "tcp://127.0.0.1:23333"
zonos2/engine/engine.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from datetime import timedelta
5
+ from typing import Dict, Tuple
6
+
7
+ import torch
8
+ from zonos2.attention import create_attention_backend
9
+ from zonos2.core import (
10
+ Context,
11
+ TTSBatch,
12
+ TTSReq,
13
+ TTSSamplingParams,
14
+ set_global_ctx,
15
+ )
16
+ from zonos2.distributed import destroy_distributed, enable_pynccl_distributed, set_tp_info
17
+ from zonos2.kvcache import create_kvcache
18
+ from zonos2.layers import set_rope_device
19
+ from zonos2.models import create_model, load_checkpoint_weight
20
+ from zonos2.utils import divide_even, init_logger, torch_dtype
21
+
22
+ from .config import EngineConfig
23
+ from .graph import GraphRunner, get_free_memory, mem_GB
24
+
25
+ logger = init_logger(__name__)
26
+
27
+
28
+ def create_page_table(shape: Tuple[int, int], device: torch.device) -> torch.Tensor:
29
+ return torch.zeros(shape, dtype=torch.int32, device=device)
30
+
31
+
32
+ def _align_up_32(num: int) -> int:
33
+ return (num + 31) // 32 * 32
34
+
35
+
36
+ def _get_frame_width(model_config) -> int:
37
+ """Get frame width for multi-codebook models.
38
+
39
+ Frame width = n_codebooks + (1 if text_vocab).
40
+ """
41
+ width = model_config.n_codebooks
42
+ if model_config.text_vocab is not None:
43
+ width += 1
44
+ return width
45
+
46
+
47
+ class Engine:
48
+ def __init__(self, config: EngineConfig):
49
+ self.model_config = config.model_config
50
+ set_tp_info(rank=config.tp_info.rank, size=config.tp_info.size)
51
+
52
+ self.device = torch.device("cuda")
53
+ # Importing the model eagerly pulls in sgl_kernel, which creates a CUDA
54
+ # context on the current device. The scheduler entry point binds this
55
+ # process to its rank's GPU before that import, so an existing context is
56
+ # expected; only one on the wrong device indicates a real ordering bug.
57
+ if config.tp_info.size > 1 and (
58
+ torch.cuda.is_initialized()
59
+ and torch.cuda.current_device() != config.tp_info.rank
60
+ ):
61
+ raise RuntimeError(
62
+ f"CUDA was initialized on cuda:{torch.cuda.current_device()} before "
63
+ f"the engine could bind TP rank {config.tp_info.rank}; "
64
+ "set the device before importing model code."
65
+ )
66
+ if config.tp_info.size > 1:
67
+ torch.cuda.set_device(config.tp_info.rank)
68
+ self.stream = torch.cuda.Stream()
69
+ torch.cuda.set_stream(self.stream)
70
+ self.dtype = config.dtype
71
+
72
+ self.tp_cpu_group = self._init_communication(config)
73
+ init_free_memory = self._sync_get_memory()[1]
74
+ logger.info_rank0(f"Free memory before loading model: {mem_GB(init_free_memory)}")
75
+
76
+ # load model and determine number of pages
77
+ set_rope_device(self.device)
78
+ with torch.device("meta"), torch_dtype(config.dtype):
79
+ self.model = create_model(config)
80
+
81
+ # Log expected shapes before loading (for debugging dimension mismatches)
82
+ logger.info_rank0(f"Model config: num_kv_heads={config.model_config.num_kv_heads}, head_dim={config.model_config.head_dim}")
83
+ expected_kv_dim = config.model_config.num_kv_heads * config.model_config.head_dim
84
+ logger.info_rank0(f"Expected kv_dim per TP rank: {expected_kv_dim}")
85
+
86
+ state_dict = self._load_weight_state_dict(config)
87
+ self._check_speaker_lda_weights(state_dict)
88
+
89
+ # Debug: Check for temp keys in state_dict
90
+ if logger.isEnabledFor(logging.DEBUG):
91
+ temp_keys = [k for k in state_dict.keys() if '.temp' in k]
92
+ logger.debug("Found %d temp keys in state_dict: %s", len(temp_keys), temp_keys[:5])
93
+ if temp_keys:
94
+ first_temp = state_dict[temp_keys[0]]
95
+ logger.debug(
96
+ "First temp tensor: shape=%s, dtype=%s, values=%s",
97
+ first_temp.shape, first_temp.dtype, first_temp.flatten()[:4].tolist(),
98
+ )
99
+
100
+ # Validate wkv weight shapes match config (catch n_kv_heads mismatch early)
101
+ for key, tensor in state_dict.items():
102
+ if ".wkv." in key and "weight" in key:
103
+ # wkv.weight should be (2, kv_dim, hidden_size) or (2*kv_dim, hidden_size)
104
+ if tensor.dim() == 3:
105
+ actual_kv_dim = tensor.shape[1]
106
+ else:
107
+ actual_kv_dim = tensor.shape[0] // 2
108
+ if actual_kv_dim != expected_kv_dim:
109
+ inferred_kv_heads = actual_kv_dim // config.model_config.head_dim
110
+ raise ValueError(
111
+ f"KV dimension mismatch for {key}: checkpoint has kv_dim={actual_kv_dim} "
112
+ f"(implying n_kv_heads={inferred_kv_heads}), but config expects "
113
+ f"kv_dim={expected_kv_dim} (n_kv_heads={config.model_config.num_kv_heads}). "
114
+ f"Please ensure your checkpoint's params.json has the correct n_kv_heads value "
115
+ f"matching the trained model weights."
116
+ )
117
+ break # Only need to check one layer
118
+
119
+ self.model.load_state_dict(state_dict)
120
+
121
+ # Debug: print attention temp values to verify they loaded correctly
122
+ if logger.isEnabledFor(logging.DEBUG):
123
+ logger.debug("Model type: %s", type(self.model).__name__)
124
+ logger.debug("Model has layers: %s", hasattr(self.model, 'layers'))
125
+ if hasattr(self.model, 'layers'):
126
+ logger.debug("layers type: %s", type(self.model.layers))
127
+ if hasattr(self.model.layers, 'op_list'):
128
+ logger.debug("num layers: %d", len(self.model.layers.op_list))
129
+ for i, layer in enumerate(self.model.layers.op_list[:2]):
130
+ logger.debug("Layer %d type: %s", i, type(layer).__name__)
131
+ if hasattr(layer, 'attention'):
132
+ attn = layer.attention
133
+ logger.debug("Layer %d attention type: %s", i, type(attn).__name__)
134
+ logger.debug("Layer %d has_qk_norm: %s", i, getattr(attn, 'has_qk_norm', 'N/A'))
135
+ if hasattr(attn, 'temp'):
136
+ temp = attn.temp
137
+ if temp is not None:
138
+ logger.debug("Layer %d attention.temp: shape=%s, values=%s", i, temp.shape, temp.flatten()[:8].tolist())
139
+ else:
140
+ logger.debug("Layer %d attention.temp is None", i)
141
+
142
+ self.num_pages = self.dummy_page = self._determine_num_pages(init_free_memory, config)
143
+ self.kv_cache = create_kvcache(
144
+ model_config=config.model_config,
145
+ num_pages=self.num_pages + 1, # +1 for dummy page
146
+ device=self.device,
147
+ dtype=self.dtype,
148
+ )
149
+ # NOTE: make page table 128 aligned (32 * sizeof(int32) == 128 bytes)
150
+ self.max_seq_len = _align_up_32(min(config.max_seq_len, self.num_pages))
151
+ self.page_table = create_page_table( # + 1 for dummy request
152
+ (config.max_running_req + 1, self.max_seq_len),
153
+ device=self.device,
154
+ )
155
+ self.attn_backend = create_attention_backend(
156
+ config.attention_backend,
157
+ config.model_config,
158
+ self.kv_cache,
159
+ self.page_table,
160
+ )
161
+ self.ctx = Context(page_size=1, attn_backend=self.attn_backend)
162
+ set_global_ctx(self.ctx)
163
+
164
+ post_free_memory = self._sync_get_memory()[0]
165
+ logger.info_rank0(f"Free memory after initialization: {mem_GB(post_free_memory)}")
166
+
167
+ frame_width = _get_frame_width(self.model_config)
168
+ self.dummy_req = TTSReq(
169
+ input_ids=torch.zeros((1, frame_width), dtype=torch.int32, device="cpu"),
170
+ table_idx=config.max_running_req,
171
+ cached_len=0,
172
+ output_len=1,
173
+ uid=-1,
174
+ sampling_params=TTSSamplingParams(),
175
+ cache_handle=None, # type: ignore
176
+ n_codebooks=self.model_config.n_codebooks,
177
+ eoa_id=self.model_config.eoa_id,
178
+ )
179
+ self.page_table[self.dummy_req.table_idx].fill_(self.dummy_page)
180
+
181
+ self.graph_runner = GraphRunner(
182
+ stream=self.stream,
183
+ device=self.device,
184
+ model=self.model,
185
+ attn_backend=self.attn_backend,
186
+ cuda_graph_bs=config.cuda_graph_bs,
187
+ cuda_graph_max_bs=config.cuda_graph_max_bs,
188
+ free_memory=init_free_memory,
189
+ max_seq_len=self.max_seq_len,
190
+ vocab_size=self.model_config.codebook_size + 2,
191
+ dummy_req=self.dummy_req,
192
+ )
193
+
194
+ def _init_communication(self, config: EngineConfig) -> torch.distributed.ProcessGroup:
195
+ if config.tp_info.size == 1 or config.use_pynccl:
196
+ torch.distributed.init_process_group(
197
+ backend="gloo",
198
+ rank=config.tp_info.rank,
199
+ world_size=config.tp_info.size,
200
+ timeout=timedelta(seconds=config.distributed_timeout),
201
+ init_method=config.distributed_addr,
202
+ )
203
+ tp_cpu_group = torch.distributed.group.WORLD
204
+ assert tp_cpu_group is not None
205
+ max_bytes = (
206
+ config.max_forward_len * config.model_config.hidden_size * self.dtype.itemsize
207
+ )
208
+ enable_pynccl_distributed(config.tp_info, tp_cpu_group, max_bytes)
209
+ else:
210
+ torch.distributed.init_process_group(
211
+ backend="nccl",
212
+ rank=config.tp_info.rank,
213
+ world_size=config.tp_info.size,
214
+ timeout=timedelta(seconds=config.distributed_timeout),
215
+ init_method=config.distributed_addr,
216
+ )
217
+ tp_cpu_group = torch.distributed.new_group(backend="gloo")
218
+ assert tp_cpu_group is not None
219
+ return tp_cpu_group
220
+
221
+ def _load_weight_state_dict(self, config: EngineConfig) -> Dict[str, torch.Tensor]:
222
+ if config.use_dummy_weight:
223
+ return {
224
+ k: torch.randn_like(v, device=self.device)
225
+ for k, v in self.model.state_dict().items()
226
+ }
227
+ else:
228
+ return {
229
+ k: v.to(self.dtype)
230
+ for k, v in load_checkpoint_weight(config.model_path, self.device).items()
231
+ }
232
+
233
+ def _check_speaker_lda_weights(self, state_dict: Dict[str, torch.Tensor]) -> None:
234
+ lda_keys = [key for key in state_dict if key.startswith("speaker_lda_projection.")]
235
+ if getattr(self.model, "speaker_lda_projection", None) is None:
236
+ for key in lda_keys:
237
+ state_dict.pop(key)
238
+ if lda_keys:
239
+ logger.warning_rank0(
240
+ "Dropped %d speaker_lda_projection weights because speaker LDA is not enabled.",
241
+ len(lda_keys),
242
+ )
243
+ return
244
+
245
+ missing = {
246
+ "speaker_lda_projection.weight",
247
+ "speaker_lda_projection.bias",
248
+ }.difference(lda_keys)
249
+ if missing:
250
+ raise ValueError(
251
+ "speaker_lda_dim is configured but the checkpoint is missing "
252
+ f"{sorted(missing)}. Use a checkpoint with the LDA projection merged in."
253
+ )
254
+
255
+ def _determine_num_pages(self, old_free_memory: int, config: EngineConfig) -> int:
256
+ new_free_memory = self._sync_get_memory()[1]
257
+ cache_per_page = (
258
+ 2 # key + value
259
+ * self.model_config.head_dim
260
+ * divide_even(self.model_config.num_kv_heads, config.tp_info.size)
261
+ * config.page_size
262
+ * self.dtype.itemsize
263
+ * self.model_config.num_layers
264
+ )
265
+ num_pages = config.num_page_override
266
+ if num_pages is None:
267
+ model_memory = old_free_memory - new_free_memory
268
+ available_memory = int(config.memory_ratio * old_free_memory) - model_memory
269
+ num_pages = available_memory // cache_per_page
270
+
271
+ assert num_pages > 1, "Not enough memory for KV cache, try reducing --num-tokens"
272
+ real_kv_size = num_pages * cache_per_page
273
+ logger.info(f"Allocating {num_pages} pages for KV cache, K + V = {mem_GB(real_kv_size)}")
274
+ return num_pages
275
+
276
+ def _sync_get_memory(self) -> Tuple[int, int]:
277
+ """Get the min and max free memory across TP ranks."""
278
+ torch.cuda.synchronize(self.device)
279
+ torch.cuda.empty_cache()
280
+ torch.cuda.reset_peak_memory_stats(self.device)
281
+ free_memory = get_free_memory(self.device)
282
+ free_mem_tensor = torch.tensor([free_memory, -free_memory], device="cpu", dtype=torch.int64)
283
+ torch.distributed.all_reduce(
284
+ free_mem_tensor, op=torch.distributed.ReduceOp.MIN, group=self.tp_cpu_group
285
+ )
286
+ min_free_memory = int(free_mem_tensor[0].item())
287
+ max_free_memory = -int(free_mem_tensor[1].item())
288
+ if max_free_memory - min_free_memory > 2 * 1024 * 1024 * 1024:
289
+ logger.error(
290
+ f"Memory across TP ranks are imbalanced:"
291
+ f" min {mem_GB(min_free_memory)}, max {mem_GB(max_free_memory)}"
292
+ )
293
+ raise RuntimeError("Memory across TP ranks are imbalanced")
294
+
295
+ return min_free_memory, max_free_memory
296
+
297
+ def forward_batch_tts(self, batch: TTSBatch) -> torch.Tensor:
298
+ """Forward pass for TTS batch, returning multi-codebook logits.
299
+
300
+ Args:
301
+ batch: TTS batch with 2D input tokens
302
+
303
+ Returns:
304
+ Logits tensor of shape (batch_size, n_codebooks, vocab_size)
305
+ """
306
+ assert torch.cuda.current_stream() == self.stream
307
+ with self.ctx.forward_batch(batch):
308
+ # For TTS, the model outputs multi-codebook logits directly
309
+ # The zonos2 model has a MultiParallelLMHead that outputs
310
+ # (batch, seq, n_codebooks, vocab_size)
311
+ if self.graph_runner.can_use_cuda_graph(batch):
312
+ logits = self.graph_runner.replay(batch)
313
+ else:
314
+ logits = self.model.forward()
315
+
316
+ # logits shape: (batch, n_codebooks, vocab) for decode
317
+ # or (total_tokens, n_codebooks, vocab) for prefill
318
+ # We only care about the last token per sequence
319
+ if batch.is_decode:
320
+ return logits[: batch.size]
321
+ else:
322
+ # For prefill, get the LAST token logits for each sequence
323
+ # logits shape: (total_tokens, n_codebooks, vocab)
324
+ # Need to extract logits at the last position of each sequence
325
+ last_indices = []
326
+ cumsum = 0
327
+ for req in batch.reqs:
328
+ cumsum += req.extend_len # extend_len = device_len - cached_len
329
+ last_indices.append(cumsum - 1)
330
+ last_indices = torch.tensor(last_indices, device=logits.device)
331
+ return logits[last_indices]
332
+
333
+ def shutdown(self) -> None:
334
+ self.graph_runner.destroy_cuda_graphs()
335
+ torch.distributed.destroy_process_group()
336
+ destroy_distributed()
zonos2/engine/graph.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import gc
4
+ from typing import TYPE_CHECKING, Dict, List
5
+
6
+ import torch
7
+ from tqdm import tqdm
8
+ from zonos2.core import TTSBatch, TTSReq, get_global_ctx
9
+ from zonos2.distributed import get_tp_info
10
+ from zonos2.utils import init_logger
11
+
12
+ if TYPE_CHECKING:
13
+ from zonos2.attention import BaseAttnBackend
14
+ from zonos2.models import Zonos2ForCausalLM
15
+
16
+ logger = init_logger(__name__)
17
+
18
+
19
+ def _determine_cuda_graph_bs(
20
+ cuda_graph_bs: List[int] | None,
21
+ cuda_graph_max_bs: int | None,
22
+ free_memory: int,
23
+ ) -> List[int]:
24
+ if cuda_graph_bs is not None:
25
+ return cuda_graph_bs
26
+
27
+ free_memory_gb = free_memory / (1 << 30)
28
+ if cuda_graph_max_bs is None:
29
+ if free_memory_gb > 80: # H200
30
+ cuda_graph_max_bs = 256
31
+ else:
32
+ cuda_graph_max_bs = 160
33
+
34
+ if cuda_graph_max_bs < 1:
35
+ return []
36
+
37
+ return [1, 2, 4] + list(range(8, cuda_graph_max_bs + 1, 8))
38
+
39
+
40
+ def mem_GB(size: int) -> str:
41
+ return f"{size / (1024**3):.2f} GiB"
42
+
43
+
44
+ def get_free_memory(device: torch.device) -> int:
45
+ return torch.cuda.mem_get_info(device)[0]
46
+
47
+
48
+ class GraphRunner:
49
+ def __init__(
50
+ self,
51
+ stream: torch.cuda.Stream,
52
+ device: torch.device,
53
+ model: Zonos2ForCausalLM,
54
+ attn_backend: BaseAttnBackend,
55
+ cuda_graph_bs: List[int] | None,
56
+ cuda_graph_max_bs: int | None,
57
+ free_memory: int,
58
+ max_seq_len: int,
59
+ vocab_size: int,
60
+ dummy_req: TTSReq,
61
+ ) -> None:
62
+ cuda_graph_bs = _determine_cuda_graph_bs(
63
+ cuda_graph_bs=cuda_graph_bs,
64
+ cuda_graph_max_bs=cuda_graph_max_bs,
65
+ free_memory=free_memory,
66
+ )
67
+ self.attn_backend = attn_backend
68
+ if not cuda_graph_bs:
69
+ logger.info_rank0("CUDA graph is disabled.")
70
+ self.max_graph_bs = 0
71
+ self.graph_bs_list = []
72
+ self.dummy_req = dummy_req
73
+ self.stream = stream
74
+ self.device = device
75
+ self.graph_map = {}
76
+ return
77
+
78
+ self.max_graph_bs = max(cuda_graph_bs)
79
+ self.graph_bs_list = sorted(cuda_graph_bs)
80
+ self.dummy_req = dummy_req
81
+ self.stream = stream
82
+ self.device = device
83
+ self.graph_map = self._capture_graphs(max_seq_len, vocab_size, model)
84
+
85
+ def _capture_graphs(self, max_seq_len: int, vocab_size: int, model: Zonos2ForCausalLM):
86
+ graph_map: Dict[int, torch.cuda.CUDAGraph] = {}
87
+ if self.max_graph_bs == 0:
88
+ logger.info_rank0("CUDA graph is disabled.")
89
+ return graph_map
90
+
91
+ self.logits = torch.empty(
92
+ (self.max_graph_bs, self.dummy_req.n_codebooks, vocab_size),
93
+ dtype=torch.float32,
94
+ device=self.device,
95
+ )
96
+ frame_width = self.dummy_req.frame_width
97
+ self.attn_backend.init_capture_graph(
98
+ max_seq_len=max_seq_len, bs_list=self.graph_bs_list, frame_width=frame_width
99
+ )
100
+
101
+ torch.cuda.synchronize(self.device)
102
+ torch.cuda.empty_cache()
103
+ torch.cuda.reset_peak_memory_stats(self.device)
104
+
105
+ logger.info_rank0(f"Start capturing CUDA graphs with sizes: {self.graph_bs_list}")
106
+ free_memory = get_free_memory(self.device)
107
+ logger.info_rank0(f"Free GPU memory before capturing CUDA graphs: {mem_GB(free_memory)}")
108
+
109
+ pbar = tqdm(
110
+ sorted(self.graph_bs_list, reverse=True),
111
+ desc="Preparing for capturing CUDA graphs...",
112
+ unit="batch",
113
+ disable=not get_tp_info().is_primary(), # disable for non-primary ranks
114
+ )
115
+ pool = None
116
+ for bs in pbar:
117
+ free_memory = get_free_memory(self.device)
118
+ pbar.desc = f"Capturing graphs: bs = {bs:<3} | avail_mem = {mem_GB(free_memory)}"
119
+ pbar.refresh()
120
+ graph = torch.cuda.CUDAGraph()
121
+ batch = TTSBatch(reqs=[self.dummy_req] * bs, phase="decode")
122
+ self.attn_backend.prepare_for_capture(batch)
123
+ with get_global_ctx().forward_batch(batch):
124
+ self.logits[:bs] = model.forward()
125
+ with torch.cuda.graph(graph, pool=pool, stream=self.stream):
126
+ self.logits[:bs] = model.forward()
127
+ if pool is None:
128
+ pool = graph.pool()
129
+ graph_map[bs] = graph
130
+
131
+ free_memory = get_free_memory(self.device)
132
+ logger.info_rank0(f"Free GPU memory after capturing CUDA graphs: {mem_GB(free_memory)}")
133
+ return graph_map
134
+
135
+ def can_use_cuda_graph(self, batch: TTSBatch) -> bool:
136
+ return batch.is_decode and batch.size <= self.max_graph_bs
137
+
138
+ def replay(self, batch: TTSBatch) -> torch.Tensor:
139
+ assert self.can_use_cuda_graph(batch)
140
+ g = self.graph_map[batch.padded_size]
141
+ self.attn_backend.prepare_for_replay(batch)
142
+ g.replay()
143
+ return self.logits[: batch.size]
144
+
145
+ def pad_batch(self, batch: TTSBatch) -> int:
146
+ padded_size = ( # choose the first available batch size
147
+ next(bs for bs in self.graph_bs_list if bs >= batch.size)
148
+ if self.can_use_cuda_graph(batch)
149
+ else batch.size
150
+ )
151
+ batch.padded_reqs = batch.reqs + [self.dummy_req] * (padded_size - batch.size)
152
+ return batch.padded_size - batch.size
153
+
154
+ # NOTE: This must be called before freeing NCCL resources to prevent program hang
155
+ def destroy_cuda_graphs(self) -> None:
156
+ del self.graph_map
157
+ gc.collect()
zonos2/engine/sample.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """TTS sampler for multi-codebook audio generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING, List
7
+
8
+ import torch
9
+ from zonos2.tts.sampler import sample_tts
10
+
11
+ if TYPE_CHECKING:
12
+ from zonos2.core import TTSBatch
13
+
14
+
15
+ def make_device_tensor(
16
+ data: List, dtype: torch.dtype, device: torch.device
17
+ ) -> torch.Tensor:
18
+ """Create a tensor on device from a list."""
19
+ return torch.tensor(data, dtype=dtype, pin_memory=True).to(
20
+ device, non_blocking=True
21
+ )
22
+
23
+
24
+ @dataclass
25
+ class TTSBatchSamplingArgs:
26
+ """Sampling arguments for a TTS batch."""
27
+
28
+ temperatures: torch.Tensor
29
+ top_ks: torch.Tensor
30
+ top_ps: torch.Tensor
31
+ min_ps: torch.Tensor
32
+ text_vocab: int
33
+ repetition_token_ids: torch.Tensor | None = None
34
+ repetition_penalties: torch.Tensor | None = None
35
+ generators: list[torch.Generator | None] = field(default_factory=list)
36
+
37
+
38
+ @dataclass
39
+ class TTSSampler:
40
+ """Sampler for multi-codebook TTS output."""
41
+
42
+ device: torch.device
43
+ n_codebooks: int
44
+ codebook_size: int
45
+ text_vocab: int
46
+
47
+ def prepare(
48
+ self, batch: TTSBatch, token_pool: torch.Tensor | None = None
49
+ ) -> TTSBatchSamplingArgs:
50
+ """Prepare sampling arguments from a TTS batch.
51
+
52
+ Args:
53
+ batch: TTS batch with requests
54
+ token_pool: Optional device-side token pool for recent generated history
55
+
56
+ Returns:
57
+ TTSBatchSamplingArgs with per-sequence sampling parameters
58
+ """
59
+ params = [r.sampling_params for r in batch.reqs]
60
+
61
+ MIN_T = 1e-6
62
+ temps = [max(p.temperature, MIN_T) for p in params]
63
+ top_ks = [p.topk if p.topk >= 1 else self.codebook_size for p in params]
64
+ top_ps = [min(max(p.top_p, 0.0), 1.0) for p in params]
65
+ min_ps = [max(p.min_p, 0.0) for p in params]
66
+ repetition_windows = [max(int(p.repetition_window), 0) for p in params]
67
+ repetition_penalties = [max(float(p.repetition_penalty), 1.0) for p in params]
68
+ repetition_codebooks = [
69
+ self.n_codebooks
70
+ if int(p.repetition_codebooks) < 0
71
+ else min(max(int(p.repetition_codebooks), 0), self.n_codebooks)
72
+ for p in params
73
+ ]
74
+ generators = [r.rng for r in batch.reqs]
75
+ repetition_token_ids = None
76
+ repetition_penalties_tensor = None
77
+
78
+ active_windows = [
79
+ min(window, r.total_generated, r.device_len)
80
+ if window > 0 and penalty > 1.0 and codebooks > 0
81
+ else 0
82
+ for r, window, penalty, codebooks in zip(
83
+ batch.reqs,
84
+ repetition_windows,
85
+ repetition_penalties,
86
+ repetition_codebooks,
87
+ )
88
+ ]
89
+ max_window = max(active_windows, default=0)
90
+ if max_window > 0:
91
+ repetition_token_ids = torch.full(
92
+ (len(batch.reqs), self.n_codebooks, max_window),
93
+ -1,
94
+ dtype=torch.int64,
95
+ device=self.device,
96
+ )
97
+ for i, (req, window) in enumerate(zip(batch.reqs, active_windows)):
98
+ if window <= 0:
99
+ continue
100
+
101
+ start = req.device_len - window
102
+ if token_pool is None:
103
+ history = req.input_ids[start : req.device_len, : self.n_codebooks].to(
104
+ device=self.device, dtype=torch.int64, non_blocking=True
105
+ )
106
+ else:
107
+ history = token_pool[
108
+ req.table_idx, start : req.device_len, : self.n_codebooks
109
+ ].to(dtype=torch.int64)
110
+
111
+ history = history.transpose(0, 1).contiguous()
112
+ valid = (history >= 0) & (history < self.codebook_size)
113
+ valid[repetition_codebooks[i] :] = False
114
+ repetition_token_ids[i, :, -window:] = torch.where(
115
+ valid, history, torch.full_like(history, -1)
116
+ )
117
+
118
+ repetition_penalties_tensor = make_device_tensor(
119
+ repetition_penalties, torch.float32, self.device
120
+ )
121
+
122
+ return TTSBatchSamplingArgs(
123
+ temperatures=make_device_tensor(temps, torch.float32, self.device),
124
+ top_ks=make_device_tensor(top_ks, torch.int32, self.device),
125
+ top_ps=make_device_tensor(top_ps, torch.float32, self.device),
126
+ min_ps=make_device_tensor(min_ps, torch.float32, self.device),
127
+ text_vocab=self.text_vocab,
128
+ repetition_token_ids=repetition_token_ids,
129
+ repetition_penalties=repetition_penalties_tensor,
130
+ generators=generators,
131
+ )
132
+
133
+ def sample(
134
+ self, logits: torch.Tensor, args: TTSBatchSamplingArgs
135
+ ) -> List[List[int]]:
136
+ """Sample from multi-codebook logits.
137
+
138
+ Args:
139
+ logits: Shape (B, n_codebooks, vocab_size)
140
+ args: Sampling arguments
141
+
142
+ Returns:
143
+ List of unpacked tokens [cb0, cb1, ..., cb8, text_placeholder]
144
+ """
145
+ return sample_tts(
146
+ logits=logits,
147
+ temperatures=args.temperatures,
148
+ top_ks=args.top_ks,
149
+ top_ps=args.top_ps,
150
+ min_ps=args.min_ps,
151
+ repetition_token_ids=args.repetition_token_ids,
152
+ repetition_penalties=args.repetition_penalties,
153
+ text_vocab=args.text_vocab,
154
+ generators=args.generators,
155
+ )
156
+
157
+ def sample_to_tensor(
158
+ self, logits: torch.Tensor, args: TTSBatchSamplingArgs
159
+ ) -> torch.Tensor:
160
+ """Sample and return as tensor instead of list.
161
+
162
+ Args:
163
+ logits: Shape (B, n_codebooks, vocab_size)
164
+ args: Sampling arguments
165
+
166
+ Returns:
167
+ Tensor of shape (B, n_codebooks + 1) with sampled tokens
168
+ """
169
+ tokens_list = self.sample(logits, args)
170
+ return torch.tensor(tokens_list, dtype=torch.int32, device=self.device)
zonos2/env.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from functools import partial
5
+ from typing import Callable, Generic, TypeVar
6
+
7
+
8
+ class BaseEnv:
9
+ def _init(self, name: str) -> None:
10
+ raise NotImplementedError
11
+
12
+
13
+ T = TypeVar("T")
14
+
15
+
16
+ class EnvVar(BaseEnv, Generic[T]):
17
+ def __init__(self, default_value: T, fn: Callable[[str], T]):
18
+ self.value = default_value
19
+ self.fn = fn
20
+ super().__init__()
21
+
22
+ def _init(self, name: str) -> None:
23
+ env_value = os.getenv(name)
24
+ if env_value is not None:
25
+ try:
26
+ self.value = self.fn(env_value)
27
+ except Exception:
28
+ pass
29
+
30
+ def __bool__(self):
31
+ return self.value
32
+
33
+ def __str__(self):
34
+ return str(self.value)
35
+
36
+
37
+ _TO_BOOL = lambda x: x.lower() in ("1", "true", "yes")
38
+
39
+
40
+ def _PARSE_MEM_BYTES(mem: str) -> int:
41
+ mem = mem.strip().upper()
42
+ if not mem[-1].isalpha():
43
+ return int(mem)
44
+ if mem.endswith("B"):
45
+ mem = mem[:-1]
46
+ UNIT_MAP = {"K": 1024, "M": 1024**2, "G": 1024**3}
47
+ return int(float(mem[:-1]) * UNIT_MAP[mem[-1]])
48
+
49
+
50
+ ZONOS2_ENV_PREFIX = "ZONOS2_"
51
+ EnvInt = partial(EnvVar[int], fn=int)
52
+ EnvFloat = partial(EnvVar[float], fn=float)
53
+ EnvBool = partial(EnvVar[bool], fn=_TO_BOOL)
54
+ EnvOption = partial(EnvVar[bool | None], fn=_TO_BOOL, default_value=None)
55
+ EnvMem = partial(EnvVar[int], fn=_PARSE_MEM_BYTES)
56
+
57
+
58
+ class EnvClassSingleton:
59
+ _instance: EnvClassSingleton | None = None
60
+
61
+ # shell
62
+ SHELL_MAX_TOKENS = EnvInt(2048)
63
+ SHELL_TOP_K = EnvInt(-1)
64
+ SHELL_TOP_P = EnvFloat(1.0)
65
+ SHELL_TEMPERATURE = EnvFloat(0.6)
66
+
67
+ # backend runtime
68
+ FLASHINFER_USE_TENSOR_CORES = EnvOption()
69
+ DISABLE_OVERLAP_SCHEDULING = EnvBool(False)
70
+ PYNCCL_MAX_BUFFER_SIZE = EnvMem(1024**3)
71
+
72
+ def __new__(cls):
73
+ # single instance
74
+ if cls._instance is None:
75
+ cls._instance = super().__new__(cls)
76
+ return cls._instance
77
+
78
+ def __init__(self) -> None:
79
+ for attr_name in dir(self):
80
+ if attr_name.startswith("_"):
81
+ continue
82
+ attr_value = getattr(self, attr_name)
83
+ assert isinstance(attr_value, BaseEnv)
84
+ attr_value._init(f"{ZONOS2_ENV_PREFIX}{attr_name}")
85
+
86
+
87
+ ENV = EnvClassSingleton()
zonos2/kernel/__init__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .index import indexing
2
+ from .pynccl import PyNCCLCommunicator, init_pynccl
3
+ from .radix import fast_compare_key
4
+ from .store import store_cache
5
+ from .tensor import test_tensor
6
+
7
+ __all__ = [
8
+ "indexing",
9
+ "fast_compare_key",
10
+ "store_cache",
11
+ "test_tensor",
12
+ "init_pynccl",
13
+ "PyNCCLCommunicator",
14
+ ]
zonos2/kernel/__main__.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ assert __name__ == "__main__"
2
+
3
+
4
+ def generate_clangd():
5
+ import os
6
+ import subprocess
7
+
8
+ from tvm_ffi.libinfo import find_dlpack_include_path, find_include_path
9
+ from zonos2.kernel.utils import DEFAULT_INCLUDE
10
+ from zonos2.utils import init_logger
11
+
12
+ logger = init_logger(__name__)
13
+ logger.info("Generating .clangd file...")
14
+ include_paths = [find_include_path(), find_dlpack_include_path()] + DEFAULT_INCLUDE
15
+ status = subprocess.run(
16
+ args=["nvidia-smi", "--query-gpu=compute_cap", "--format=csv,noheader"],
17
+ capture_output=True,
18
+ check=True,
19
+ )
20
+ compute_cap = status.stdout.decode("utf-8").strip().split("\n")[0]
21
+ major, minor = compute_cap.split(".")
22
+ compile_flags = ",\n ".join(
23
+ [
24
+ "-xcuda",
25
+ f"--cuda-gpu-arch=sm_{major}{minor}",
26
+ "-std=c++20",
27
+ "-Wall",
28
+ "-Wextra",
29
+ ]
30
+ + [f"-isystem{path}" for path in include_paths]
31
+ )
32
+ clangd_content = f"""
33
+ CompileFlags:
34
+ Add: [
35
+ {compile_flags}
36
+ ]
37
+ """
38
+ if os.path.exists(".clangd"):
39
+ logger.warning(".clangd file already exists, nothing done.")
40
+ logger.warning(f"suggested content: {clangd_content}")
41
+ else:
42
+ with open(".clangd", "w") as f:
43
+ f.write(clangd_content)
44
+ logger.info(".clangd file generated.")
45
+
46
+
47
+ generate_clangd()
zonos2/kernel/csrc/include/zonos2/nccl227.h ADDED
@@ -0,0 +1,571 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*************************************************************************
2
+ * Copyright (c) 2015-2021, NVIDIA CORPORATION. All rights reserved.
3
+ *
4
+ * See LICENSE.txt for license information
5
+ ************************************************************************/
6
+
7
+ #ifndef NCCL_H_
8
+ #define NCCL_H_
9
+
10
+ #include <cuda_runtime.h>
11
+ #include <cuda_fp16.h>
12
+ #if CUDART_VERSION >= 11000
13
+ #include <cuda_bf16.h>
14
+ #endif
15
+ #if __cplusplus && CUDART_VERSION >= 11080
16
+ #include <cuda_fp8.h>
17
+ #endif
18
+
19
+ #define NCCL_MAJOR 2
20
+ #define NCCL_MINOR 27
21
+ #define NCCL_PATCH 1
22
+ #define NCCL_SUFFIX ""
23
+
24
+ #define NCCL_VERSION_CODE 22701
25
+ #define NCCL_VERSION(X,Y,Z) (((X) <= 2 && (Y) <= 8) ? (X) * 1000 + (Y) * 100 + (Z) : (X) * 10000 + (Y) * 100 + (Z))
26
+
27
+ #ifdef __cplusplus
28
+ extern "C" {
29
+ #endif
30
+
31
+ #include <limits.h>
32
+
33
+ /* Opaque handle to communicator */
34
+ typedef struct ncclComm* ncclComm_t;
35
+ typedef struct ncclWindow_vidmem* ncclWindow_t;
36
+ #define NCCL_COMM_NULL NULL
37
+
38
+ #define NCCL_UNIQUE_ID_BYTES 128
39
+ typedef struct { char internal[NCCL_UNIQUE_ID_BYTES]; } ncclUniqueId;
40
+
41
+ /* Error type */
42
+ typedef enum { ncclSuccess = 0,
43
+ ncclUnhandledCudaError = 1,
44
+ ncclSystemError = 2,
45
+ ncclInternalError = 3,
46
+ ncclInvalidArgument = 4,
47
+ ncclInvalidUsage = 5,
48
+ ncclRemoteError = 6,
49
+ ncclInProgress = 7,
50
+ ncclNumResults = 8 } ncclResult_t;
51
+
52
+ #define NCCL_CONFIG_UNDEF_INT INT_MIN
53
+ #define NCCL_CONFIG_UNDEF_PTR NULL
54
+ #define NCCL_SPLIT_NOCOLOR -1
55
+ #define NCCL_UNDEF_FLOAT -1.0f
56
+
57
+ /* Window Registration flags */
58
+ #define NCCL_WIN_DEFAULT 0x00
59
+ #define NCCL_WIN_COLL_SYMMETRIC 0x01
60
+
61
+ #define NCCL_WIN_REQUIRED_ALIGNMENT 4096
62
+
63
+ /* NCCL performance policy */
64
+ #define NCCL_CTA_POLICY_DEFAULT 0x00
65
+ #define NCCL_CTA_POLICY_EFFICIENCY 0x01
66
+ #define NCCL_CTA_POLICY_ZERO 0x02
67
+
68
+ /* ncclCommShrink flags*/
69
+ #define NCCL_SHRINK_DEFAULT 0x00 /* shrink the parent communicator */
70
+ #define NCCL_SHRINK_ABORT 0x01 /* First, terminate ongoing parent operations, and then shrink the parent communicator */
71
+
72
+ /* Communicator configuration. Users can assign value to attributes to specify the
73
+ * behavior of a communicator. */
74
+ typedef struct ncclConfig_v22800 {
75
+ /* attributes that users should never touch. */
76
+ size_t size;
77
+ unsigned int magic;
78
+ unsigned int version;
79
+ /* attributes that users are able to customize. */
80
+ int blocking;
81
+ int cgaClusterSize;
82
+ int minCTAs;
83
+ int maxCTAs;
84
+ const char *netName;
85
+ int splitShare;
86
+ int trafficClass;
87
+ const char *commName;
88
+ int collnetEnable;
89
+ int CTAPolicy;
90
+ int shrinkShare;
91
+ int nvlsCTAs;
92
+ int nChannelsPerNetPeer;
93
+ int nvlinkCentricSched;
94
+ } ncclConfig_t;
95
+
96
+ /* Config initializer must be assigned to initialize config structure when it is created.
97
+ * Not initialized config will result in NCCL error. */
98
+ #define NCCL_CONFIG_INITIALIZER { \
99
+ sizeof(ncclConfig_t), /* size */ \
100
+ 0xcafebeef, /* magic */ \
101
+ NCCL_VERSION(NCCL_MAJOR, NCCL_MINOR, NCCL_PATCH), /* version */ \
102
+ NCCL_CONFIG_UNDEF_INT, /* blocking */ \
103
+ NCCL_CONFIG_UNDEF_INT, /* cgaClusterSize */ \
104
+ NCCL_CONFIG_UNDEF_INT, /* minCTAs */ \
105
+ NCCL_CONFIG_UNDEF_INT, /* maxCTAs */ \
106
+ NCCL_CONFIG_UNDEF_PTR, /* netName */ \
107
+ NCCL_CONFIG_UNDEF_INT, /* splitShare */ \
108
+ NCCL_CONFIG_UNDEF_INT, /* trafficClass */ \
109
+ NCCL_CONFIG_UNDEF_PTR, /* commName */ \
110
+ NCCL_CONFIG_UNDEF_INT, /* collnetEnable */ \
111
+ NCCL_CONFIG_UNDEF_INT, /* CTAPolicy */ \
112
+ NCCL_CONFIG_UNDEF_INT, /* shrinkShare */ \
113
+ NCCL_CONFIG_UNDEF_INT, /* nvlsCTAs */ \
114
+ NCCL_CONFIG_UNDEF_INT, /* nChannelsPerNetPeer */ \
115
+ NCCL_CONFIG_UNDEF_INT, /* nvlinkCentricSched */ \
116
+ }
117
+
118
+ /* This struct will be used by ncclGroupSimulateEnd() API to query information about simulation. */
119
+ typedef struct ncclSimInfo_v22200 {
120
+ size_t size;
121
+ unsigned int magic;
122
+ unsigned int version;
123
+ float estimatedTime;
124
+ } ncclSimInfo_t;
125
+
126
+ /* NCCL_SIM_INFO_INITIALIZER must be assigned to initialize simInfo structure when it is created.
127
+ * Not initialized simInfo will result in NCCL error. */
128
+ #define NCCL_SIM_INFO_INITIALIZER { \
129
+ sizeof(ncclSimInfo_t), /* size */ \
130
+ 0x74685283, /* magic */ \
131
+ NCCL_VERSION(NCCL_MAJOR, NCCL_MINOR, NCCL_PATCH), /* version */ \
132
+ NCCL_UNDEF_FLOAT /* estimated time */ \
133
+ }
134
+
135
+ /* NCCL malloc and free function for all types of NCCL optimizations
136
+ * (e.g. user buffer registration). The actual allocated size might
137
+ * be larger than requested due to granularity requirement. */
138
+ ncclResult_t ncclMemAlloc(void** ptr, size_t size);
139
+ ncclResult_t pncclMemAlloc(void** ptr, size_t size);
140
+
141
+ ncclResult_t ncclMemFree(void *ptr);
142
+ ncclResult_t pncclMemFree(void *ptr);
143
+
144
+ /* Return the NCCL_VERSION_CODE of the NCCL library in the supplied integer.
145
+ * This integer is coded with the MAJOR, MINOR and PATCH level of the
146
+ * NCCL library
147
+ */
148
+ ncclResult_t ncclGetVersion(int *version);
149
+ ncclResult_t pncclGetVersion(int *version);
150
+
151
+ /* Generates an Id to be used in ncclCommInitRank. ncclGetUniqueId should be
152
+ * called once and the Id should be distributed to all ranks in the
153
+ * communicator before calling ncclCommInitRank. */
154
+ ncclResult_t ncclGetUniqueId(ncclUniqueId* uniqueId);
155
+ ncclResult_t pncclGetUniqueId(ncclUniqueId* uniqueId);
156
+
157
+ /* Create a new communicator (multi thread/process version) with a configuration
158
+ * set by users. */
159
+ ncclResult_t ncclCommInitRankConfig(ncclComm_t* comm, int nranks, ncclUniqueId commId, int rank, ncclConfig_t* config);
160
+ ncclResult_t pncclCommInitRankConfig(ncclComm_t* comm, int nranks, ncclUniqueId commId, int rank, ncclConfig_t* config);
161
+
162
+ /* Creates a new communicator (multi thread/process version).
163
+ * rank must be between 0 and nranks-1 and unique within a communicator clique.
164
+ * Each rank is associated to a CUDA device, which has to be set before calling
165
+ * ncclCommInitRank.
166
+ * ncclCommInitRank implicitly syncronizes with other ranks, so it must be
167
+ * called by different threads/processes or use ncclGroupStart/ncclGroupEnd. */
168
+ ncclResult_t ncclCommInitRank(ncclComm_t* comm, int nranks, ncclUniqueId commId, int rank);
169
+ ncclResult_t pncclCommInitRank(ncclComm_t* comm, int nranks, ncclUniqueId commId, int rank);
170
+
171
+ /* Creates a clique of communicators (single process version).
172
+ * This is a convenience function to create a single-process communicator clique.
173
+ * Returns an array of ndev newly initialized communicators in comm.
174
+ * comm should be pre-allocated with size at least ndev*sizeof(ncclComm_t).
175
+ * If devlist is NULL, the first ndev CUDA devices are used.
176
+ * Order of devlist defines user-order of processors within the communicator. */
177
+ ncclResult_t ncclCommInitAll(ncclComm_t* comm, int ndev, const int* devlist);
178
+ ncclResult_t pncclCommInitAll(ncclComm_t* comm, int ndev, const int* devlist);
179
+
180
+ /* Finalize a communicator. ncclCommFinalize flushes all issued communications,
181
+ * and marks communicator state as ncclInProgress. The state will change to ncclSuccess
182
+ * when the communicator is globally quiescent and related resources are freed; then,
183
+ * calling ncclCommDestroy can locally free the rest of the resources (e.g. communicator
184
+ * itself) without blocking. */
185
+ ncclResult_t ncclCommFinalize(ncclComm_t comm);
186
+ ncclResult_t pncclCommFinalize(ncclComm_t comm);
187
+
188
+ /* Frees local resources associated with communicator object. */
189
+ ncclResult_t ncclCommDestroy(ncclComm_t comm);
190
+ ncclResult_t pncclCommDestroy(ncclComm_t comm);
191
+
192
+ /* Frees resources associated with communicator object and aborts any operations
193
+ * that might still be running on the device. */
194
+ ncclResult_t ncclCommAbort(ncclComm_t comm);
195
+ ncclResult_t pncclCommAbort(ncclComm_t comm);
196
+
197
+ /* Creates one or more communicators from an existing one.
198
+ * Ranks with the same color will end up in the same communicator.
199
+ * Within the new communicator, key will be used to order ranks.
200
+ * NCCL_SPLIT_NOCOLOR as color will indicate the rank will not be part of any group
201
+ * and will therefore return a NULL communicator.
202
+ * If config is NULL, the new communicator will inherit the original communicator's
203
+ * configuration*/
204
+ ncclResult_t ncclCommSplit(ncclComm_t comm, int color, int key, ncclComm_t *newcomm, ncclConfig_t* config);
205
+ ncclResult_t pncclCommSplit(ncclComm_t comm, int color, int key, ncclComm_t *newcomm, ncclConfig_t* config);
206
+
207
+ /* Shrink existing communicator.
208
+ * Ranks in excludeRanksList will be removed form the existing communicator.
209
+ * Within the new communicator, ranks will be re-ordered to fill the gap of removed ones.
210
+ * If config is NULL, the new communicator will inherit the original communicator's configuration
211
+ * The flag enables NCCL to adapt to various states of the parent communicator, see NCCL_SHRINK flags.*/
212
+ ncclResult_t ncclCommShrink(ncclComm_t comm, int* excludeRanksList, int excludeRanksCount, ncclComm_t* newcomm, ncclConfig_t* config, int shrinkFlags);
213
+ ncclResult_t pncclCommShrink(ncclComm_t comm, int* excludeRanksList, int excludeRanksCount, ncclComm_t* newcomm, ncclConfig_t* config, int shrinkFlags);
214
+
215
+ /* Creates a new communicator (multi thread/process version), similar to ncclCommInitRankConfig.
216
+ * Allows to use more than one ncclUniqueId (up to one per rank), indicated by nId, to accelerate the init operation.
217
+ * The number of ncclUniqueIds and their order must be the same for every rank.
218
+ */
219
+ ncclResult_t ncclCommInitRankScalable(ncclComm_t* newcomm, int nranks, int myrank, int nId, ncclUniqueId* commIds, ncclConfig_t* config);
220
+ ncclResult_t pncclCommInitRankScalable(ncclComm_t* newcomm, int nranks, int myrank, int nId, ncclUniqueId* commIds, ncclConfig_t* config);
221
+
222
+ /* Returns a string for each error code. */
223
+ const char* ncclGetErrorString(ncclResult_t result);
224
+ const char* pncclGetErrorString(ncclResult_t result);
225
+
226
+ /* Returns a human-readable message of the last error that occurred. */
227
+ const char* ncclGetLastError(ncclComm_t comm);
228
+ const char* pncclGetLastError(ncclComm_t comm);
229
+
230
+ /* Reload environment variables that determine logging. */
231
+ __attribute__ ((deprecated("ncclResetDebugInit is not supported as part of the NCCL API and will be removed in the future")))
232
+ void ncclResetDebugInit();
233
+ __attribute__ ((deprecated("pncclResetDebugInit is not supported as part of the NCCL API and will be removed in the future")))
234
+ void pncclResetDebugInit();
235
+
236
+ /* Checks whether the comm has encountered any asynchronous errors */
237
+ ncclResult_t ncclCommGetAsyncError(ncclComm_t comm, ncclResult_t *asyncError);
238
+ ncclResult_t pncclCommGetAsyncError(ncclComm_t comm, ncclResult_t *asyncError);
239
+
240
+ /* Gets the number of ranks in the communicator clique. */
241
+ ncclResult_t ncclCommCount(const ncclComm_t comm, int* count);
242
+ ncclResult_t pncclCommCount(const ncclComm_t comm, int* count);
243
+
244
+ /* Returns the cuda device number associated with the communicator. */
245
+ ncclResult_t ncclCommCuDevice(const ncclComm_t comm, int* device);
246
+ ncclResult_t pncclCommCuDevice(const ncclComm_t comm, int* device);
247
+
248
+ /* Returns the user-ordered "rank" associated with the communicator. */
249
+ ncclResult_t ncclCommUserRank(const ncclComm_t comm, int* rank);
250
+ ncclResult_t pncclCommUserRank(const ncclComm_t comm, int* rank);
251
+
252
+ /* Register CUDA buffer for zero-copy operation */
253
+ ncclResult_t ncclCommRegister(const ncclComm_t comm, void* buff, size_t size, void** handle);
254
+ ncclResult_t pncclCommRegister(const ncclComm_t comm, void* buff, size_t size, void** handle);
255
+
256
+ /* Deregister CUDA buffer */
257
+ ncclResult_t ncclCommDeregister(const ncclComm_t comm, void* handle);
258
+ ncclResult_t pncclCommDeregister(const ncclComm_t comm, void* handle);
259
+
260
+ /* Register memory window */
261
+ ncclResult_t ncclCommWindowRegister(ncclComm_t comm, void* buff, size_t size, ncclWindow_t* win, int winFlags);
262
+ ncclResult_t pncclCommWindowRegister(ncclComm_t comm, void* buff, size_t size, ncclWindow_t* win, int winFlags);
263
+
264
+ /* Deregister symmetric memory */
265
+ ncclResult_t ncclCommWindowDeregister(ncclComm_t comm, ncclWindow_t win);
266
+ ncclResult_t pncclCommWindowDeregister(ncclComm_t comm, ncclWindow_t win);
267
+
268
+ /* Reduction operation selector */
269
+ typedef enum { ncclNumOps_dummy = 5 } ncclRedOp_dummy_t;
270
+ typedef enum { ncclSum = 0,
271
+ ncclProd = 1,
272
+ ncclMax = 2,
273
+ ncclMin = 3,
274
+ ncclAvg = 4,
275
+ /* ncclNumOps: The number of built-in ncclRedOp_t values. Also
276
+ * serves as the least possible value for dynamic ncclRedOp_t's
277
+ * as constructed by ncclRedOpCreate*** functions. */
278
+ ncclNumOps = 5,
279
+ /* ncclMaxRedOp: The largest valid value for ncclRedOp_t.
280
+ * It is defined to be the largest signed value (since compilers
281
+ * are permitted to use signed enums) that won't grow
282
+ * sizeof(ncclRedOp_t) when compared to previous NCCL versions to
283
+ * maintain ABI compatibility. */
284
+ ncclMaxRedOp = 0x7fffffff>>(32-8*sizeof(ncclRedOp_dummy_t))
285
+ } ncclRedOp_t;
286
+
287
+ /* Data types */
288
+ typedef enum { ncclInt8 = 0, ncclChar = 0,
289
+ ncclUint8 = 1,
290
+ ncclInt32 = 2, ncclInt = 2,
291
+ ncclUint32 = 3,
292
+ ncclInt64 = 4,
293
+ ncclUint64 = 5,
294
+ ncclFloat16 = 6, ncclHalf = 6,
295
+ ncclFloat32 = 7, ncclFloat = 7,
296
+ ncclFloat64 = 8, ncclDouble = 8,
297
+ ncclBfloat16 = 9,
298
+ ncclFloat8e4m3 = 10,
299
+ ncclFloat8e5m2 = 11,
300
+ ncclNumTypes = 12
301
+ } ncclDataType_t;
302
+
303
+ /* ncclScalarResidence_t: Location and dereferencing logic for scalar arguments. */
304
+ typedef enum {
305
+ /* ncclScalarDevice: The scalar is in device-visible memory and will be
306
+ * dereferenced while the collective is running. */
307
+ ncclScalarDevice = 0,
308
+
309
+ /* ncclScalarHostImmediate: The scalar is in host-visible memory and will be
310
+ * dereferenced before the ncclRedOpCreate***() function returns. */
311
+ ncclScalarHostImmediate = 1
312
+ } ncclScalarResidence_t;
313
+
314
+ /*
315
+ * ncclRedOpCreatePreMulSum
316
+ *
317
+ * Creates a new reduction operator which pre-multiplies input values by a given
318
+ * scalar locally before reducing them with peer values via summation. For use
319
+ * only with collectives launched against *comm* and *datatype*. The
320
+ * *residence* argument indicates how/when the memory pointed to by *scalar*
321
+ * will be dereferenced. Upon return, the newly created operator's handle
322
+ * is stored in *op*.
323
+ */
324
+ ncclResult_t ncclRedOpCreatePreMulSum(ncclRedOp_t *op, void *scalar, ncclDataType_t datatype, ncclScalarResidence_t residence, ncclComm_t comm);
325
+ ncclResult_t pncclRedOpCreatePreMulSum(ncclRedOp_t *op, void *scalar, ncclDataType_t datatype, ncclScalarResidence_t residence, ncclComm_t comm);
326
+
327
+ /*
328
+ * ncclRedOpDestroy
329
+ *
330
+ * Destroys the reduction operator *op*. The operator must have been created by
331
+ * ncclRedOpCreatePreMul with the matching communicator *comm*. An operator may be
332
+ * destroyed as soon as the last NCCL function which is given that operator returns.
333
+ */
334
+ ncclResult_t ncclRedOpDestroy(ncclRedOp_t op, ncclComm_t comm);
335
+ ncclResult_t pncclRedOpDestroy(ncclRedOp_t op, ncclComm_t comm);
336
+
337
+ /*
338
+ * Collective communication operations
339
+ *
340
+ * Collective communication operations must be called separately for each
341
+ * communicator in a communicator clique.
342
+ *
343
+ * They return when operations have been enqueued on the CUDA stream.
344
+ *
345
+ * Since they may perform inter-CPU synchronization, each call has to be done
346
+ * from a different thread or process, or need to use Group Semantics (see
347
+ * below).
348
+ */
349
+
350
+ /*
351
+ * Reduce
352
+ *
353
+ * Reduces data arrays of length count in sendbuff into recvbuff using op
354
+ * operation.
355
+ * recvbuff may be NULL on all calls except for root device.
356
+ * root is the rank (not the CUDA device) where data will reside after the
357
+ * operation is complete.
358
+ *
359
+ * In-place operation will happen if sendbuff == recvbuff.
360
+ */
361
+ ncclResult_t ncclReduce(const void* sendbuff, void* recvbuff, size_t count, ncclDataType_t datatype,
362
+ ncclRedOp_t op, int root, ncclComm_t comm, cudaStream_t stream);
363
+ ncclResult_t pncclReduce(const void* sendbuff, void* recvbuff, size_t count, ncclDataType_t datatype,
364
+ ncclRedOp_t op, int root, ncclComm_t comm, cudaStream_t stream);
365
+
366
+ /*
367
+ * (deprecated) Broadcast (in-place)
368
+ *
369
+ * Copies count values from root to all other devices.
370
+ * root is the rank (not the CUDA device) where data resides before the
371
+ * operation is started.
372
+ *
373
+ * This operation is implicitely in place.
374
+ */
375
+ ncclResult_t ncclBcast(void* buff, size_t count, ncclDataType_t datatype, int root,
376
+ ncclComm_t comm, cudaStream_t stream);
377
+ ncclResult_t pncclBcast(void* buff, size_t count, ncclDataType_t datatype, int root,
378
+ ncclComm_t comm, cudaStream_t stream);
379
+
380
+ /*
381
+ * Broadcast
382
+ *
383
+ * Copies count values from root to all other devices.
384
+ * root is the rank (not the CUDA device) where data resides before the
385
+ * operation is started.
386
+ *
387
+ * In-place operation will happen if sendbuff == recvbuff.
388
+ */
389
+ ncclResult_t ncclBroadcast(const void* sendbuff, void* recvbuff, size_t count, ncclDataType_t datatype, int root,
390
+ ncclComm_t comm, cudaStream_t stream);
391
+ ncclResult_t pncclBroadcast(const void* sendbuff, void* recvbuff, size_t count, ncclDataType_t datatype, int root,
392
+ ncclComm_t comm, cudaStream_t stream);
393
+
394
+ /*
395
+ * All-Reduce
396
+ *
397
+ * Reduces data arrays of length count in sendbuff using op operation, and
398
+ * leaves identical copies of result on each recvbuff.
399
+ *
400
+ * In-place operation will happen if sendbuff == recvbuff.
401
+ */
402
+ ncclResult_t ncclAllReduce(const void* sendbuff, void* recvbuff, size_t count,
403
+ ncclDataType_t datatype, ncclRedOp_t op, ncclComm_t comm, cudaStream_t stream);
404
+ ncclResult_t pncclAllReduce(const void* sendbuff, void* recvbuff, size_t count,
405
+ ncclDataType_t datatype, ncclRedOp_t op, ncclComm_t comm, cudaStream_t stream);
406
+
407
+ /*
408
+ * Reduce-Scatter
409
+ *
410
+ * Reduces data in sendbuff using op operation and leaves reduced result
411
+ * scattered over the devices so that recvbuff on rank i will contain the i-th
412
+ * block of the result.
413
+ * Assumes sendcount is equal to nranks*recvcount, which means that sendbuff
414
+ * should have a size of at least nranks*recvcount elements.
415
+ *
416
+ * In-place operations will happen if recvbuff == sendbuff + rank * recvcount.
417
+ */
418
+ ncclResult_t ncclReduceScatter(const void* sendbuff, void* recvbuff,
419
+ size_t recvcount, ncclDataType_t datatype, ncclRedOp_t op, ncclComm_t comm,
420
+ cudaStream_t stream);
421
+ ncclResult_t pncclReduceScatter(const void* sendbuff, void* recvbuff,
422
+ size_t recvcount, ncclDataType_t datatype, ncclRedOp_t op, ncclComm_t comm,
423
+ cudaStream_t stream);
424
+
425
+ /*
426
+ * All-Gather
427
+ *
428
+ * Each device gathers sendcount values from other GPUs into recvbuff,
429
+ * receiving data from rank i at offset i*sendcount.
430
+ * Assumes recvcount is equal to nranks*sendcount, which means that recvbuff
431
+ * should have a size of at least nranks*sendcount elements.
432
+ *
433
+ * In-place operations will happen if sendbuff == recvbuff + rank * sendcount.
434
+ */
435
+ ncclResult_t ncclAllGather(const void* sendbuff, void* recvbuff, size_t sendcount,
436
+ ncclDataType_t datatype, ncclComm_t comm, cudaStream_t stream);
437
+ ncclResult_t pncclAllGather(const void* sendbuff, void* recvbuff, size_t sendcount,
438
+ ncclDataType_t datatype, ncclComm_t comm, cudaStream_t stream);
439
+
440
+ /*
441
+ * All-to-All
442
+ *
443
+ * Each device sends count values to all other devices and receives count values
444
+ * from all other devices. Data to send to destination rank j is taken from
445
+ * sendbuff+j*count and data received from source rank i is placed at
446
+ * recvbuff+i*count.
447
+ */
448
+ ncclResult_t ncclAlltoAll(const void* sendbuff, void* recvbuff, size_t count,
449
+ ncclDataType_t datatype, ncclComm_t comm, cudaStream_t stream);
450
+ ncclResult_t pncclAlltoAll(const void* sendbuff, void* recvbuff, size_t count,
451
+ ncclDataType_t datatype, ncclComm_t comm, cudaStream_t stream);
452
+
453
+ /*
454
+ * Gather
455
+ *
456
+ * Each rank sends count elements from sendbuff to the root rank.
457
+ * On the root rank, data from rank i is placed at recvbuff + i*count.
458
+ * On non-root ranks, recvbuff is not used.
459
+ * root is the rank where data will be gathered.
460
+ *
461
+ * In-place operations will happen if sendbuff == recvbuff + root * count.
462
+ */
463
+ ncclResult_t ncclGather(const void* sendbuff, void* recvbuff, size_t count,
464
+ ncclDataType_t datatype, int root, ncclComm_t comm, cudaStream_t stream);
465
+ ncclResult_t pncclGather(const void* sendbuff, void* recvbuff, size_t count,
466
+ ncclDataType_t datatype, int root, ncclComm_t comm, cudaStream_t stream);
467
+
468
+ /*
469
+ * Scatter
470
+ *
471
+ * On the root rank, count elements from sendbuff+i*count are sent to rank i.
472
+ * On non-root ranks, sendbuff is not used.
473
+ * Each rank receives count elements into recvbuff.
474
+ * root is the rank that will distribute the data.
475
+ *
476
+ * In-place operations will happen if recvbuff == sendbuff + root * count.
477
+ */
478
+ ncclResult_t ncclScatter(const void* sendbuff, void* recvbuff, size_t count,
479
+ ncclDataType_t datatype, int root, ncclComm_t comm, cudaStream_t stream);
480
+ ncclResult_t pncclScatter(const void* sendbuff, void* recvbuff, size_t count,
481
+ ncclDataType_t datatype, int root, ncclComm_t comm, cudaStream_t stream);
482
+
483
+ /*
484
+ * Send
485
+ *
486
+ * Send data from sendbuff to rank peer.
487
+ *
488
+ * Rank peer needs to call ncclRecv with the same datatype and the same count from this
489
+ * rank.
490
+ *
491
+ * This operation is blocking for the GPU. If multiple ncclSend and ncclRecv operations
492
+ * need to progress concurrently to complete, they must be fused within a ncclGroupStart/
493
+ * ncclGroupEnd section.
494
+ */
495
+ ncclResult_t ncclSend(const void* sendbuff, size_t count, ncclDataType_t datatype, int peer,
496
+ ncclComm_t comm, cudaStream_t stream);
497
+ ncclResult_t pncclSend(const void* sendbuff, size_t count, ncclDataType_t datatype, int peer,
498
+ ncclComm_t comm, cudaStream_t stream);
499
+
500
+ /*
501
+ * Receive
502
+ *
503
+ * Receive data from rank peer into recvbuff.
504
+ *
505
+ * Rank peer needs to call ncclSend with the same datatype and the same count to this
506
+ * rank.
507
+ *
508
+ * This operation is blocking for the GPU. If multiple ncclSend and ncclRecv operations
509
+ * need to progress concurrently to complete, they must be fused within a ncclGroupStart/
510
+ * ncclGroupEnd section.
511
+ */
512
+ ncclResult_t pncclRecv(void* recvbuff, size_t count, ncclDataType_t datatype, int peer,
513
+ ncclComm_t comm, cudaStream_t stream);
514
+ ncclResult_t ncclRecv(void* recvbuff, size_t count, ncclDataType_t datatype, int peer,
515
+ ncclComm_t comm, cudaStream_t stream);
516
+
517
+ /*
518
+ * Group semantics
519
+ *
520
+ * When managing multiple GPUs from a single thread, and since NCCL collective
521
+ * calls may perform inter-CPU synchronization, we need to "group" calls for
522
+ * different ranks/devices into a single call.
523
+ *
524
+ * Grouping NCCL calls as being part of the same collective operation is done
525
+ * using ncclGroupStart and ncclGroupEnd. ncclGroupStart will enqueue all
526
+ * collective calls until the ncclGroupEnd call, which will wait for all calls
527
+ * to be complete. Note that for collective communication, ncclGroupEnd only
528
+ * guarantees that the operations are enqueued on the streams, not that
529
+ * the operation is effectively done.
530
+ *
531
+ * Both collective communication and ncclCommInitRank can be used in conjunction
532
+ * of ncclGroupStart/ncclGroupEnd, but not together.
533
+ *
534
+ * Group semantics also allow to fuse multiple operations on the same device
535
+ * to improve performance (for aggregated collective calls), or to permit
536
+ * concurrent progress of multiple send/receive operations.
537
+ */
538
+
539
+ /*
540
+ * Group Start
541
+ *
542
+ * Start a group call. All calls to NCCL until ncclGroupEnd will be fused into
543
+ * a single NCCL operation. Nothing will be started on the CUDA stream until
544
+ * ncclGroupEnd.
545
+ */
546
+ ncclResult_t ncclGroupStart();
547
+ ncclResult_t pncclGroupStart();
548
+
549
+ /*
550
+ * Group End
551
+ *
552
+ * End a group call. Start a fused NCCL operation consisting of all calls since
553
+ * ncclGroupStart. Operations on the CUDA stream depending on the NCCL operations
554
+ * need to be called after ncclGroupEnd.
555
+ */
556
+ ncclResult_t ncclGroupEnd();
557
+ ncclResult_t pncclGroupEnd();
558
+
559
+ /*
560
+ * Group Simulate End
561
+ *
562
+ * Simulate a ncclGroupEnd() call and return NCCL's simulation info in a struct.
563
+ */
564
+ ncclResult_t ncclGroupSimulateEnd(ncclSimInfo_t* simInfo);
565
+ ncclResult_t pncclGroupSimulateEnd(ncclSimInfo_t* simInfo);
566
+
567
+ #ifdef __cplusplus
568
+ } // end extern "C"
569
+ #endif
570
+
571
+ #endif // end include guard
zonos2/kernel/csrc/include/zonos2/tensor.h ADDED
@@ -0,0 +1,496 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ #include <zonos2/utils.h>
3
+
4
+ #include <dlpack/dlpack.h>
5
+ #include <tvm/ffi/container/tensor.h>
6
+ #include <tvm/ffi/dtype.h>
7
+
8
+ #include <algorithm>
9
+ #include <array>
10
+ #include <concepts>
11
+ #include <cstddef>
12
+ #include <cstdint>
13
+ #include <initializer_list>
14
+ #include <optional>
15
+ #include <ranges>
16
+ #include <source_location>
17
+ #include <span>
18
+ #include <sstream>
19
+ #include <string>
20
+ #include <string_view>
21
+ #include <type_traits>
22
+ #include <utility>
23
+
24
+ namespace host {
25
+
26
+ namespace stdr = std::ranges;
27
+ namespace stdv = std::views;
28
+
29
+ namespace details {
30
+
31
+ struct SizeRef;
32
+ struct DTypeRef;
33
+ struct DeviceRef;
34
+
35
+ inline constexpr auto kAnyDeviceID = -1;
36
+ inline constexpr auto kAnySize = static_cast<int64_t>(-1);
37
+ inline constexpr auto kNullSize = static_cast<int64_t>(0);
38
+ inline constexpr auto kNullDType = static_cast<DLDataTypeCode>(18u);
39
+ inline constexpr auto kNullDevice = static_cast<DLDeviceType>(-1);
40
+
41
+ template <typename T> struct dtype_trait {};
42
+
43
+ template <std::integral T> struct dtype_trait<T> {
44
+ inline static constexpr auto value =
45
+ DLDataType{.code = std::is_signed_v<T> ? DLDataTypeCode::kDLInt
46
+ : DLDataTypeCode::kDLUInt,
47
+ .bits = static_cast<std::uint8_t>(sizeof(T) * 8),
48
+ .lanes = 1};
49
+ };
50
+
51
+ template <std::floating_point T> struct dtype_trait<T> {
52
+ inline static constexpr auto value =
53
+ DLDataType{.code = DLDataTypeCode::kDLFloat,
54
+ .bits = static_cast<std::uint8_t>(sizeof(T) * 8),
55
+ .lanes = 1};
56
+ };
57
+
58
+ template <DLDeviceType Code> struct device_trait {
59
+ inline static constexpr auto value =
60
+ DLDevice{.device_type = Code, .device_id = kAnyDeviceID};
61
+ };
62
+
63
+ template <typename... Ts>
64
+ inline constexpr auto kDTypeList =
65
+ std::array<DLDataType, sizeof...(Ts)>{dtype_trait<Ts>::value...};
66
+
67
+ template <DLDeviceType... Codes>
68
+ inline constexpr auto kDeviceList =
69
+ std::array<DLDevice, sizeof...(Codes)>{device_trait<Codes>::value...};
70
+
71
+ template <typename T> struct PrintAbleSpan {
72
+ explicit PrintAbleSpan(std::span<const T> data) : data(data) {}
73
+ std::span<const T> data;
74
+ };
75
+
76
+ // define DLDataType comparison and printing in root namespace
77
+ inline constexpr auto kDeviceStringMap = [] {
78
+ constexpr auto map =
79
+ std::array<std::pair<DLDeviceType, std::string_view>, 16>{
80
+ std::pair{DLDeviceType::kDLCPU, "cpu"},
81
+ std::pair{DLDeviceType::kDLCUDA, "cuda"},
82
+ std::pair{DLDeviceType::kDLCUDAHost, "cuda_host"},
83
+ std::pair{DLDeviceType::kDLOpenCL, "opencl"},
84
+ std::pair{DLDeviceType::kDLVulkan, "vulkan"},
85
+ std::pair{DLDeviceType::kDLMetal, "metal"},
86
+ std::pair{DLDeviceType::kDLVPI, "vpi"},
87
+ std::pair{DLDeviceType::kDLROCM, "rocm"},
88
+ std::pair{DLDeviceType::kDLROCMHost, "rocm_host"},
89
+ std::pair{DLDeviceType::kDLExtDev, "ext_dev"},
90
+ std::pair{DLDeviceType::kDLCUDAManaged, "cuda_managed"},
91
+ std::pair{DLDeviceType::kDLOneAPI, "oneapi"},
92
+ std::pair{DLDeviceType::kDLWebGPU, "webgpu"},
93
+ std::pair{DLDeviceType::kDLHexagon, "hexagon"},
94
+ std::pair{DLDeviceType::kDLMAIA, "maia"},
95
+ std::pair{DLDeviceType::kDLTrn, "trn"},
96
+ };
97
+ constexpr auto max_type = stdr::max(map | stdv::keys);
98
+ auto result = std::array<std::string_view, max_type + 1>{};
99
+ for (const auto &[code, name] : map) {
100
+ result[static_cast<std::size_t>(code)] = name;
101
+ }
102
+ return result;
103
+ }();
104
+
105
+ struct PrintableDevice {
106
+ DLDevice device;
107
+ };
108
+
109
+ inline auto &operator<<(std::ostream &os, DLDevice device) {
110
+ const auto &mapping = kDeviceStringMap;
111
+ const auto entry = static_cast<std::size_t>(device.device_type);
112
+ RuntimeCheck(entry < mapping.size());
113
+ const auto name = mapping[entry];
114
+ RuntimeCheck(!name.empty(), "Unknown device: ", int(device.device_type));
115
+ os << name;
116
+ if (device.device_id != kAnyDeviceID)
117
+ os << "[" << device.device_id << "]";
118
+ return os;
119
+ }
120
+
121
+ inline auto &operator<<(std::ostream &os, PrintableDevice pd) {
122
+ return os << pd.device;
123
+ }
124
+
125
+ template <typename T>
126
+ inline auto &operator<<(std::ostream &os, PrintAbleSpan<T> span) {
127
+ os << "[";
128
+ for (const auto i : stdv::iota(std::size_t{0}, span.data.size())) {
129
+ if (i > 0) {
130
+ os << ", ";
131
+ }
132
+ os << span.data[i];
133
+ }
134
+ os << "]";
135
+ return os;
136
+ }
137
+
138
+ } // namespace details
139
+
140
+ struct SymbolicSize {
141
+ public:
142
+ SymbolicSize(std::string_view annotation = {})
143
+ : m_value(details::kNullSize), m_annotation(annotation) {}
144
+
145
+ auto get_name() const -> std::string_view { return m_annotation; }
146
+ auto set_value(int64_t value) -> void {
147
+ RuntimeCheck(!this->has_value(), "Size value already set");
148
+ m_value = value;
149
+ }
150
+ auto has_value() const -> bool { return m_value != 0; }
151
+ auto get_value() const -> std::optional<int64_t> {
152
+ return this->has_value() ? std::optional{m_value} : std::nullopt;
153
+ }
154
+ auto unwrap() const -> int64_t {
155
+ RuntimeCheck(this->has_value(), "Size value is not set");
156
+ return m_value;
157
+ }
158
+
159
+ SymbolicSize(const SymbolicSize &) = delete;
160
+ SymbolicSize &operator=(const SymbolicSize &) = delete;
161
+
162
+ auto verify(int64_t size, const char *prefix, int64_t dim) -> void {
163
+ if (this->has_value()) {
164
+ if (m_value != size) {
165
+ [[unlikely]];
166
+ Panic("Size mismatch for ", m_name(prefix, dim), ": expected ", m_value,
167
+ " but got ", size);
168
+ }
169
+ } else {
170
+ this->set_value(size);
171
+ }
172
+ }
173
+
174
+ auto value_or_name(const char *prefix, int64_t dim) const -> std::string {
175
+ if (const auto value = this->get_value()) {
176
+ return std::to_string(*value);
177
+ } else {
178
+ return m_name(prefix, dim);
179
+ }
180
+ }
181
+
182
+ private:
183
+ auto m_name(const char *prefix, int64_t dim) const -> std::string {
184
+ const auto annotation = this->get_name();
185
+ std::ostringstream os;
186
+ if (annotation.empty()) {
187
+ os << prefix << '#' << dim;
188
+ } else {
189
+ os << annotation << '(' << prefix << '#' << dim << ')';
190
+ }
191
+ return std::move(os).str();
192
+ }
193
+
194
+ std::int64_t m_value;
195
+ std::string_view m_annotation;
196
+ };
197
+
198
+ inline auto operator==(DLDevice lhs, DLDevice rhs) -> bool {
199
+ return lhs.device_type == rhs.device_type && lhs.device_id == rhs.device_id;
200
+ }
201
+
202
+ struct SymbolicDType {
203
+ public:
204
+ SymbolicDType() : m_value({details::kNullDType, 0, 0}) {}
205
+
206
+ auto set_value(DLDataType value) -> void {
207
+ RuntimeCheck(!this->has_value(), "Dtype value already set");
208
+ RuntimeCheck(
209
+ m_check(value), "Dtype value [", value,
210
+ "] not in the allowed options: ", details::PrintAbleSpan{m_options});
211
+ m_value = value;
212
+ }
213
+ auto has_value() const -> bool { return m_value.code != details::kNullDType; }
214
+ auto get_value() const -> std::optional<DLDataType> {
215
+ return this->has_value() ? std::optional{m_value} : std::nullopt;
216
+ }
217
+ auto unwrap() const -> DLDataType {
218
+ RuntimeCheck(this->has_value(), "Dtype value is not set");
219
+ return m_value;
220
+ }
221
+
222
+ auto set_options(std::span<const DLDataType> options) -> void {
223
+ m_options = options;
224
+ }
225
+ template <typename... Ts> auto set_options() -> void {
226
+ m_options = details::kDTypeList<Ts...>;
227
+ }
228
+
229
+ auto verify(DLDataType dtype) -> void {
230
+ if (this->has_value()) {
231
+ RuntimeCheck(m_value == dtype, "DType mismatch: expected ", m_value,
232
+ " but got ", dtype);
233
+ } else {
234
+ this->set_value(dtype);
235
+ }
236
+ }
237
+
238
+ private:
239
+ auto m_check(DLDataType value) const -> bool {
240
+ return stdr::empty(m_options) ||
241
+ (stdr::find(m_options, value) != stdr::end(m_options));
242
+ }
243
+
244
+ std::span<const DLDataType> m_options;
245
+ DLDataType m_value;
246
+ };
247
+
248
+ struct SymbolicDevice {
249
+ public:
250
+ SymbolicDevice() : m_value({details::kNullDevice, details::kAnyDeviceID}) {}
251
+
252
+ auto set_value(DLDevice value) -> void {
253
+ RuntimeCheck(!this->has_value(), "Device value already set");
254
+ RuntimeCheck(
255
+ m_check(value), "Device value [", details::PrintableDevice{value},
256
+ "] not in the allowed options: ", details::PrintAbleSpan{m_options});
257
+ m_value = value;
258
+ }
259
+ auto has_value() const -> bool {
260
+ return m_value.device_type != details::kNullDevice;
261
+ }
262
+ auto get_value() const -> std::optional<DLDevice> {
263
+ return this->has_value() ? std::optional{m_value} : std::nullopt;
264
+ }
265
+ auto unwrap() const -> DLDevice {
266
+ RuntimeCheck(this->has_value(), "Device value is not set");
267
+ return m_value;
268
+ }
269
+
270
+ auto set_options(std::span<const DLDevice> options) -> void {
271
+ m_options = options;
272
+ }
273
+ template <DLDeviceType... Codes> auto set_options() -> void {
274
+ m_options = details::kDeviceList<Codes...>;
275
+ }
276
+
277
+ auto verify(DLDevice device) -> void {
278
+ if (this->has_value()) {
279
+ RuntimeCheck(m_value == device, "Device mismatch: expected ",
280
+ details::PrintableDevice{m_value}, " but got ",
281
+ details::PrintableDevice{device});
282
+ } else {
283
+ this->set_value(device);
284
+ }
285
+ }
286
+
287
+ private:
288
+ auto m_check(DLDevice value) const -> bool {
289
+ return stdr::empty(m_options) ||
290
+ (stdr::any_of(m_options, [value](const DLDevice &opt) {
291
+ // device type must exactly match
292
+ if (opt.device_type != value.device_type)
293
+ return false;
294
+ // device id can be wildcarded
295
+ return opt.device_id == details::kAnyDeviceID ||
296
+ opt.device_id == value.device_id;
297
+ }));
298
+ }
299
+
300
+ std::span<const DLDevice> m_options;
301
+ DLDevice m_value;
302
+ };
303
+
304
+ struct TensorMatcher;
305
+
306
+ namespace details {
307
+
308
+ template <typename T> struct BaseRef {
309
+ public:
310
+ BaseRef(const BaseRef &) = delete;
311
+ BaseRef &operator=(const BaseRef &) = delete;
312
+
313
+ auto operator->() const -> T * { return m_ref; }
314
+ auto operator*() const -> T & { return *m_ref; }
315
+ auto rebind(T &other) -> void { m_ref = &other; }
316
+
317
+ explicit BaseRef() : m_ref(&m_cache), m_cache() {}
318
+ BaseRef(T &size) : m_ref(&size), m_cache() {}
319
+
320
+ private:
321
+ T *m_ref;
322
+ T m_cache;
323
+ };
324
+
325
+ struct SizeRef : BaseRef<SymbolicSize> {
326
+ using BaseRef::BaseRef;
327
+ SizeRef(int64_t value) {
328
+ if (value != kAnySize) {
329
+ (**this).set_value(value);
330
+ } else {
331
+ // otherwise, we can match any size
332
+ }
333
+ }
334
+ };
335
+
336
+ struct DTypeRef : BaseRef<SymbolicDType> {
337
+ using BaseRef::BaseRef;
338
+ DTypeRef(DLDataType options) { (**this).set_value(options); }
339
+ DTypeRef(std::initializer_list<DLDataType> options) {
340
+ (**this).set_options(options);
341
+ }
342
+ DTypeRef(std::span<const DLDataType> options) {
343
+ (**this).set_options(options);
344
+ }
345
+ };
346
+
347
+ struct DeviceRef : BaseRef<SymbolicDevice> {
348
+ using BaseRef::BaseRef;
349
+ DeviceRef(DLDevice options) { (**this).set_value(options); }
350
+ DeviceRef(std::initializer_list<DLDevice> options) {
351
+ (**this).set_options(options);
352
+ }
353
+ DeviceRef(std::span<const DLDevice> options) {
354
+ (**this).set_options(options);
355
+ }
356
+ };
357
+
358
+ } // namespace details
359
+
360
+ struct TensorMatcher {
361
+ private:
362
+ using SizeRef = details::SizeRef;
363
+ using DTypeRef = details::DTypeRef;
364
+ using DeviceRef = details::DeviceRef;
365
+ using Loc_t = std::source_location;
366
+
367
+ public:
368
+ TensorMatcher(const TensorMatcher &) = delete;
369
+ TensorMatcher &operator=(const TensorMatcher &) = delete;
370
+
371
+ explicit TensorMatcher(std::initializer_list<SizeRef> shape)
372
+ : m_shape(shape), m_strides(), m_dtype() {}
373
+
374
+ auto with_strides(std::initializer_list<SizeRef> strides) && //
375
+ -> TensorMatcher && {
376
+ // no partial update allowed
377
+ RuntimeCheck(m_strides.size() == 0, "Strides already specified");
378
+ RuntimeCheck(m_shape.size() == strides.size(),
379
+ "Strides size must match shape size");
380
+ m_strides = strides;
381
+ return std::move(*this);
382
+ }
383
+
384
+ template <typename... Ts>
385
+ auto with_dtype(DTypeRef &&dtype) && -> TensorMatcher && {
386
+ m_init_dtype();
387
+ m_dtype.rebind(*dtype);
388
+ return std::move(*this);
389
+ }
390
+
391
+ template <typename... Ts>
392
+ requires(sizeof...(Ts) > 0)
393
+ auto with_dtype() && -> TensorMatcher && {
394
+ m_init_dtype();
395
+ m_dtype->set_options<Ts...>();
396
+ return std::move(*this);
397
+ }
398
+
399
+ template <DLDeviceType... Codes>
400
+ auto with_device(DeviceRef &&device) && -> TensorMatcher && {
401
+ m_init_device();
402
+ m_device.rebind(*device);
403
+ return std::move(*this);
404
+ }
405
+
406
+ template <DLDeviceType... Codes>
407
+ requires(sizeof...(Codes) > 0)
408
+ auto with_device() && -> TensorMatcher && {
409
+ m_init_device();
410
+ m_device->set_options<Codes...>();
411
+ return std::move(*this);
412
+ }
413
+
414
+ // once we start verification, we cannot modify anymore
415
+ auto verify(tvm::ffi::TensorView view,
416
+ Loc_t loc = Loc_t::current()) const && -> const TensorMatcher && {
417
+ try {
418
+ this->m_verify_impl(view);
419
+ } catch (PanicError &e) {
420
+ auto oss = std::ostringstream{};
421
+ oss << "Tensor match failed for " << this->debug_str() << " at "
422
+ << loc.file_name() << ":" << loc.line()
423
+ << "\n- Root cause: " << e.detail();
424
+ throw PanicError(std::move(oss).str());
425
+ }
426
+ return std::move(*this);
427
+ }
428
+
429
+ auto debug_str() const -> std::string {
430
+ auto oss = std::ostringstream{};
431
+ oss << "Tensor<";
432
+ std::size_t dim = 0;
433
+ for (const auto &size_ref : m_shape) {
434
+ if (dim > 0) {
435
+ oss << ", ";
436
+ }
437
+ oss << size_ref->value_or_name("shape", dim++);
438
+ }
439
+ oss << ">";
440
+ if (m_strides.size() > 0) {
441
+ oss << " [strides=<";
442
+ dim = 0;
443
+ for (const auto &stride_ref : m_strides) {
444
+ if (dim > 0) {
445
+ oss << ", ";
446
+ }
447
+ oss << stride_ref->value_or_name("stride", dim++);
448
+ }
449
+ oss << ">]";
450
+ }
451
+ return std::move(oss).str();
452
+ }
453
+
454
+ private:
455
+ auto m_verify_impl(tvm::ffi::TensorView view) const -> void {
456
+ const auto dim = static_cast<std::size_t>(view.dim());
457
+ RuntimeCheck(dim == m_shape.size(), "Tensor dimension mismatch: expected ",
458
+ m_shape.size(), " but got ", dim);
459
+ for (const auto i : stdv::iota(std::size_t{0}, dim)) {
460
+ m_shape[i]->verify(view.size(i), "shape", i);
461
+ }
462
+ if (this->m_has_strides()) {
463
+ for (const auto i : stdv::iota(std::size_t{0}, dim)) {
464
+ if (view.size(i) != 1 || !m_strides[i]->has_value()) {
465
+ // skip stride check for size 1 dimension
466
+ m_strides[i]->verify(view.stride(i), "stride", i);
467
+ }
468
+ }
469
+ } else {
470
+ RuntimeCheck(view.is_contiguous(),
471
+ "Tensor is not contiguous as expected");
472
+ }
473
+ // since we may double verify, we will force to check
474
+ m_dtype->verify(view.dtype());
475
+ m_device->verify(view.device());
476
+ }
477
+
478
+ auto m_init_dtype() -> void {
479
+ RuntimeCheck(!m_has_dtype, "DType already specified");
480
+ m_has_dtype = true;
481
+ }
482
+ auto m_init_device() -> void {
483
+ RuntimeCheck(!m_has_device, "Device already specified");
484
+ m_has_device = true;
485
+ }
486
+ auto m_has_strides() const -> bool { return !m_strides.empty(); }
487
+
488
+ std::span<const SizeRef> m_shape;
489
+ std::span<const SizeRef> m_strides;
490
+ DTypeRef m_dtype;
491
+ DeviceRef m_device;
492
+ bool m_has_dtype = false;
493
+ bool m_has_device = false;
494
+ };
495
+
496
+ } // namespace host
zonos2/kernel/csrc/include/zonos2/utils.cuh ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+
3
+ #include <zonos2/utils.h>
4
+
5
+ #include <dlpack/dlpack.h>
6
+ #include <tvm/ffi/extra/c_env_api.h>
7
+
8
+ #include <concepts>
9
+ #include <cstddef>
10
+ #include <source_location>
11
+ #include <type_traits>
12
+
13
+ namespace device {
14
+
15
+ inline constexpr auto kWarpThreads = 32u;
16
+
17
+ template <std::integral T, std::integral U>
18
+ __always_inline __device__ constexpr auto div_ceil(T a, U b) {
19
+ return (a + b - 1) / b;
20
+ }
21
+
22
+ namespace pointer {
23
+
24
+ // we only allow void * pointer arithmetic for safety
25
+
26
+ template <typename T, std::integral... U>
27
+ __always_inline __device__ auto offset(T *ptr, U... offset) -> void * {
28
+ static_assert(std::is_same_v<T, void>,
29
+ "Pointer arithmetic is only allowed for void* pointers");
30
+ return static_cast<char *>(ptr) + (... + offset);
31
+ }
32
+
33
+ template <typename T, std::integral... U>
34
+ __always_inline __device__ auto offset(const T *ptr, U... offset) -> const
35
+ void * {
36
+ static_assert(std::is_same_v<T, void>,
37
+ "Pointer arithmetic is only allowed for void* pointers");
38
+ return static_cast<const char *>(ptr) + (... + offset);
39
+ }
40
+
41
+ } // namespace pointer
42
+
43
+ namespace PDL {
44
+
45
+ template <bool kUsePDL> __always_inline __device__ void wait() {
46
+ if constexpr (kUsePDL) {
47
+ asm volatile("griddepcontrol.wait;" ::: "memory");
48
+ }
49
+ }
50
+
51
+ template <bool kUsePDL> __always_inline __device__ void launch() {
52
+ if constexpr (kUsePDL) {
53
+ asm volatile("griddepcontrol.launch_dependents;" :::);
54
+ }
55
+ }
56
+
57
+ } // namespace PDL
58
+
59
+ } // namespace device
60
+
61
+ namespace host {
62
+
63
+ inline auto
64
+ CUDA_CHECK(::cudaError_t error,
65
+ std::source_location location = std::source_location::current())
66
+ -> void {
67
+ if (error != ::cudaSuccess) {
68
+ [[unlikely]];
69
+ ::host::panic(location, "CUDA error: ", ::cudaGetErrorString(error));
70
+ }
71
+ }
72
+
73
+ inline auto
74
+ CUDA_CHECK(std::source_location location = std::source_location::current())
75
+ -> void {
76
+ return CUDA_CHECK(::cudaGetLastError(), location);
77
+ }
78
+
79
+ template <auto F> inline void set_smem_once(std::size_t smem_size) {
80
+ static const auto last_smem_size = [&] {
81
+ CUDA_CHECK(::cudaFuncSetAttribute(
82
+ F, ::cudaFuncAttributeMaxDynamicSharedMemorySize, smem_size));
83
+ return smem_size;
84
+ }();
85
+ RuntimeCheck(
86
+ smem_size <= last_smem_size,
87
+ "Dynamic shared memory size exceeds the previously set maximum size: ",
88
+ last_smem_size, " bytes");
89
+ }
90
+
91
+ struct LaunchKernel {
92
+ public:
93
+ explicit LaunchKernel(dim3 grid_dim, dim3 block_dim, DLDevice device,
94
+ std::size_t dynamic_shared_mem_bytes = 0) noexcept
95
+ : m_config(s_make_config(grid_dim, block_dim, resolve_device(device),
96
+ dynamic_shared_mem_bytes)) {}
97
+
98
+ explicit LaunchKernel(dim3 grid_dim, dim3 block_dim, cudaStream_t stream,
99
+ std::size_t dynamic_shared_mem_bytes = 0) noexcept
100
+ : m_config(s_make_config(grid_dim, block_dim, stream,
101
+ dynamic_shared_mem_bytes)) {}
102
+
103
+ static auto resolve_device(DLDevice device) -> cudaStream_t {
104
+ return static_cast<cudaStream_t>(
105
+ ::TVMFFIEnvGetStream(device.device_type, device.device_id));
106
+ }
107
+
108
+ LaunchKernel(const LaunchKernel &) = delete;
109
+ LaunchKernel &operator=(const LaunchKernel &) = delete;
110
+
111
+ template <typename T, typename... Args>
112
+ auto operator()(T &&kernel, Args &&...args) const -> void {
113
+ CUDA_CHECK(
114
+ ::cudaLaunchKernelEx(&m_config, kernel, std::forward<Args>(args)...));
115
+ }
116
+
117
+ auto with_attr(bool use_pdl) -> LaunchKernel & {
118
+ if (use_pdl) {
119
+ m_attr_cache.id = ::cudaLaunchAttributeProgrammaticStreamSerialization;
120
+ m_attr_cache.val.programmaticStreamSerializationAllowed = 1;
121
+ m_config.attrs = &m_attr_cache;
122
+ m_config.numAttrs = 1;
123
+ } else {
124
+ m_config.numAttrs = 0;
125
+ }
126
+ return *this;
127
+ }
128
+
129
+ private:
130
+ static auto s_make_config(dim3 grid_dim, dim3 block_dim, cudaStream_t stream,
131
+ std::size_t smem) -> cudaLaunchConfig_t {
132
+ auto config = ::cudaLaunchConfig_t{};
133
+ config.gridDim = grid_dim;
134
+ config.blockDim = block_dim;
135
+ config.dynamicSmemBytes = smem;
136
+ config.stream = stream;
137
+ config.numAttrs = 0;
138
+ return config;
139
+ }
140
+ cudaLaunchConfig_t m_config;
141
+ cudaLaunchAttribute m_attr_cache;
142
+ };
143
+
144
+ } // namespace host
zonos2/kernel/csrc/include/zonos2/utils.h ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+
3
+ // ref:
4
+ // https://forums.developer.nvidia.com/t/c-20s-source-location-compilation-error-when-using-nvcc-12-1/258026/3
5
+ #ifdef __CUDACC__
6
+ #pragma push_macro("__cpp_consteval")
7
+ #pragma push_macro("_NODISCARD")
8
+ #pragma push_macro("__builtin_LINE")
9
+
10
+ #pragma clang diagnostic push
11
+ #pragma clang diagnostic ignored "-Wbuiltin-macro-redefined"
12
+ #define __cpp_consteval 201811L
13
+ #pragma clang diagnostic pop
14
+
15
+ #ifdef _NODISCARD
16
+ #undef _NODISCARD
17
+ #define _NODISCARD
18
+ #endif
19
+
20
+ #define consteval constexpr
21
+
22
+ #include <source_location>
23
+
24
+ #undef consteval
25
+ #pragma pop_macro("__cpp_consteval")
26
+ #pragma pop_macro("_NODISCARD")
27
+ #else
28
+ #include <source_location>
29
+ #endif
30
+
31
+ #include <dlpack/dlpack.h>
32
+
33
+ #include <concepts>
34
+ #include <ostream>
35
+ #include <sstream>
36
+ #include <utility>
37
+
38
+ namespace host {
39
+
40
+ struct PanicError : public std::runtime_error {
41
+ public:
42
+ // copy and move constructors
43
+ PanicError(std::string msg) : runtime_error(msg), m_message(std::move(msg)) {}
44
+ auto detail() const -> std::string_view {
45
+ const auto sv = std::string_view{m_message};
46
+ const auto pos = sv.find(": ");
47
+ return pos == std::string_view::npos ? sv : sv.substr(pos + 2);
48
+ }
49
+
50
+ private:
51
+ std::string m_message;
52
+ };
53
+
54
+ template <typename... Args>
55
+ [[noreturn]]
56
+ inline auto panic(std::source_location location, Args &&...args) -> void {
57
+ std::ostringstream os;
58
+ os << "Runtime check failed at " << location.file_name() << ":"
59
+ << location.line();
60
+ if constexpr (sizeof...(args) > 0) {
61
+ os << ": ";
62
+ (os << ... << std::forward<Args>(args));
63
+ } else {
64
+ os << " in " << location.function_name();
65
+ }
66
+ throw PanicError(std::move(os).str());
67
+ }
68
+
69
+ template <typename... Args> struct Panic {
70
+ explicit Panic(Args &&...args, std::source_location location =
71
+ std::source_location::current()) {
72
+ [[unlikely]];
73
+ ::host::panic(location, std::forward<Args>(args)...);
74
+ }
75
+ [[noreturn]] ~Panic() { std::terminate(); }
76
+ };
77
+
78
+ template <typename... Args> struct RuntimeCheck {
79
+ template <typename T>
80
+ explicit RuntimeCheck(
81
+ T &&condition, Args &&...args,
82
+ std::source_location location = std::source_location::current()) {
83
+ if (!condition) {
84
+ [[unlikely]];
85
+ ::host::panic(location, std::forward<Args>(args)...);
86
+ }
87
+ }
88
+ };
89
+
90
+ template <typename T, typename... Args>
91
+ explicit RuntimeCheck(T &&, Args &&...) -> RuntimeCheck<Args...>;
92
+
93
+ template <typename... Args> explicit Panic(Args &&...) -> Panic<Args...>;
94
+
95
+ template <std::integral T, std::integral U>
96
+ inline constexpr auto div_ceil(T a, U b) {
97
+ return (a + b - 1) / b;
98
+ }
99
+
100
+ inline auto dtype_bytes(DLDataType dtype) -> std::size_t {
101
+ return static_cast<std::size_t>(dtype.bits / 8);
102
+ }
103
+
104
+ namespace pointer {
105
+
106
+ template <typename T, std::integral... U>
107
+ inline auto offset(T *ptr, U... offset) -> void * {
108
+ static_assert(std::is_same_v<T, void>,
109
+ "Pointer arithmetic is only allowed for void* pointers");
110
+ return static_cast<char *>(ptr) + (... + offset);
111
+ }
112
+
113
+ template <typename T, std::integral... U>
114
+ inline auto offset(const T *ptr, U... offset) -> const void * {
115
+ static_assert(std::is_same_v<T, void>,
116
+ "Pointer arithmetic is only allowed for void* pointers");
117
+ return static_cast<const char *>(ptr) + (... + offset);
118
+ }
119
+
120
+ } // namespace pointer
121
+
122
+ } // namespace host
zonos2/kernel/csrc/include/zonos2/warp.cuh ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ #include <zonos2/utils.cuh>
3
+
4
+ #include <sys/cdefs.h>
5
+
6
+ #include <cstddef>
7
+
8
+ namespace device::warp {
9
+
10
+ namespace details {
11
+
12
+ template <std::size_t kUnit> inline constexpr auto get_mem_package() {
13
+ if constexpr (kUnit == 16) {
14
+ return uint4{};
15
+ } else if constexpr (kUnit == 8) {
16
+ return uint2{};
17
+ } else if constexpr (kUnit == 4) {
18
+ return uint1{};
19
+ } else {
20
+ static_assert(kUnit == 16 || kUnit == 8 || kUnit == 4,
21
+ "Unsupported memory package size");
22
+ }
23
+ }
24
+
25
+ inline constexpr auto resolve_unit_size(std::size_t x) -> std::size_t {
26
+ if (x % (16 * kWarpThreads) == 0)
27
+ return 16;
28
+ if (x % (8 * kWarpThreads) == 0)
29
+ return 8;
30
+ if (x % (4 * kWarpThreads) == 0)
31
+ return 4;
32
+ return 0; // trigger static assert in _get_mem_package
33
+ }
34
+
35
+ template <std::size_t kBytes, std::size_t kUnit>
36
+ using mem_package_t = decltype(get_mem_package<kUnit>());
37
+
38
+ } // namespace details
39
+
40
+ template <std::size_t kBytes,
41
+ std::size_t kUnit = details::resolve_unit_size(kBytes)>
42
+ __always_inline __device__ void copy(void *__restrict__ dst,
43
+ const void *__restrict__ src) {
44
+ using Package = details::mem_package_t<kBytes, kUnit>;
45
+ constexpr auto kBytesPerLoop = sizeof(Package) * kWarpThreads;
46
+ constexpr auto kLoopCount = kBytes / kBytesPerLoop;
47
+ static_assert(kBytes % kBytesPerLoop == 0,
48
+ "kBytes must be multiple of 128 bytes");
49
+
50
+ const auto dst_packed = static_cast<Package *>(dst);
51
+ const auto src_packed = static_cast<const Package *>(src);
52
+ const auto lane_id = threadIdx.x % kWarpThreads;
53
+
54
+ #pragma unroll kLoopCount
55
+ for (std::size_t i = 0; i < kLoopCount; ++i) {
56
+ const auto j = i * kWarpThreads + lane_id;
57
+ dst_packed[j] = src_packed[j];
58
+ }
59
+ }
60
+
61
+ template <std::size_t kBytes,
62
+ std::size_t kUnit = details::resolve_unit_size(kBytes)>
63
+ __always_inline __device__ void reset(void *__restrict__ dst) {
64
+ using Package = details::mem_package_t<kBytes, kUnit>;
65
+ constexpr auto kBytesPerLoop = sizeof(Package) * kWarpThreads;
66
+ constexpr auto kLoopCount = kBytes / kBytesPerLoop;
67
+ static_assert(kBytes % kBytesPerLoop == 0,
68
+ "warp_copy: kBytes must be multiple of 128 bytes");
69
+
70
+ const auto dst_ = static_cast<Package *>(dst);
71
+ const auto lane_id = threadIdx.x % kWarpThreads;
72
+ const auto zero_value = Package{};
73
+
74
+ #pragma unroll kLoopCount
75
+ for (std::size_t i = 0; i < kLoopCount; ++i) {
76
+ dst_[i * kWarpThreads + lane_id] = zero_value;
77
+ }
78
+ }
79
+
80
+ } // namespace device::warp
zonos2/kernel/csrc/jit/index.cu ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include <zonos2/tensor.h>
2
+ #include <zonos2/utils.cuh>
3
+ #include <zonos2/utils.h>
4
+ #include <zonos2/warp.cuh>
5
+
6
+ #include <dlpack/dlpack.h>
7
+ #include <tvm/ffi/container/array.h>
8
+ #include <tvm/ffi/container/tensor.h>
9
+ #include <tvm/ffi/container/tuple.h>
10
+
11
+ #include <bit>
12
+ #include <concepts>
13
+ #include <cstddef>
14
+ #include <cstdint>
15
+
16
+ namespace {
17
+
18
+ struct IndexKernelParams {
19
+ void *__restrict__ output;
20
+ const void *__restrict__ weight;
21
+ const void *__restrict__ indice;
22
+ std::size_t num_warps;
23
+ };
24
+
25
+ struct MaskedKernelParams {
26
+ IndexKernelParams params;
27
+ std::size_t start;
28
+ std::size_t length;
29
+ };
30
+
31
+ template <std::size_t kNumThreads, std::size_t kMaxOccupancy, bool kUsePDL,
32
+ std::size_t kElementSize, std::size_t kNumSplits, std::integral T>
33
+ __global__ __launch_bounds__(kNumThreads, kMaxOccupancy) void //
34
+ index_kernel(const __grid_constant__ IndexKernelParams params) {
35
+ using namespace device;
36
+ constexpr auto kSize = kElementSize;
37
+ constexpr auto kSizePerWarp = kSize / kNumSplits;
38
+ constexpr auto kWarpPerBlock = static_cast<unsigned>(kNumThreads / 32);
39
+
40
+ static_assert(kNumThreads % 32 == 0);
41
+ static_assert(std::has_single_bit(kNumSplits));
42
+ static_assert(kElementSize % kNumSplits == 0);
43
+
44
+ const auto &[output, weight, indices_, num_warps] = params;
45
+ const auto indices = static_cast<const T *>(indices_);
46
+ const auto warp_id =
47
+ (threadIdx.x / kWarpThreads) + blockIdx.x * kWarpPerBlock;
48
+ PDL::wait<kUsePDL>();
49
+
50
+ if (warp_id < num_warps) {
51
+ const auto pos = indices[warp_id / kNumSplits];
52
+ const auto dst = pointer::offset(output, warp_id * kSizePerWarp);
53
+ const auto src = pointer::offset(weight, pos * kSize,
54
+ (warp_id % kNumSplits) * kSizePerWarp);
55
+ warp::copy<kSizePerWarp>(dst, src);
56
+ }
57
+
58
+ PDL::launch<kUsePDL>();
59
+ }
60
+
61
+ template <std::size_t kNumThreads, std::size_t kMaxOccupancy, bool kUsePDL,
62
+ std::size_t kElementSize, std::size_t kNumSplits, std::integral T>
63
+ __global__ __launch_bounds__(kNumThreads, kMaxOccupancy) void //
64
+ masked_index_kernel(
65
+ const __grid_constant__ MaskedKernelParams mask_params) {
66
+ using namespace device;
67
+ constexpr auto kSize = kElementSize;
68
+ constexpr auto kSizePerWarp = kSize / kNumSplits;
69
+ constexpr auto kWarpPerBlock = static_cast<unsigned>(kNumThreads / 32);
70
+
71
+ static_assert(kNumThreads % 32 == 0);
72
+ static_assert(std::has_single_bit(kNumSplits));
73
+ static_assert(kElementSize % kNumSplits == 0);
74
+
75
+ const auto &[params, start, length] = mask_params;
76
+ const auto &[output, weight, indices_, num_warps] = params;
77
+ const auto indices = static_cast<const T *>(indices_);
78
+ const auto warp_id =
79
+ (threadIdx.x / kWarpThreads) + blockIdx.x * kWarpPerBlock;
80
+
81
+ PDL::wait<kUsePDL>();
82
+
83
+ if (warp_id < num_warps) {
84
+ const auto pos = indices[warp_id / kNumSplits] - start;
85
+ const auto dst = pointer::offset(output, warp_id * kSizePerWarp);
86
+ if (pos < length) {
87
+ const auto src = pointer::offset(weight, pos * kSize,
88
+ (warp_id % kNumSplits) * kSizePerWarp);
89
+ warp::copy<kSizePerWarp>(dst, src);
90
+ } else {
91
+ warp::reset<kSizePerWarp>(dst);
92
+ }
93
+ }
94
+
95
+ PDL::launch<kUsePDL>();
96
+ }
97
+
98
+ template <std::size_t element_size, // depends on data type and embedding dim
99
+ std::size_t num_splits = 1, // how many warps handles one element
100
+ std::size_t num_threads = 128, // number of threads per block
101
+ std::size_t max_concurrency = 1, // max blocks per SM
102
+ bool use_pdl = false>
103
+ struct IndexKernel {
104
+ static void run(const tvm::ffi::TensorView weights,
105
+ const tvm::ffi::TensorView indices,
106
+ const tvm::ffi::TensorView output,
107
+ tvm::ffi::Optional<tvm::ffi::Tuple<int, int>> mask_opts) {
108
+ using namespace host;
109
+ auto D = SymbolicSize{"D"}; // embedding size
110
+ auto L = SymbolicSize{"L"}; // num indices
111
+ auto device_ = SymbolicDevice{};
112
+ auto weights_dtype_ = SymbolicDType{};
113
+ auto indices_dtype_ = SymbolicDType{};
114
+
115
+ TensorMatcher({-1, D}) //
116
+ .with_dtype(weights_dtype_)
117
+ .with_device<kDLCUDA>(device_)
118
+ .verify(weights);
119
+ TensorMatcher({L, D}) //
120
+ .with_dtype(weights_dtype_)
121
+ .with_device<kDLCUDA>(device_)
122
+ .verify(output);
123
+ TensorMatcher({L}) //
124
+ .with_dtype<int32_t, int64_t>(indices_dtype_)
125
+ .with_device<kDLCUDA>(device_)
126
+ .verify(indices);
127
+
128
+ const auto device = device_.unwrap();
129
+ const auto use_int32 = indices_dtype_.unwrap().bits == 32;
130
+ const auto num_indices = L.unwrap();
131
+ const auto entry_size = dtype_bytes(weights_dtype_.unwrap()) * D.unwrap();
132
+ RuntimeCheck(entry_size == element_size,
133
+ "IndexKernel: element_size mismatch.");
134
+
135
+ constexpr auto kWarpPerBlock = num_threads / 32;
136
+ const auto num_warps = num_splits * num_indices;
137
+ const auto num_blocks = div_ceil(num_warps, kWarpPerBlock);
138
+ const auto params = IndexKernelParams{
139
+ .output = static_cast<char *>(output.data_ptr()),
140
+ .weight = static_cast<const char *>(weights.data_ptr()),
141
+ .indice = indices.data_ptr(),
142
+ .num_warps = num_warps,
143
+ };
144
+
145
+ if (mask_opts.has_value()) {
146
+ const auto &obj = mask_opts.value();
147
+ const auto [start, length] = obj;
148
+ const auto m_params = MaskedKernelParams{
149
+ .params = params,
150
+ .start = static_cast<std::size_t>(start),
151
+ .length = static_cast<std::size_t>(length),
152
+ };
153
+ const auto kernel =
154
+ use_int32 ? masked_index_kernel<num_threads, max_concurrency, use_pdl,
155
+ element_size, num_splits, int32_t>
156
+ : masked_index_kernel<num_threads, max_concurrency, use_pdl,
157
+ element_size, num_splits, int64_t>;
158
+ LaunchKernel(num_blocks, num_threads, device)
159
+ .with_attr(use_pdl)(kernel, m_params);
160
+ } else {
161
+ const auto kernel =
162
+ use_int32 ? index_kernel<num_threads, max_concurrency, use_pdl,
163
+ element_size, num_splits, int32_t>
164
+ : index_kernel<num_threads, max_concurrency, use_pdl,
165
+ element_size, num_splits, int64_t>;
166
+ LaunchKernel(num_blocks, num_threads, device)
167
+ .with_attr(use_pdl)(kernel, params);
168
+ }
169
+ }
170
+ };
171
+
172
+ } // namespace
zonos2/kernel/csrc/jit/store.cu ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include <zonos2/tensor.h>
2
+ #include <zonos2/utils.cuh>
3
+ #include <zonos2/utils.h>
4
+ #include <zonos2/warp.cuh>
5
+
6
+ #include <tvm/ffi/container/tensor.h>
7
+
8
+ #include <concepts>
9
+ #include <cstddef>
10
+ #include <cstdint>
11
+
12
+ namespace {
13
+
14
+ struct StoreKernelParams {
15
+ void *__restrict__ k_cache;
16
+ void *__restrict__ v_cache;
17
+ const void *__restrict__ indices;
18
+ const void *__restrict__ k;
19
+ const void *__restrict__ v;
20
+ std::size_t kv_cache_stride;
21
+ std::size_t kv_input_stride;
22
+ std::size_t length;
23
+ };
24
+
25
+ template <std::size_t kNumThreads, std::size_t kMaxOccupancy, bool kUsePDL,
26
+ std::size_t kElementSize, std::integral T>
27
+ __global__ __launch_bounds__(kNumThreads, kMaxOccupancy) void //
28
+ store_kv_cache(const __grid_constant__ StoreKernelParams params) {
29
+ using namespace device;
30
+
31
+ constexpr auto kWarpPerBlock =
32
+ static_cast<unsigned>(kNumThreads / kWarpThreads);
33
+ static_assert(kNumThreads % kWarpThreads == 0);
34
+
35
+ const auto &[k_cache, v_cache, indices, k, v, kv_cache_stride,
36
+ kv_input_stride, length] = params;
37
+ const auto warp_id =
38
+ (threadIdx.x / kWarpThreads) + blockIdx.x * kWarpPerBlock;
39
+ PDL::wait<kUsePDL>();
40
+
41
+ // each warp handles one element
42
+ if (warp_id < length) {
43
+ const auto pos = static_cast<const T *>(indices)[warp_id];
44
+ const auto dst_k = pointer::offset(k_cache, pos * kv_cache_stride);
45
+ const auto src_k = pointer::offset(k, warp_id * kv_input_stride);
46
+ warp::copy<kElementSize>(dst_k, src_k);
47
+ const auto dst_v = pointer::offset(v_cache, pos * kv_cache_stride);
48
+ const auto src_v = pointer::offset(v, warp_id * kv_input_stride);
49
+ warp::copy<kElementSize>(dst_v, src_v);
50
+ }
51
+
52
+ PDL::launch<kUsePDL>();
53
+ }
54
+
55
+ template <std::size_t element_size, // depends on data type and embedding dim
56
+ std::size_t num_threads = 128, // number of threads per block
57
+ std::size_t max_concurrency = 1, // max blocks per SM
58
+ bool use_pdl = false>
59
+ struct StoreKernel {
60
+ static void run(const tvm::ffi::TensorView k_cache,
61
+ const tvm::ffi::TensorView v_cache,
62
+ const tvm::ffi::TensorView indices,
63
+ const tvm::ffi::TensorView k, const tvm::ffi::TensorView v) {
64
+ using namespace host;
65
+ auto D = SymbolicSize{"D"}; // element size
66
+ auto L = SymbolicSize{"L"}; // length
67
+ auto X = SymbolicSize{"X"}; // stride kv cache
68
+ auto Y = SymbolicSize{"Y"}; // stride kv input
69
+ auto indices_dtype_ = SymbolicDType{};
70
+ auto dtype_ = SymbolicDType{};
71
+ auto device_ = SymbolicDevice{};
72
+
73
+ TensorMatcher({-1, D}) //
74
+ .with_strides({X, 1})
75
+ .with_device<kDLCUDA>(device_)
76
+ .with_dtype(dtype_)
77
+ .verify(k_cache)
78
+ .verify(v_cache);
79
+ TensorMatcher({L, D}) //
80
+ .with_strides({Y, 1})
81
+ .with_device<kDLCUDA>(device_)
82
+ .with_dtype(dtype_)
83
+ .verify(k)
84
+ .verify(v);
85
+ TensorMatcher({L}) //
86
+ .with_device<kDLCUDA>(device_)
87
+ .with_dtype<int32_t, int64_t>(indices_dtype_)
88
+ .verify(indices);
89
+
90
+ const auto dtype_size = dtype_bytes(dtype_.unwrap());
91
+ RuntimeCheck(element_size == dtype_size * D.unwrap());
92
+
93
+ const auto device = device_.unwrap();
94
+ const auto use_int32 = indices_dtype_.unwrap().bits == 32;
95
+ const auto length = static_cast<std::size_t>(L.unwrap());
96
+ const auto kv_cache_stride = X.unwrap() * dtype_size;
97
+ const auto kv_input_stride = Y.unwrap() * dtype_size;
98
+
99
+ const auto params = StoreKernelParams{
100
+ .k_cache = k_cache.data_ptr(),
101
+ .v_cache = v_cache.data_ptr(),
102
+ .indices = indices.data_ptr(),
103
+ .k = k.data_ptr(),
104
+ .v = v.data_ptr(),
105
+ .kv_cache_stride = kv_cache_stride,
106
+ .kv_input_stride = kv_input_stride,
107
+ .length = length,
108
+ };
109
+
110
+ constexpr auto kWarpPerBlock = num_threads / 32;
111
+ static_assert(num_threads % 32 == 0);
112
+ const auto num_blocks = div_ceil(length, kWarpPerBlock);
113
+ const auto kernel = use_int32
114
+ ? store_kv_cache<num_threads, max_concurrency,
115
+ use_pdl, element_size, int32_t>
116
+ : store_kv_cache<num_threads, max_concurrency,
117
+ use_pdl, element_size, int64_t>;
118
+ LaunchKernel(num_blocks, num_threads, device)
119
+ .with_attr(use_pdl)(kernel, params);
120
+ }
121
+ };
122
+
123
+ } // namespace
zonos2/kernel/csrc/src/pynccl.cu ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include <zonos2/nccl227.h>
2
+ #include <zonos2/tensor.h>
3
+ #include <zonos2/utils.cuh>
4
+ #include <zonos2/utils.h>
5
+
6
+ #include <dlpack/dlpack.h>
7
+ #include <tvm/ffi/container/array.h>
8
+ #include <tvm/ffi/container/tensor.h>
9
+ #include <tvm/ffi/function.h>
10
+ #include <tvm/ffi/reflection/registry.h>
11
+
12
+ #include <bit>
13
+ #include <cstdint>
14
+ #include <memory>
15
+ #include <string>
16
+ #include <string_view>
17
+ #include <unordered_map>
18
+
19
+ namespace {
20
+
21
+ using NCCLIDList = tvm::ffi::Array<char>;
22
+
23
+ auto NCCL_CHECK(::ncclResult_t result) -> void {
24
+ if (result != ::ncclSuccess) {
25
+ host::RuntimeCheck(false, ::ncclGetErrorString(result));
26
+ }
27
+ }
28
+
29
+ auto get_uid(const NCCLIDList &wrapper) -> ncclUniqueId {
30
+ host::RuntimeCheck(wrapper.size() == NCCL_UNIQUE_ID_BYTES,
31
+ "Invalid NCCL ID wrapper size");
32
+ ncclUniqueId id;
33
+ std::copy(wrapper.begin(), wrapper.end(), id.internal);
34
+ return id;
35
+ }
36
+
37
+ auto create_uid() -> NCCLIDList {
38
+ ncclUniqueId id;
39
+ NCCL_CHECK(::ncclGetUniqueId(&id));
40
+ return NCCLIDList(id.internal, id.internal + NCCL_UNIQUE_ID_BYTES);
41
+ }
42
+
43
+ const auto kNCCLReduceOPMap = std::unordered_map<std::string_view, ncclRedOp_t>{
44
+ {"sum", ::ncclSum}, {"prod", ::ncclProd}, {"max", ::ncclMax},
45
+ {"min", ::ncclMin}, {"avg", ::ncclAvg},
46
+ };
47
+
48
+ struct DLDataTypeHash {
49
+ auto operator()(const DLDataType &dtype) const noexcept -> std::size_t {
50
+ return std::bit_cast<std::uint32_t>(dtype);
51
+ }
52
+ };
53
+
54
+ template <typename = void>
55
+ auto operator==(const DLDataType &a, const DLDataType &b) -> bool {
56
+ return a.code == b.code && a.bits == b.bits && a.lanes == b.lanes;
57
+ }
58
+
59
+ const auto kNCCLDtypeMap =
60
+ std::unordered_map<DLDataType, ncclDataType_t, DLDataTypeHash>{
61
+ {{DLDataTypeCode::kDLFloat, 16, 1}, ncclFloat16},
62
+ {{DLDataTypeCode::kDLBfloat, 16, 1}, ncclBfloat16},
63
+ };
64
+
65
+ using std::shared_ptr;
66
+
67
+ template <typename T> using shared_obj = shared_ptr<std::remove_pointer_t<T>>;
68
+ template <auto Fn>
69
+ inline constexpr auto template_fn =
70
+ [](auto &&...args) { return Fn(std::forward<decltype(args)>(args)...); };
71
+
72
+ struct NCCLWrapper : public tvm::ffi::Object {
73
+ public:
74
+ NCCLWrapper(int rank, int world_size, const size_t max_bytes, NCCLIDList uid)
75
+ : m_rank(rank), m_world_size(world_size), m_max_bytes(max_bytes) {
76
+ ncclUniqueId id = get_uid(uid);
77
+ ncclComm_t comm;
78
+ NCCL_CHECK(::ncclCommInitRank(&comm, m_world_size, id, m_rank));
79
+ m_comm = {comm, template_fn<::ncclCommDestroy>};
80
+
81
+ void *buf;
82
+ NCCL_CHECK(::ncclMemAlloc(&buf, max_bytes));
83
+ m_sym_mem = {buf, template_fn<::ncclMemFree>};
84
+
85
+ ncclWindow_t win;
86
+ NCCL_CHECK(::ncclCommWindowRegister(comm, buf, max_bytes, &win,
87
+ NCCL_WIN_COLL_SYMMETRIC));
88
+ m_win = {win, [comm = m_comm](ncclWindow_t w) {
89
+ return NCCL_CHECK(::ncclCommWindowDeregister(comm.get(), w));
90
+ }};
91
+ }
92
+
93
+ auto all_reduce(tvm::ffi::TensorView t, std::string op) const -> void {
94
+ using namespace host;
95
+ RuntimeCheck(t.device().device_type == kDLCUDA,
96
+ "Tensor must be on CUDA device");
97
+ RuntimeCheck(t.is_contiguous(), "Tensor must be contiguous");
98
+ const auto size_dim = static_cast<size_t>(t.shape().Product());
99
+ const auto dtype = kNCCLDtypeMap.at(t.dtype());
100
+ const auto size_bytes = size_dim * (t.dtype().bits / 8);
101
+ const auto data_ptr = t.data_ptr();
102
+ const auto reduce_op = kNCCLReduceOPMap.at(op);
103
+ const auto stream = LaunchKernel::resolve_device(t.device());
104
+
105
+ if (size_bytes <= m_max_bytes) { // use internal buffer
106
+ const auto buf_ptr = m_sym_mem.get();
107
+ const auto need_memcpy = (buf_ptr != data_ptr);
108
+ if (need_memcpy) {
109
+ CUDA_CHECK(::cudaMemcpyAsync(buf_ptr, data_ptr, size_bytes,
110
+ ::cudaMemcpyDeviceToDevice, stream));
111
+ }
112
+ NCCL_CHECK(::ncclAllReduce(
113
+ /*sendbuff=*/buf_ptr,
114
+ /*recvbuff=*/buf_ptr,
115
+ /*count=*/size_dim,
116
+ /*datatype=*/dtype,
117
+ /*op=*/reduce_op,
118
+ /*comm=*/m_comm.get(),
119
+ /*stream=*/stream));
120
+ if (need_memcpy) {
121
+ CUDA_CHECK(::cudaMemcpyAsync(data_ptr, buf_ptr, size_bytes,
122
+ ::cudaMemcpyDeviceToDevice, stream));
123
+ }
124
+ } else {
125
+ NCCL_CHECK(::ncclAllReduce(
126
+ /*sendbuff=*/data_ptr,
127
+ /*recvbuff=*/data_ptr,
128
+ /*count=*/size_dim,
129
+ /*datatype=*/dtype,
130
+ /*op=*/reduce_op,
131
+ /*comm=*/m_comm.get(),
132
+ /*stream=*/stream));
133
+ }
134
+ }
135
+
136
+ auto all_gather(tvm::ffi::TensorView dst, tvm::ffi::TensorView src) const
137
+ -> void {
138
+ using namespace host;
139
+ RuntimeCheck(src.device().device_type == kDLCUDA,
140
+ "Tensor must be on CUDA device");
141
+ RuntimeCheck(src.is_contiguous(), "Tensor must be contiguous");
142
+ RuntimeCheck(dst.device().device_type == kDLCUDA,
143
+ "Tensor must be on CUDA device");
144
+ RuntimeCheck(dst.is_contiguous(), "Tensor must be contiguous");
145
+ RuntimeCheck(dst.size(0) == src.size(0) * m_world_size,
146
+ "Destination tensor has incorrect size");
147
+ const auto size_dim = static_cast<size_t>(src.shape().Product());
148
+ const auto dtype = kNCCLDtypeMap.at(src.dtype());
149
+ const auto src_ptr = src.data_ptr();
150
+ const auto dst_ptr = dst.data_ptr();
151
+ const auto stream = LaunchKernel::resolve_device(src.device());
152
+ // do not use internal buffer for all_gather, directly gather to output
153
+ // tensor
154
+ NCCL_CHECK(::ncclAllGather(
155
+ /*sendbuff=*/src_ptr,
156
+ /*recvbuff=*/dst_ptr,
157
+ /*sendcount=*/size_dim,
158
+ /*datatype=*/dtype,
159
+ /*comm=*/m_comm.get(),
160
+ /*stream=*/stream));
161
+ }
162
+
163
+ auto get_buffer() const -> void * { return m_sym_mem.get(); }
164
+
165
+ TVM_FFI_DECLARE_OBJECT_INFO_FINAL("zonos2.NCCLWrapper", NCCLWrapper,
166
+ tvm::ffi::Object);
167
+
168
+ private:
169
+ int m_rank;
170
+ int m_world_size;
171
+ size_t m_max_bytes;
172
+ shared_obj<ncclComm_t> m_comm;
173
+ shared_ptr<void> m_sym_mem;
174
+ shared_obj<ncclWindow_t> m_win;
175
+ };
176
+
177
+ TVM_FFI_STATIC_INIT_BLOCK() {
178
+ namespace refl = tvm::ffi::reflection;
179
+ refl::ObjectDef<NCCLWrapper>()
180
+ .def(refl::init<int, int, size_t, NCCLIDList>(), "__init__")
181
+ .def("all_reduce", &NCCLWrapper::all_reduce)
182
+ .def("all_gather", &NCCLWrapper::all_gather)
183
+ .def("get_buffer", &NCCLWrapper::get_buffer);
184
+ }
185
+
186
+ TVM_FFI_DLL_EXPORT_TYPED_FUNC(create_nccl_uid, &create_uid);
187
+
188
+ } // namespace
zonos2/kernel/csrc/src/radix.cpp ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include <zonos2/utils.h>
2
+
3
+ #include <dlpack/dlpack.h>
4
+ #include <tvm/ffi/container/tensor.h>
5
+ #include <tvm/ffi/dtype.h>
6
+ #include <tvm/ffi/extra/c_env_api.h>
7
+ #include <tvm/ffi/function.h>
8
+ #include <tvm/ffi/object.h>
9
+
10
+ namespace {
11
+
12
+ auto _is_1d_cpu_int_tensor(const tvm::ffi::TensorView tensor) -> bool {
13
+ return tensor.ndim() == 1 && tensor.is_contiguous() &&
14
+ tensor.device().device_type == kDLCPU &&
15
+ (tensor.dtype().code == kDLInt) &&
16
+ (tensor.dtype().bits == 32 || tensor.dtype().bits == 64);
17
+ }
18
+
19
+ auto fast_compare_key(const tvm::ffi::TensorView a,
20
+ const tvm::ffi::TensorView b) -> size_t {
21
+ host::RuntimeCheck(_is_1d_cpu_int_tensor(a) && _is_1d_cpu_int_tensor(b),
22
+ "Both tensors must be 1D CPU int tensors.");
23
+ host::RuntimeCheck(a.dtype() == b.dtype());
24
+ const auto a_ptr = a.data_ptr();
25
+ const auto b_ptr = b.data_ptr();
26
+ const auto common_len = std::min(a.size(0), b.size(0));
27
+ if (a.dtype().bits == 64) {
28
+ const auto a_ptr_64 = static_cast<const int64_t *>(a_ptr);
29
+ const auto b_ptr_64 = static_cast<const int64_t *>(b_ptr);
30
+ const auto diff_pos =
31
+ std::mismatch(a_ptr_64, a_ptr_64 + common_len, b_ptr_64);
32
+ return static_cast<size_t>(diff_pos.first - a_ptr_64);
33
+ } else {
34
+ const auto a_ptr_32 = static_cast<const int32_t *>(a_ptr);
35
+ const auto b_ptr_32 = static_cast<const int32_t *>(b_ptr);
36
+ const auto diff_pos =
37
+ std::mismatch(a_ptr_32, a_ptr_32 + common_len, b_ptr_32);
38
+ return static_cast<size_t>(diff_pos.first - a_ptr_32);
39
+ }
40
+ }
41
+
42
+ } // namespace
43
+
44
+ TVM_FFI_DLL_EXPORT_TYPED_FUNC(fast_compare_key, fast_compare_key);
zonos2/kernel/csrc/src/tensor.cpp ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include <zonos2/tensor.h>
2
+ #include <zonos2/utils.h>
3
+
4
+ #include <dlpack/dlpack.h>
5
+ #include <tvm/ffi/container/array.h>
6
+ #include <tvm/ffi/container/tensor.h>
7
+ #include <tvm/ffi/dtype.h>
8
+ #include <tvm/ffi/extra/c_env_api.h>
9
+ #include <tvm/ffi/function.h>
10
+ #include <tvm/ffi/object.h>
11
+
12
+ namespace {
13
+
14
+ auto test(tvm::ffi::TensorView x, tvm::ffi::TensorView y) -> void {
15
+ auto N = host::SymbolicSize{"N"};
16
+ const auto M = 1024;
17
+ host::TensorMatcher({N, M})
18
+ .with_strides({-1, 1}) // -1 means any
19
+ .with_dtype<int, float>()
20
+ .with_device<kDLCPU>()
21
+ .verify(x);
22
+ host::TensorMatcher({N, M}) // default contiguous
23
+ .with_dtype({{kDLInt, 32, 1}, {kDLInt, 64, 1}})
24
+ .with_device({{kDLCUDA, 1}})
25
+ .verify(y);
26
+ host::RuntimeCheck(N.unwrap() % 4 == 0);
27
+ }
28
+
29
+ } // namespace
30
+
31
+ TVM_FFI_DLL_EXPORT_TYPED_FUNC(test, test);
zonos2/kernel/index.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+ from typing import TYPE_CHECKING, Tuple
5
+
6
+ from .utils import KernelConfig, load_jit, make_cpp_args
7
+
8
+ if TYPE_CHECKING:
9
+ import torch
10
+ from tvm_ffi import Module
11
+
12
+ DEFAULT_INDEX_KERNEL_CONFIG = KernelConfig(num_threads=128, max_occupancy=1, use_pdl=False)
13
+
14
+
15
+ @lru_cache(maxsize=None)
16
+ def _jit_index_module(
17
+ element_size: int,
18
+ *,
19
+ num_splits: int = 1,
20
+ config: KernelConfig = DEFAULT_INDEX_KERNEL_CONFIG,
21
+ ) -> Module:
22
+ args = make_cpp_args(element_size, num_splits, *config)
23
+ return load_jit(
24
+ "index",
25
+ *args,
26
+ cuda_files=["index.cu"],
27
+ cuda_wrappers=[("launch", f"IndexKernel<{args}>::run")],
28
+ )
29
+
30
+
31
+ def indexing(
32
+ weights: torch.Tensor,
33
+ indices: torch.Tensor,
34
+ *,
35
+ output: torch.Tensor | None = None,
36
+ vocab_range: Tuple[int, int] | None = None, # (start, length)
37
+ ) -> torch.Tensor:
38
+ if output is None:
39
+ output = weights.new_empty(indices.shape[0], weights.shape[1])
40
+
41
+ element_size = weights.shape[1] * weights.element_size()
42
+ if element_size % 2048 == 0:
43
+ num_splits = 4
44
+ elif element_size % 1024 == 0:
45
+ num_splits = 2
46
+ else:
47
+ num_splits = 1
48
+ module = _jit_index_module(element_size, num_splits=num_splits)
49
+ module.launch(weights, indices, output, vocab_range)
50
+ return output
zonos2/kernel/moe_impl.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Dict
2
+
3
+ import torch
4
+ import triton
5
+ import triton.language as tl
6
+ from zonos2.kernel.triton.fused_moe import fused_moe_kernel
7
+
8
+
9
+ def fused_moe_kernel_triton(
10
+ A: torch.Tensor,
11
+ B: torch.Tensor,
12
+ C: torch.Tensor,
13
+ topk_weights: torch.Tensor,
14
+ topk_ids: torch.Tensor,
15
+ sorted_token_ids: torch.Tensor,
16
+ expert_ids: torch.Tensor,
17
+ num_tokens_post_padded: torch.Tensor,
18
+ mul_routed_weight: bool,
19
+ top_k: int,
20
+ config: Dict[str, Any],
21
+ compute_type: tl.dtype,
22
+ ) -> None:
23
+ assert topk_weights.stride(1) == 1
24
+ assert sorted_token_ids.stride(0) == 1
25
+ padded_size = 0
26
+ grid = lambda META: (
27
+ triton.cdiv(sorted_token_ids.shape[0], META["BLOCK_SIZE_M"])
28
+ * triton.cdiv(B.shape[1], META["BLOCK_SIZE_N"]),
29
+ )
30
+ K = B.shape[2] - padded_size
31
+ if K % config["BLOCK_SIZE_K"] == 0:
32
+ even_Ks = True
33
+ else:
34
+ even_Ks = False
35
+ fused_moe_kernel[grid](
36
+ A,
37
+ B,
38
+ C,
39
+ topk_weights,
40
+ sorted_token_ids,
41
+ expert_ids,
42
+ num_tokens_post_padded,
43
+ B.shape[1],
44
+ B.shape[2] - padded_size,
45
+ sorted_token_ids.shape[0],
46
+ topk_ids.numel(),
47
+ A.stride(0),
48
+ A.stride(1),
49
+ B.stride(0),
50
+ B.stride(2),
51
+ B.stride(1),
52
+ C.stride(1),
53
+ C.stride(2),
54
+ MUL_ROUTED_WEIGHT=mul_routed_weight,
55
+ top_k=top_k,
56
+ compute_type=compute_type,
57
+ even_Ks=even_Ks,
58
+ **config,
59
+ )
zonos2/kernel/pynccl.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+ from typing import TYPE_CHECKING, Any, Literal
5
+
6
+ from zonos2.env import ENV
7
+
8
+ from .utils import load_aot
9
+
10
+ if TYPE_CHECKING:
11
+ from abc import abstractmethod
12
+
13
+ import torch
14
+ from tvm_ffi import Module
15
+
16
+ class PyNCCLCommunicator:
17
+ @abstractmethod
18
+ def all_reduce(self, input: torch.Tensor, op: Literal["sum"]) -> None: ...
19
+ @abstractmethod
20
+ def all_gather(self, output: torch.Tensor, input: torch.Tensor) -> None: ...
21
+ @abstractmethod
22
+ def get_buffer(self) -> int: ...
23
+
24
+ else:
25
+ PyNCCLCommunicator = Any
26
+
27
+
28
+ @lru_cache(maxsize=None)
29
+ def _load_nccl_module() -> Module:
30
+ return load_aot("pynccl", cuda_files=["pynccl.cu"], extra_ldflags=["-lnccl"])
31
+
32
+
33
+ @lru_cache(maxsize=None)
34
+ def _get_pynccl_wrapper_cls():
35
+ import tvm_ffi
36
+
37
+ @tvm_ffi.register_object("zonos2.NCCLWrapper")
38
+ class PyNCCLImpl(tvm_ffi.Object):
39
+ def __init__(self, *args):
40
+ self.__ffi_init__(*args)
41
+
42
+ return PyNCCLImpl
43
+
44
+
45
+ def init_pynccl(
46
+ *,
47
+ tp_rank: int,
48
+ tp_size: int,
49
+ tp_cpu_group: torch.distributed.ProcessGroup,
50
+ max_size_bytes: int = 0,
51
+ ) -> PyNCCLCommunicator:
52
+ import torch
53
+
54
+ max_size_bytes = min(max_size_bytes, ENV.PYNCCL_MAX_BUFFER_SIZE.value)
55
+
56
+ module = _load_nccl_module()
57
+ cls = _get_pynccl_wrapper_cls()
58
+
59
+ if tp_rank == 0:
60
+ id_list = [module.create_nccl_uid()]
61
+ torch.distributed.broadcast_object_list(
62
+ id_list,
63
+ src=0,
64
+ group=tp_cpu_group,
65
+ )
66
+ else:
67
+ id_list = [None]
68
+ torch.distributed.broadcast_object_list(
69
+ id_list,
70
+ src=0,
71
+ group=tp_cpu_group,
72
+ )
73
+
74
+ nccl_id = id_list[0]
75
+ assert not nccl_id is None, f"Failed to get NCCL unique ID on {tp_rank = }"
76
+
77
+ # bypass type checking for the FFI object
78
+ return cls(tp_rank, tp_size, max_size_bytes, nccl_id) # type: ignore
zonos2/kernel/radix.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+ from typing import TYPE_CHECKING
5
+
6
+ from .utils import load_aot
7
+
8
+ if TYPE_CHECKING:
9
+ import torch
10
+ from tvm_ffi import Module
11
+
12
+
13
+ @lru_cache(maxsize=None)
14
+ def _load_radix_module() -> Module:
15
+ return load_aot("radix", cpp_files=["radix.cpp"])
16
+
17
+
18
+ def fast_compare_key(x: torch.Tensor, y: torch.Tensor) -> int:
19
+ # compare 2 1-D int cpu tensors for equality
20
+ return _load_radix_module().fast_compare_key(x, y)
zonos2/kernel/store.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+ from typing import TYPE_CHECKING
5
+
6
+ from .utils import KernelConfig, load_jit, make_cpp_args
7
+
8
+ if TYPE_CHECKING:
9
+ import torch
10
+ from tvm_ffi import Module
11
+
12
+ DEFAULT_INDEX_KERNEL_CONFIG = KernelConfig(num_threads=128, max_occupancy=1, use_pdl=False)
13
+
14
+
15
+ @lru_cache(maxsize=None)
16
+ def _jit_store_module(
17
+ element_size: int,
18
+ *,
19
+ config: KernelConfig = DEFAULT_INDEX_KERNEL_CONFIG,
20
+ ) -> Module:
21
+ args = make_cpp_args(element_size, *config)
22
+ return load_jit(
23
+ "store",
24
+ *args,
25
+ cuda_files=["store.cu"],
26
+ cuda_wrappers=[("launch", f"StoreKernel<{args}>::run")],
27
+ )
28
+
29
+
30
+ def store_cache(
31
+ k_cache: torch.Tensor,
32
+ v_cache: torch.Tensor,
33
+ indices: torch.Tensor,
34
+ k: torch.Tensor,
35
+ v: torch.Tensor,
36
+ ) -> None:
37
+ num_tokens = k_cache.shape[0]
38
+ k_cache = k_cache.view(num_tokens, -1)
39
+ v_cache = v_cache.view(num_tokens, -1)
40
+ element_size = k_cache.shape[1] * k_cache.element_size()
41
+ module = _jit_store_module(element_size)
42
+ module.launch(k_cache, v_cache, indices, k, v)
zonos2/kernel/tensor.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+ from typing import TYPE_CHECKING
5
+
6
+ from .utils import load_aot
7
+
8
+ if TYPE_CHECKING:
9
+ import torch
10
+ from tvm_ffi import Module
11
+
12
+
13
+ @lru_cache(maxsize=None)
14
+ def _load_test_tensor_module() -> Module:
15
+ return load_aot("test_tensor", cpp_files=["tensor.cpp"])
16
+
17
+
18
+ def test_tensor(x: torch.Tensor, y: torch.Tensor) -> int:
19
+ return _load_test_tensor_module().test(x, y)
zonos2/kernel/triton/fused_moe.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import triton
3
+ import triton.language as tl
4
+
5
+
6
+ @triton.jit
7
+ def _moe_sum_reduce_kernel(
8
+ input_ptr,
9
+ input_stride_0,
10
+ input_stride_1,
11
+ input_stride_2,
12
+ output_ptr,
13
+ output_stride_0,
14
+ output_stride_1,
15
+ token_num: int,
16
+ topk_num: int,
17
+ hidden_dim: int,
18
+ routed_scaling_factor: tl.constexpr,
19
+ BLOCK_M: tl.constexpr,
20
+ BLOCK_DIM: tl.constexpr,
21
+ NUM_STAGE: tl.constexpr,
22
+ ):
23
+ input_stride_0 = tl.cast(input_stride_0, dtype=tl.int64)
24
+ input_stride_1 = tl.cast(input_stride_1, dtype=tl.int64)
25
+ output_stride_0 = tl.cast(output_stride_0, dtype=tl.int64)
26
+
27
+ token_block_id = tl.program_id(0)
28
+ dim_block_id = tl.program_id(1)
29
+
30
+ token_start = token_block_id * BLOCK_M
31
+ token_end = min((token_block_id + 1) * BLOCK_M, token_num)
32
+
33
+ dim_start = dim_block_id * BLOCK_DIM
34
+ dim_end = min((dim_block_id + 1) * BLOCK_DIM, hidden_dim)
35
+
36
+ offs_dim = dim_start + tl.arange(0, BLOCK_DIM)
37
+
38
+ for token_index in range(token_start, token_end):
39
+ accumulator = tl.zeros((BLOCK_DIM,), dtype=tl.float32)
40
+ input_t_ptr = input_ptr + token_index * input_stride_0 + offs_dim
41
+ for i in tl.range(0, topk_num, num_stages=NUM_STAGE):
42
+ tmp = tl.load(input_t_ptr + i * input_stride_1, mask=offs_dim < dim_end, other=0.0)
43
+ accumulator += tmp
44
+ accumulator = accumulator * routed_scaling_factor
45
+ store_t_ptr = output_ptr + token_index * output_stride_0 + offs_dim
46
+ tl.store(
47
+ store_t_ptr,
48
+ accumulator.to(input_ptr.dtype.element_ty),
49
+ mask=offs_dim < dim_end,
50
+ )
51
+
52
+
53
+ @triton.jit
54
+ def fused_moe_kernel(
55
+ # Pointers to matrices
56
+ a_ptr,
57
+ b_ptr,
58
+ c_ptr,
59
+ topk_weights_ptr,
60
+ sorted_token_ids_ptr,
61
+ expert_ids_ptr,
62
+ num_tokens_post_padded_ptr,
63
+ # Matrix dimensions
64
+ N,
65
+ K,
66
+ EM,
67
+ num_valid_tokens,
68
+ # The stride variables represent how much to increase the ptr by when
69
+ # moving by 1 element in a particular dimension. E.g. `stride_am` is
70
+ # how much to increase `a_ptr` by to get the element one row down
71
+ # (A has M rows).
72
+ stride_am,
73
+ stride_ak,
74
+ stride_be,
75
+ stride_bk,
76
+ stride_bn,
77
+ stride_cm,
78
+ stride_cn,
79
+ # Meta-parameters
80
+ BLOCK_SIZE_M: tl.constexpr,
81
+ BLOCK_SIZE_N: tl.constexpr,
82
+ BLOCK_SIZE_K: tl.constexpr,
83
+ GROUP_SIZE_M: tl.constexpr,
84
+ MUL_ROUTED_WEIGHT: tl.constexpr,
85
+ top_k: tl.constexpr,
86
+ compute_type: tl.constexpr,
87
+ even_Ks: tl.constexpr,
88
+ ):
89
+ """
90
+ Implements the fused computation for a Mixture of Experts (MOE) using
91
+ token and expert matrices.
92
+
93
+ Key Parameters:
94
+ - A: The input tensor representing tokens with shape (*, K), where '*' can
95
+ be any shape representing batches and K is the feature dimension of
96
+ each token.
97
+ - B: The stacked MOE weight tensor with shape (E, N, K), where E is
98
+ the number of experts, K is the input feature dimension, and N is
99
+ the output feature dimension.
100
+ - C: The output cache tensor with shape (M, topk, N), where M is the
101
+ total number of tokens post padding, topk is the number of times
102
+ each token is repeated, and N is the output feature dimension.
103
+ - sorted_token_ids: A tensor containing the sorted indices of tokens,
104
+ repeated topk times and arranged by the expert index they are
105
+ assigned to.
106
+ - expert_ids: A tensor containing the indices of the expert for each
107
+ block. It determines which expert matrix from B should be used for
108
+ each block in A.
109
+
110
+ This kernel performs the multiplication of a token by its corresponding
111
+ expert matrix as determined by `expert_ids`. The sorting of
112
+ `sorted_token_ids` by expert index and padding ensures divisibility by
113
+ BLOCK_SIZE_M, which is necessary to maintain consistency in block matrix
114
+ multiplication across different blocks processed by the same expert.
115
+ """
116
+ # -----------------------------------------------------------
117
+ # Map program ids `pid` to the block of C it should compute.
118
+ # This is done in a grouped ordering to promote L2 data reuse.
119
+ pid = tl.program_id(axis=0)
120
+ num_pid_m = tl.cdiv(EM, BLOCK_SIZE_M)
121
+ num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
122
+ num_pid_in_group = GROUP_SIZE_M * num_pid_n
123
+ group_id = pid // num_pid_in_group
124
+ first_pid_m = group_id * GROUP_SIZE_M
125
+ group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M)
126
+ pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m)
127
+ pid_n = (pid % num_pid_in_group) // group_size_m
128
+
129
+ # ----------------------------------------------------------
130
+ # Create pointers for the first blocks of A and B.
131
+ # We will advance this pointer as we move in the K direction
132
+ # and accumulate
133
+ # `a_ptrs` is a block of [BLOCK_SIZE_M, BLOCK_SIZE_K] pointers
134
+ # `b_ptrs` is a block of [BLOCK_SIZE_K, BLOCK_SIZE_N] pointers
135
+ num_tokens_post_padded = tl.load(num_tokens_post_padded_ptr)
136
+ if pid_m * BLOCK_SIZE_M >= num_tokens_post_padded:
137
+ return
138
+ offs_token_id = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
139
+ offs_token = tl.load(sorted_token_ids_ptr + offs_token_id)
140
+ offs_token = offs_token.to(tl.int64)
141
+ token_mask = offs_token < num_valid_tokens
142
+
143
+ offs_bn = (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)) % N
144
+ offs_k = tl.arange(0, BLOCK_SIZE_K)
145
+ a_ptrs = a_ptr + (offs_token[:, None] // top_k * stride_am + offs_k[None, :] * stride_ak)
146
+
147
+ off_experts = tl.load(expert_ids_ptr + pid_m)
148
+ b_ptrs = (
149
+ b_ptr
150
+ + off_experts * stride_be
151
+ + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn)
152
+ )
153
+
154
+ # -----------------------------------------------------------
155
+ # Iterate to compute a block of the C matrix.
156
+ # We accumulate into a `[BLOCK_SIZE_M, BLOCK_SIZE_N]` block
157
+ # of fp32 values for higher accuracy.
158
+ # `accumulator` will be converted back to fp16 after the loop.
159
+ accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
160
+
161
+ for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
162
+ # Load the next block of A and B, generate a mask by checking the
163
+ # K dimension.
164
+ if even_Ks:
165
+ a = tl.load(
166
+ a_ptrs,
167
+ mask=token_mask[:, None],
168
+ other=0.0,
169
+ )
170
+ b = tl.load(b_ptrs)
171
+ else:
172
+ a = tl.load(
173
+ a_ptrs,
174
+ mask=token_mask[:, None] & (offs_k[None, :] < K - k * BLOCK_SIZE_K),
175
+ other=0.0,
176
+ )
177
+ b = tl.load(b_ptrs, mask=offs_k[:, None] < K - k * BLOCK_SIZE_K, other=0.0)
178
+
179
+ # We accumulate along the K dimension.
180
+
181
+ accumulator += tl.dot(a, b)
182
+ # Advance the ptrs to the next K block.
183
+ a_ptrs += BLOCK_SIZE_K * stride_ak
184
+ b_ptrs += BLOCK_SIZE_K * stride_bk
185
+
186
+ if MUL_ROUTED_WEIGHT:
187
+ moe_weight = tl.load(topk_weights_ptr + offs_token, mask=token_mask, other=0)
188
+ accumulator = accumulator * moe_weight[:, None]
189
+
190
+ accumulator = accumulator.to(compute_type)
191
+ # -----------------------------------------------------------
192
+ # Write back the block of the output
193
+ offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
194
+ c_ptrs = c_ptr + stride_cm * offs_token[:, None] + stride_cn * offs_cn[None, :]
195
+ c_mask = token_mask[:, None] & (offs_cn[None, :] < N)
196
+ tl.store(c_ptrs, accumulator, mask=c_mask)
197
+
198
+
199
+ def moe_sum_reduce_triton(input: torch.Tensor, output: torch.Tensor, routed_scaling_factor: float):
200
+ assert input.is_contiguous()
201
+ assert output.is_contiguous()
202
+
203
+ token_num, topk_num, hidden_dim = input.shape
204
+ assert output.shape[0] == token_num and output.shape[1] == hidden_dim
205
+
206
+ BLOCK_M = 1
207
+ BLOCK_DIM = 2048
208
+ NUM_STAGE = 1
209
+ num_warps = 8
210
+
211
+ grid = (
212
+ triton.cdiv(token_num, BLOCK_M),
213
+ triton.cdiv(hidden_dim, BLOCK_DIM),
214
+ )
215
+
216
+ _moe_sum_reduce_kernel[grid](
217
+ input,
218
+ *input.stride(),
219
+ output,
220
+ *output.stride(),
221
+ token_num=token_num,
222
+ topk_num=topk_num,
223
+ hidden_dim=hidden_dim,
224
+ routed_scaling_factor=routed_scaling_factor,
225
+ BLOCK_M=BLOCK_M,
226
+ BLOCK_DIM=BLOCK_DIM,
227
+ NUM_STAGE=NUM_STAGE,
228
+ num_warps=num_warps,
229
+ )
230
+ return
zonos2/kernel/utils.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import pathlib
4
+ from typing import TYPE_CHECKING, List, NamedTuple, Tuple, TypeAlias, Union
5
+
6
+ if TYPE_CHECKING:
7
+ from tvm_ffi import Module
8
+
9
+ KERNEL_PATH = pathlib.Path(__file__).parent / "csrc"
10
+ DEFAULT_INCLUDE = [str(KERNEL_PATH / "include")]
11
+ DEFAULT_CFLAGS = ["-std=c++20", "-O3"]
12
+ DEFAULT_CUDA_CFLAGS = ["-std=c++20", "-O3", "--expt-relaxed-constexpr"]
13
+ DEFAULT_LDFLAGS = []
14
+ CPP_TEMPLATE_TYPE: TypeAlias = Union[int, float, bool]
15
+
16
+
17
+ class CppArgList(list[str]):
18
+ def __str__(self) -> str:
19
+ return ", ".join(self)
20
+
21
+
22
+ class KernelConfig(NamedTuple):
23
+ num_threads: int
24
+ max_occupancy: int
25
+ use_pdl: bool
26
+
27
+ @property
28
+ def template_args(self) -> str:
29
+ pdl = "true" if self.use_pdl else "false"
30
+ return f"{self.num_threads},{self.max_occupancy},{pdl}"
31
+
32
+
33
+ def _make_name(*args: str) -> str:
34
+ return "zonos2__" + "_".join(str(arg) for arg in args)
35
+
36
+
37
+ def _make_wrapper(tup: Tuple[str, str]) -> str:
38
+ export_name, kernel_name = tup
39
+ return f"TVM_FFI_DLL_EXPORT_TYPED_FUNC({export_name}, ({kernel_name}));"
40
+
41
+
42
+ def make_cpp_args(*args: CPP_TEMPLATE_TYPE) -> CppArgList:
43
+ def _convert(arg: CPP_TEMPLATE_TYPE) -> str:
44
+ if isinstance(arg, bool):
45
+ return "true" if arg else "false"
46
+ if isinstance(arg, (int, float)):
47
+ return str(arg)
48
+ raise TypeError(f"Unsupported argument type for cpp template: {type(arg)}")
49
+
50
+ return CppArgList(_convert(arg) for arg in args)
51
+
52
+
53
+ def load_aot(
54
+ *args: str,
55
+ cpp_files: List[str] | None = None,
56
+ cuda_files: List[str] | None = None,
57
+ extra_cflags: List[str] | None = None,
58
+ extra_cuda_cflags: List[str] | None = None,
59
+ extra_ldflags: List[str] | None = None,
60
+ extra_include_paths: List[str] | None = None,
61
+ build_directory: str | None = None,
62
+ ) -> Module:
63
+ from tvm_ffi.cpp import load
64
+
65
+ cpp_files = cpp_files or []
66
+ cuda_files = cuda_files or []
67
+ extra_cflags = extra_cflags or []
68
+ extra_cuda_cflags = extra_cuda_cflags or []
69
+ extra_ldflags = extra_ldflags or []
70
+ extra_include_paths = extra_include_paths or []
71
+
72
+ cpp_files = [str((KERNEL_PATH / "src" / f).resolve()) for f in cpp_files]
73
+ cuda_files = [str((KERNEL_PATH / "src" / f).resolve()) for f in cuda_files]
74
+
75
+ return load(
76
+ _make_name(*args),
77
+ cpp_files=cpp_files,
78
+ cuda_files=cuda_files,
79
+ extra_cflags=DEFAULT_CFLAGS + extra_cflags,
80
+ extra_cuda_cflags=DEFAULT_CUDA_CFLAGS + extra_cuda_cflags,
81
+ extra_ldflags=DEFAULT_LDFLAGS + extra_ldflags,
82
+ extra_include_paths=DEFAULT_INCLUDE + extra_include_paths,
83
+ build_directory=build_directory,
84
+ )
85
+
86
+
87
+ def load_jit(
88
+ *args: str,
89
+ cpp_files: List[str] | None = None,
90
+ cuda_files: List[str] | None = None,
91
+ cpp_wrappers: List[Tuple[str, str]] | None = None,
92
+ cuda_wrappers: List[Tuple[str, str]] | None = None,
93
+ extra_cflags: List[str] | None = None,
94
+ extra_cuda_cflags: List[str] | None = None,
95
+ extra_ldflags: List[str] | None = None,
96
+ extra_include_paths: List[str] | None = None,
97
+ build_directory: str | None = None,
98
+ ) -> Module:
99
+ from tvm_ffi.cpp import load_inline
100
+
101
+ cpp_files = cpp_files or []
102
+ cuda_files = cuda_files or []
103
+ cpp_wrappers = cpp_wrappers or []
104
+ cuda_wrappers = cuda_wrappers or []
105
+ extra_cflags = extra_cflags or []
106
+ extra_cuda_cflags = extra_cuda_cflags or []
107
+ extra_ldflags = extra_ldflags or []
108
+ extra_include_paths = extra_include_paths or []
109
+
110
+ # include cpp files
111
+ cpp_paths = [(KERNEL_PATH / "jit" / f).resolve() for f in cpp_files]
112
+ cpp_sources = [f'#include "{path}"' for path in cpp_paths]
113
+ cpp_sources += [_make_wrapper(tup) for tup in cpp_wrappers]
114
+
115
+ # include cuda files
116
+ cuda_paths = [(KERNEL_PATH / "jit" / f).resolve() for f in cuda_files]
117
+ cuda_sources = [f'#include "{path}"' for path in cuda_paths]
118
+ cuda_sources += [_make_wrapper(tup) for tup in cuda_wrappers]
119
+
120
+ return load_inline(
121
+ _make_name(*args),
122
+ cpp_sources=cpp_sources,
123
+ cuda_sources=cuda_sources,
124
+ extra_cflags=DEFAULT_CFLAGS + extra_cflags,
125
+ extra_cuda_cflags=DEFAULT_CUDA_CFLAGS + extra_cuda_cflags,
126
+ extra_ldflags=DEFAULT_LDFLAGS + extra_ldflags,
127
+ extra_include_paths=DEFAULT_INCLUDE + extra_include_paths,
128
+ build_directory=build_directory,
129
+ )
zonos2/kvcache/__init__.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Protocol
4
+
5
+ from zonos2.utils import Registry
6
+
7
+ if TYPE_CHECKING:
8
+ import torch
9
+ from zonos2.models import ModelConfig
10
+
11
+ from .base import (
12
+ BaseCacheHandle,
13
+ BaseCacheManager,
14
+ BaseKVCache,
15
+ KVCacheLayout,
16
+ SizeInfo,
17
+ )
18
+
19
+
20
+ class CacheManagerCreator(Protocol):
21
+ def __call__(self, device: torch.device) -> BaseCacheManager: ...
22
+
23
+
24
+ SUPPORTED_CACHE_MANAGER = Registry[CacheManagerCreator]("Cache Manager")
25
+
26
+
27
+ def create_kvcache(
28
+ model_config: ModelConfig,
29
+ num_pages: int,
30
+ dtype: torch.dtype,
31
+ device: torch.device,
32
+ cache_layout: KVCacheLayout = KVCacheLayout.LayerFirst,
33
+ ) -> BaseKVCache:
34
+ from .mha_pool import MHAKVCache # TODO: support other variants (e.g. MLA)
35
+
36
+ return MHAKVCache(
37
+ num_kv_heads=model_config.num_kv_heads,
38
+ num_pages=num_pages,
39
+ kv_layout=cache_layout,
40
+ num_layers=model_config.num_layers,
41
+ head_dim=model_config.head_dim,
42
+ device=device,
43
+ dtype=dtype,
44
+ )
45
+
46
+
47
+ @SUPPORTED_CACHE_MANAGER.register("naive")
48
+ def create_naive_cache_manager(device: torch.device):
49
+ from .naive_manager import NaiveCacheManager
50
+
51
+ return NaiveCacheManager(device=device)
52
+
53
+
54
+ @SUPPORTED_CACHE_MANAGER.register("radix")
55
+ def create_radix_cache_manager(device: torch.device):
56
+ from .radix_manager import RadixCacheManager
57
+
58
+ return RadixCacheManager(device=device)
59
+
60
+
61
+ def create_cache_manager(device: torch.device, type: str) -> BaseCacheManager:
62
+ return SUPPORTED_CACHE_MANAGER[type](device)
63
+
64
+
65
+ __all__ = [
66
+ "create_kvcache",
67
+ "create_cache_manager",
68
+ "BaseKVCache",
69
+ "KVCacheLayout",
70
+ "BaseCacheHandle",
71
+ "BaseCacheManager",
72
+ "SizeInfo",
73
+ "SUPPORTED_CACHE_MANAGER",
74
+ ]
zonos2/kvcache/base.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+ from abc import ABC, abstractmethod
5
+ from dataclasses import dataclass
6
+ from typing import NamedTuple, Tuple
7
+
8
+ import torch
9
+
10
+
11
+ class BaseKVCache(ABC):
12
+ """
13
+ Base class for key-value caches.
14
+ This class defines the interface for key-value caches used.
15
+ """
16
+
17
+ @abstractmethod
18
+ def k_cache(self, index: int) -> torch.Tensor: ...
19
+
20
+ @abstractmethod
21
+ def v_cache(self, index: int) -> torch.Tensor: ...
22
+
23
+ @abstractmethod
24
+ def store_kv(
25
+ self, k: torch.Tensor, v: torch.Tensor, out_loc: torch.Tensor, layer_id: int
26
+ ) -> None: ...
27
+
28
+ @property
29
+ @abstractmethod
30
+ def device(self) -> torch.device: ...
31
+
32
+ @property
33
+ @abstractmethod
34
+ def dtype(self) -> torch.dtype: ...
35
+
36
+ @property
37
+ @abstractmethod
38
+ def num_layers(self) -> int: ...
39
+
40
+
41
+ class KVCacheLayout(enum.Enum):
42
+ LayerFirst = enum.auto()
43
+ PageFirst = enum.auto()
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class BaseCacheHandle(ABC):
48
+ cached_len: int
49
+
50
+
51
+ class SizeInfo(NamedTuple):
52
+ evictable_size: int
53
+ protected_size: int
54
+
55
+ @property
56
+ def total_size(self) -> int:
57
+ return self.evictable_size + self.protected_size
58
+
59
+
60
+ class BaseCacheManager(ABC):
61
+ @abstractmethod
62
+ def match_prefix(self, input_ids: torch.Tensor) -> Tuple[BaseCacheHandle, torch.Tensor]:
63
+ """
64
+ Match prefix and return the indices of the matched prefix in the cache.
65
+ This operation will not modify the cache.
66
+ The returned indices is only safe to use when the handle is locked.
67
+
68
+ Args:
69
+ input_ids (torch.Tensor): The input ids to match. Shape: (seq_len,)
70
+ Returns:
71
+ handle (BaseCacheHandle): The handle to the matched prefix.
72
+ indices (torch.Tensor): The indices of the longest-matched prefix in the cache.
73
+ """
74
+
75
+ @abstractmethod
76
+ def lock_handle(self, handle: BaseCacheHandle, unlock: bool = False) -> None:
77
+ """
78
+ Lock or unlock a cache handle.
79
+ This operation will not modify the cache, but change the size info only.
80
+ When a handle is locked, it cannot be evicted.
81
+ Handles must be locked before the previously-returned tensor of `match_prefix` is used.
82
+ Otherwise it may be evicted by calling evict.
83
+
84
+ Args:
85
+ handle (BaseCacheHandle): The cache handle to lock or unlock.
86
+ unlock (bool): Whether to unlock the handle. Defaults to False.
87
+ """
88
+
89
+ @abstractmethod
90
+ def insert_prefix(self, input_ids: torch.Tensor, indices: torch.Tensor) -> int:
91
+ """
92
+ Insert a new prefix into the cache.
93
+ This operation will modify the cache.
94
+ Args:
95
+ input_ids (torch.Tensor): The input ids to insert. Shape: (seq_len,)
96
+ indices (torch.Tensor): The indices to store the new prefix. Shape: (seq_len,)
97
+
98
+ Returns:
99
+ int: The length of prefix that is already in the cache. This part is not
100
+ inserted, so the caller should free these indices.
101
+ """
102
+
103
+ @abstractmethod
104
+ def evict(self, size: int) -> torch.Tensor:
105
+ """
106
+ Evict some prefixes from the cache to free up space.
107
+ This operation will modify the cache.
108
+ Note that evict 0 is always safe and does nothing.
109
+ Note that the actual evict size may be larger than the requested size.
110
+ Args:
111
+ size (int): The size to evict.
112
+
113
+ Returns:
114
+ torch.Tensor: The indices evicted. Shape: (evict_size,)
115
+ Raises:
116
+ RuntimeError: If the requested size is larger than the evictable size.
117
+ """
118
+
119
+ @abstractmethod
120
+ def reset(self) -> None:
121
+ """Reset the cache manager and the underlying cache."""
122
+
123
+ @property
124
+ @abstractmethod
125
+ def size_info(self) -> SizeInfo:
126
+ """Get the size information of the cache."""
127
+
128
+ @abstractmethod
129
+ def check_integrity(self) -> None:
130
+ """Check the integrity of the cache. Raise an error if the cache is corrupted."""
zonos2/kvcache/mha_pool.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+ from zonos2.distributed import get_tp_info
5
+ from zonos2.utils import divide_even
6
+
7
+ from .base import BaseKVCache, KVCacheLayout
8
+
9
+
10
+ class MHAKVCache(BaseKVCache):
11
+ """
12
+ Base class for key-value caches.
13
+ This class defines the interface for key-value caches used in LLMs.
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ num_kv_heads: int,
19
+ num_layers: int,
20
+ head_dim: int,
21
+ num_pages: int,
22
+ dtype: torch.dtype,
23
+ kv_layout: KVCacheLayout,
24
+ device: torch.device,
25
+ ):
26
+ tp_info = get_tp_info()
27
+ local_kv_heads = divide_even(num_kv_heads, tp_info.size)
28
+ match kv_layout:
29
+ case KVCacheLayout.PageFirst:
30
+ kv_buffer = torch.empty(
31
+ (2, num_pages, num_layers, local_kv_heads, head_dim),
32
+ device=device,
33
+ dtype=dtype,
34
+ ).permute(0, 2, 1, 3, 4)
35
+ case KVCacheLayout.LayerFirst:
36
+ kv_buffer = torch.empty(
37
+ (2, num_layers, num_pages, local_kv_heads, head_dim),
38
+ device=device,
39
+ dtype=dtype,
40
+ )
41
+ case _:
42
+ raise ValueError(f"Unsupported kv_layout: {kv_layout}")
43
+ self._kv_buffer = kv_buffer.view(2, num_layers, num_pages, 1, local_kv_heads, head_dim)
44
+ self._num_layers = num_layers
45
+ self._k_buffer = self._kv_buffer[0]
46
+ self._v_buffer = self._kv_buffer[1]
47
+ self._device = device
48
+ self._storage_shape = (num_pages, local_kv_heads, head_dim)
49
+
50
+ def k_cache(self, index: int) -> torch.Tensor:
51
+ return self._k_buffer[index]
52
+
53
+ def v_cache(self, index: int) -> torch.Tensor:
54
+ return self._v_buffer[index]
55
+
56
+ def store_kv(
57
+ self, k: torch.Tensor, v: torch.Tensor, out_loc: torch.Tensor, layer_id: int
58
+ ) -> None:
59
+ from zonos2.kernel import store_cache
60
+
61
+ store_cache(
62
+ k_cache=self._k_buffer[layer_id].view(self._storage_shape),
63
+ v_cache=self._v_buffer[layer_id].view(self._storage_shape),
64
+ indices=out_loc,
65
+ k=k,
66
+ v=v,
67
+ )
68
+
69
+ @property
70
+ def device(self) -> torch.device:
71
+ return self._device
72
+
73
+ @property
74
+ def dtype(self) -> torch.dtype:
75
+ return self._kv_buffer.dtype
76
+
77
+ @property
78
+ def num_layers(self) -> int:
79
+ return self._num_layers
zonos2/kvcache/naive_manager.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Tuple
4
+
5
+ import torch
6
+
7
+ from .base import BaseCacheHandle, BaseCacheManager, SizeInfo
8
+
9
+
10
+ class NaiveCacheHandle(BaseCacheHandle):
11
+ pass
12
+
13
+
14
+ class NaiveCacheManager(BaseCacheManager):
15
+ def __init__(self, device: torch.device):
16
+ self.device = device
17
+ self.empty_tensor = torch.empty(0, dtype=torch.int32, device=device)
18
+ super().__init__()
19
+
20
+ def match_prefix(self, input_ids: torch.Tensor) -> Tuple[NaiveCacheHandle, torch.Tensor]:
21
+ _ = input_ids # unused
22
+ return NaiveCacheHandle(0), self.empty_tensor
23
+
24
+ def lock_handle(self, handle: BaseCacheHandle, unlock: bool = False) -> None:
25
+ _ = handle, unlock # unused
26
+
27
+ def insert_prefix(self, input_ids: torch.Tensor, indices: torch.Tensor) -> int:
28
+ assert len(indices) == len(input_ids)
29
+ return len(indices)
30
+
31
+ def evict(self, size: int) -> torch.Tensor:
32
+ if size == 0:
33
+ return self.empty_tensor
34
+ raise NotImplementedError("NaiveCacheManager does not support eviction.")
35
+
36
+ def reset(self) -> None:
37
+ pass
38
+
39
+ @property
40
+ def size_info(self) -> SizeInfo:
41
+ return SizeInfo(evictable_size=0, protected_size=0)
42
+
43
+ def check_integrity(self) -> None:
44
+ pass
zonos2/kvcache/radix_manager.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import heapq
4
+ import time
5
+ from dataclasses import dataclass
6
+ from typing import Dict, List, Tuple
7
+
8
+ import torch
9
+
10
+ from .base import BaseCacheHandle, BaseCacheManager, SizeInfo
11
+
12
+
13
+ class RadixTreeNode:
14
+ counter: int = 0
15
+
16
+ def __init__(self, tic: int | None = None) -> None:
17
+ self.children: Dict[int, RadixTreeNode] = {}
18
+ self._parent: RadixTreeNode | None = None
19
+ self.ref_count: int = 0
20
+ self.uuid = RadixTreeNode.counter
21
+ RadixTreeNode.counter += 1
22
+ self.timestamp = tic or time.monotonic_ns()
23
+
24
+ # these fields should be updated later
25
+ self._key: torch.Tensor
26
+ self._value: torch.Tensor
27
+ self._length: int
28
+
29
+ def set_key_value(self, key: torch.Tensor, value: torch.Tensor) -> None:
30
+ assert len(key) == len(value)
31
+ self._key = key
32
+ self._value = value
33
+ self._length = len(key)
34
+
35
+ def set_parent(self, parent: RadixTreeNode) -> None:
36
+ self._parent = parent
37
+ parent.children[int(self._key[0].item())] = self
38
+
39
+ @property
40
+ def length(self) -> int:
41
+ return self._length
42
+
43
+ @property
44
+ def parent(self) -> RadixTreeNode:
45
+ assert self._parent is not None
46
+ return self._parent
47
+
48
+ @property
49
+ def value(self) -> torch.Tensor:
50
+ return self._value
51
+
52
+ def is_root(self) -> bool:
53
+ return self._parent is None
54
+
55
+ def is_leaf(self) -> bool:
56
+ return len(self.children) == 0
57
+
58
+ def get_match_len(self, input_ids: torch.Tensor) -> int:
59
+ from zonos2.kernel import fast_compare_key
60
+
61
+ # compare key and input_ids, find the first diff
62
+ return fast_compare_key(self._key, input_ids)
63
+
64
+ def _split_at(self, pos: int) -> RadixTreeNode:
65
+ assert 0 < pos < self.length
66
+ parent = self.parent
67
+
68
+ new_node = RadixTreeNode(self.timestamp)
69
+ new_node.set_key_value(self._key[:pos], self._value[:pos])
70
+ new_node.set_parent(parent)
71
+ new_node.ref_count = self.ref_count
72
+
73
+ self.set_key_value(self._key[pos:], self._value[pos:])
74
+ self.set_parent(new_node)
75
+
76
+ return new_node
77
+
78
+ def __lt__(self, other: RadixTreeNode) -> bool:
79
+ return self.timestamp < other.timestamp
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class RadixCacheHandle(BaseCacheHandle):
84
+ node: RadixTreeNode
85
+
86
+
87
+ class RadixCacheManager(BaseCacheManager):
88
+ def __init__(self, device: torch.device):
89
+ self.device = device
90
+ self.empty_tensor = torch.empty(0, dtype=torch.int32, device=device)
91
+ super().__init__()
92
+ self.root_node = RadixTreeNode()
93
+ self.root_node.ref_count = 1 # root is always protected
94
+ self.evictable_size = 0
95
+ self.protected_size = 0
96
+
97
+ def lock_handle(self, handle: BaseCacheHandle, unlock: bool = False) -> None:
98
+ assert isinstance(handle, RadixCacheHandle)
99
+ node = handle.node
100
+ if unlock:
101
+ while not node.is_root():
102
+ node.ref_count -= 1
103
+ assert node.ref_count >= 0
104
+ if node.ref_count == 0:
105
+ self.evictable_size += node.length
106
+ self.protected_size -= node.length
107
+ node = node.parent
108
+ else:
109
+ while not node.is_root():
110
+ if node.ref_count == 0:
111
+ self.evictable_size -= node.length
112
+ self.protected_size += node.length
113
+ node.ref_count += 1
114
+ node = node.parent
115
+
116
+ def match_prefix(self, input_ids: torch.Tensor) -> Tuple[RadixCacheHandle, torch.Tensor]:
117
+ node, prefix_len = self._walk(input_ids)
118
+ if prefix_len == 0:
119
+ assert node.is_root() and node is self.root_node and prefix_len == 0
120
+ return RadixCacheHandle(prefix_len, node), self.empty_tensor
121
+ value_list: List[torch.Tensor] = []
122
+ matched_node = node
123
+ while not node.is_root():
124
+ value_list.append(node.value)
125
+ node = node.parent
126
+ value_list.reverse()
127
+ return RadixCacheHandle(prefix_len, matched_node), torch.cat(value_list)
128
+
129
+ def insert_prefix(self, input_ids: torch.Tensor, indices: torch.Tensor) -> int:
130
+ node, prefix_len = self._walk(input_ids)
131
+ assert prefix_len <= len(input_ids)
132
+ if prefix_len < len(input_ids):
133
+ new_node = RadixTreeNode()
134
+ new_node.set_key_value(input_ids[prefix_len:], indices[prefix_len:])
135
+ new_node.set_parent(node)
136
+ self.evictable_size += new_node.length
137
+ return prefix_len
138
+
139
+ def _walk(self, input_ids: torch.Tensor) -> Tuple[RadixTreeNode, int]:
140
+ prefix_len = 0
141
+ indice_len = len(input_ids)
142
+ node = self.root_node
143
+ tic = time.monotonic_ns()
144
+
145
+ while prefix_len < indice_len:
146
+ this_id = int(input_ids[prefix_len].item())
147
+ if this_id not in node.children:
148
+ return node, prefix_len
149
+
150
+ node = node.children[this_id]
151
+
152
+ # NOTE: at least 1 char is matched, so match_len >= 1
153
+ match_len = node.get_match_len(input_ids[prefix_len:])
154
+ prefix_len += match_len
155
+
156
+ # need to split the node if not fully matched
157
+ if match_len != node.length:
158
+ node = node._split_at(match_len)
159
+ return node, prefix_len
160
+
161
+ # update timestamp for accessed node
162
+ node.timestamp = tic
163
+
164
+ return node, prefix_len
165
+
166
+ def evict(self, size: int) -> torch.Tensor:
167
+ if size == 0:
168
+ return self.empty_tensor
169
+ assert (
170
+ size <= self.evictable_size
171
+ ), f"Cannot evict {size}, only {self.evictable_size} is evictable"
172
+
173
+ leave_nodes = self._collect_leave_nodes_for_evict()
174
+ heapq.heapify(leave_nodes)
175
+ evicted_indices: List[torch.Tensor] = []
176
+ evicted_size = 0
177
+
178
+ while evicted_size < size:
179
+ assert (
180
+ leave_nodes
181
+ ), f"Cannot evict enough cache, need {size}, only {evicted_size} evicted"
182
+ node = heapq.heappop(leave_nodes)
183
+ assert node.ref_count == 0 and node.is_leaf() and not node.is_root()
184
+ evicted_size += node.length
185
+ evicted_indices.append(node.value)
186
+ self.evictable_size -= node.length
187
+ parent = node.parent
188
+ del parent.children[int(node._key[0].item())]
189
+ # NOTE: root is always protected, so won't be evicted
190
+ if parent.is_leaf() and parent.ref_count == 0:
191
+ heapq.heappush(leave_nodes, parent)
192
+
193
+ return torch.cat(evicted_indices)
194
+
195
+ def _collect_leave_nodes_for_evict(self) -> List[RadixTreeNode]:
196
+ nodes: List[RadixTreeNode] = [self.root_node]
197
+ leave_nodes: List[RadixTreeNode] = []
198
+
199
+ while len(nodes) > 0:
200
+ node = nodes.pop()
201
+ if node.is_leaf():
202
+ if node.ref_count == 0:
203
+ leave_nodes.append(node)
204
+ else:
205
+ for child in node.children.values():
206
+ nodes.append(child)
207
+
208
+ return leave_nodes
209
+
210
+ def reset(self) -> None:
211
+ raise NotImplementedError("RadixManager.reset is not implemented")
212
+
213
+ @property
214
+ def size_info(self) -> SizeInfo:
215
+ return SizeInfo(
216
+ evictable_size=self.evictable_size,
217
+ protected_size=self.protected_size,
218
+ )
219
+
220
+ def check_integrity(self) -> None:
221
+ pass
zonos2/layers/__init__.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .activation import silu_and_mul
2
+ from .attention import AttentionLayer
3
+ from .base import BaseOP, OPList, StateLessOP
4
+ from .embedding import ParallelLMHead, VocabParallelEmbedding
5
+ from .linear import (
6
+ ChunkedLinear,
7
+ LinearColParallelMerged,
8
+ LinearOProj,
9
+ LinearQKVMerged,
10
+ LinearRowParallel,
11
+ )
12
+ from .norm import RMSNorm, RMSNormFused
13
+ from .rotary import get_rope, set_rope_device
14
+
15
+ __all__ = [
16
+ "silu_and_mul",
17
+ "AttentionLayer",
18
+ "BaseOP",
19
+ "StateLessOP",
20
+ "OPList",
21
+ "VocabParallelEmbedding",
22
+ "ParallelLMHead",
23
+ "ChunkedLinear",
24
+ "LinearColParallelMerged",
25
+ "LinearRowParallel",
26
+ "LinearOProj",
27
+ "LinearQKVMerged",
28
+ "RMSNorm",
29
+ "RMSNormFused",
30
+ "get_rope",
31
+ "set_rope_device",
32
+ ]
zonos2/layers/activation.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ import torch
7
+
8
+
9
+ def silu_and_mul(x: torch.Tensor) -> torch.Tensor:
10
+ from flashinfer import silu_and_mul
11
+
12
+ return silu_and_mul(x)
13
+
14
+
15
+ __all__ = ["silu_and_mul"]