tt-hous commited on
Commit
9dd40b4
·
verified ·
1 Parent(s): 0608a65

Add files using upload-large-folder tool

Browse files
Files changed (50) hide show
  1. README.md +75 -0
  2. code/models/common/README.md +319 -0
  3. code/models/common/auto_compose.py +161 -0
  4. code/models/common/device_utils.py +46 -0
  5. code/models/common/distribute_as.py +129 -0
  6. code/models/common/generation_utils.py +279 -0
  7. code/models/common/helper_funcs.py +43 -0
  8. code/models/common/lightweightmodule.py +12 -0
  9. code/models/common/llama_models.py +212 -0
  10. code/models/common/metrics.py +488 -0
  11. code/models/common/model_capabilities.py +28 -0
  12. code/models/common/rmsnorm.py +258 -0
  13. code/models/common/tensor_utils.py +229 -0
  14. code/models/common/utility_functions.py +1266 -0
  15. code/models/common/utils.py +53 -0
  16. code/models/common/validation_tools.py +719 -0
  17. code/models/common/weight_cache.py +426 -0
  18. code/models/tt_transformers/Host_Mem_Profile.md +88 -0
  19. code/models/tt_transformers/PERF.md +247 -0
  20. code/models/tt_transformers/README.md +388 -0
  21. code/models/tt_transformers/conftest.py +36 -0
  22. code/models/tt_transformers/host_mem_profiler.py +188 -0
  23. code/models/tt_transformers/scripts/op_perf_results.py +190 -0
  24. code/models/tt_transformers/scripts/repack_weights_70b.py +96 -0
  25. code/models/tt_transformers/scripts/repack_weights_90b.py +193 -0
  26. code/models/tt_transformers/tests/conftest.py +55 -0
  27. code/models/tt_transformers/tests/generate_reference_outputs.py +172 -0
  28. code/models/tt_transformers/tests/generate_reference_outputs.sh +82 -0
  29. code/models/tt_transformers/tests/test_attention_prefill.py +277 -0
  30. code/models/tt_transformers/tests/test_batched_prefill_slots.py +122 -0
  31. code/models/tt_transformers/tests/test_ci_dispatch.py +54 -0
  32. code/models/tt_transformers/tests/test_decoder.py +281 -0
  33. code/models/tt_transformers/tests/test_device_perf.py +362 -0
  34. code/models/tt_transformers/tests/test_hybrid_attention_for_causal_lm.py +143 -0
  35. code/models/tt_transformers/tests/test_interleaved_to_sharded.py +89 -0
  36. code/models/tt_transformers/tests/test_llama90b_decoder_json.py +72 -0
  37. code/models/tt_transformers/tests/test_lm_head.py +110 -0
  38. code/models/tt_transformers/tests/test_lm_head_cache.py +42 -0
  39. code/models/tt_transformers/tests/test_mha_wo_sharding.py +81 -0
  40. code/models/tt_transformers/tests/test_mlp.py +133 -0
  41. code/models/tt_transformers/tests/test_model_config_utils.py +48 -0
  42. code/models/tt_transformers/tests/test_model_prefill.py +322 -0
  43. code/models/tt_transformers/tests/test_rope_utils.py +20 -0
  44. code/models/tt_transformers/tests/test_utils.py +439 -0
  45. code/models/tt_transformers/tests/test_vllm_kv_cache.py +141 -0
  46. code/models/tt_transformers/tests/test_warm_cache_marker.py +437 -0
  47. image/index.json +1 -0
  48. image/manifest.json +1 -0
  49. image/oci-layout +1 -0
  50. requirements.lock +423 -0
README.md ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ tags:
3
+ - blackhole
4
+ - tt-model-cache
5
+ - tt-model-container
6
+ - vllm-plugin
7
+ ---
8
+
9
+ # qwen3.8-flash-next-p300x2
10
+
11
+ Qwen3.8-Flash-Next served as an OpenAI-compatible endpoint on two P300
12
+ boards (four Blackhole devices). The optimized runtime uses TP4+EP4 with
13
+ all 512 routed experts resident on-device; only the PLE n-gram table and
14
+ its sparse row assembly remain host-backed.
15
+
16
+ Runs on **p300x2** (mesh `(4, 1)`) — 262,144-token context, up to 2 concurrent sequences.
17
+
18
+ Packaged and published with [tt-model-manager](https://github.com/tenstorrent/tt-model-manager) 0.1.0 (manifest schema 5.1).
19
+
20
+ ## Quickstart
21
+
22
+ ```bash
23
+ tt-model pull tt-hous/qwen3.8-flash-next-p300x2
24
+ tt-model serve tt-hous/qwen3.8-flash-next-p300x2
25
+ ```
26
+
27
+ `pull` downloads the Docker image and the [`Qwen/Qwen3.8-Flash-Next`](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) weights at `f5d08274bafd880402bd16f5e3e6c514136ec06c` (into your HF cache; they are not in the image). `serve` starts an OpenAI-compatible server on port 8000; the first start compiles kernels for your device, which takes several minutes, and the server is ready when it logs `Application startup complete`.
28
+
29
+ ### Serve on two P300 boards (four chips)
30
+
31
+ ```bash
32
+ tt-model serve tt-hous/qwen3.8-flash-next-p300x2 --follow
33
+ ```
34
+
35
+ The package pins the exact Hugging Face checkpoint revision, vLLM 0.24.0,
36
+ Transformers 5.16.0, and the Qwen3.8-capable TT plugin revision. It exposes
37
+ two virtual request slots over a physical batch-1 decode trace.
38
+
39
+ ## Endpoint performance
40
+
41
+ Measured with `vllm bench serve` against this packaged endpoint on four
42
+ devices. Requests use exact random token lengths, greedy temperature 0,
43
+ EOS ignored, and an unmeasured warmup probe. Decode tok/s/user is derived
44
+ from mean TPOT; aggregate output throughput includes TTFT.
45
+
46
+ | ISL | OSL | Max concurrency | Requests | Median TTFT | Mean TPOT | Decode tok/s/user | Aggregate output tok/s |
47
+ | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
48
+ | 128 | 128 | 1 | 3/3 | 504 ms | 181.61 ms | 5.506 | 5.229 |
49
+ | 1,024 | 128 | 1 | 3/3 | 3.818 s | 181.70 ms | 5.504 | 4.625 |
50
+ | 4,096 | 128 | 1 | 3/3 | 15.052 s | 181.81 ms | 5.500 | 3.289 |
51
+ | 128 | 128 | 2 | 4/4 | 1.955 s | 368.36 ms | 2.715 | 5.253 |
52
+
53
+ The two virtual request slots share a physical-B1 decode trace. Concurrency
54
+ 2 is state-safe but does not improve aggregate throughput, so concurrency 1
55
+ is the headline per-user profile. The canonical direct model harness is
56
+ faster at 99.940 ms TPOT / 10.006 tok/s/user; the endpoint figures include
57
+ vLLM scheduling, virtual-slot state, PLE service, and plugin handoff.
58
+
59
+ Runtime telemetry reported all 48 expert layers resident with
60
+ 16,986,931,200 expert bytes per device, zero expert host-store bytes, zero
61
+ expert H2D, and zero route D2H/stall time. Only PLE n-gram row lookup and
62
+ selected-row DMA remain host-backed.
63
+
64
+ ## Provenance
65
+
66
+ The exact sources the image was built from — `code/` in this repo is byte-identical to the model code inside the image:
67
+
68
+ | component | built from |
69
+ | --- | --- |
70
+ | tt-metal | a local checkout — commit not published *(dirty tree — the image includes uncommitted changes)* |
71
+ | vLLM | [`v0.24.0`](https://github.com/vllm-project/vllm/releases/tag/v0.24.0) |
72
+ | vllm-tt-plugin | a local checkout — commit not published |
73
+ | `code/` digest | `bc605f7e1f6f2342` (sha256, first 16 hex digits) |
74
+ | built | 2026-09-03T04:02:56+00:00 by tt-model 0.1.0 |
75
+
code/models/common/README.md ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TTNN Validation & Testing Utilities
2
+
3
+ Helpers for validating TTNN computations against reference implementations and
4
+ for moving tensors between TTNN and PyTorch. The public API is implemented
5
+ across `models.common.validation_tools`, `models.common.metrics`,
6
+ `models.common.auto_compose`, and `models.common.distribute_as`, and is
7
+ exercised in:
8
+
9
+ - `models/common/tests/test_validation_tools.py`
10
+ - `models/common/tests/test_metrics.py`
11
+ - `models/common/tests/test_auto_compose.py`
12
+ - `models/common/tests/test_distribute_as.py`
13
+ - `models/common/tests/host/test_metrics_pytorch_only.py`
14
+
15
+ The examples in these tests are the most up‑to‑date reference for usage.
16
+
17
+ ## Quick Start – host reference (`compare_to_torch`)
18
+
19
+ Use `compare_to_torch` when your reference implementation is a PyTorch function.
20
+ Inputs and outputs are automatically converted between TTNN and PyTorch.
21
+
22
+ ```python
23
+ import torch
24
+ import ttnn
25
+ from models.common.validation_tools import compare_to_torch, Metric, get_validation_registry
26
+
27
+
28
+ @compare_to_torch(
29
+ reference_fn=torch.matmul,
30
+ metric_tolerances={
31
+ Metric.MAX_ABS_ERROR: 1e-1,
32
+ Metric.PCC: 0.99,
33
+ },
34
+ )
35
+ def ttnn_matmul(a, b):
36
+ # a, b are TTNN tensors (possibly sharded)
37
+ return ttnn.matmul(a, b)
38
+
39
+
40
+ def run_example(device: ttnn.MeshDevice):
41
+ m, n, k = 16, 24, 12
42
+ a = torch.randn(1, m, k, dtype=torch.bfloat16)
43
+ b = torch.randn(1, k, n, dtype=torch.bfloat16)
44
+
45
+ a_tt = ttnn.from_torch(a.unsqueeze(0), device=device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
46
+ b_tt = ttnn.from_torch(b.unsqueeze(0), device=device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
47
+
48
+ _ = ttnn_matmul(a_tt, b_tt)
49
+
50
+ registry = get_validation_registry()
51
+ registry.print_report()
52
+ ```
53
+
54
+ Note:
55
+ - When the signature of the decorated function is different from the reference function, `input_to_torch` and `output_to_torch` can be used to map the inputs and outputs between the decorated function and the reference function.
56
+ - See `models/common/tests/test_validation_tools.py::test_validation_matmul` for a real test using
57
+ this pattern.
58
+
59
+ ## Quick Start – TTNN reference (`compare_to_ttnn`)
60
+
61
+ Use `compare_to_ttnn` when both your implementation and reference are TTNN‑based
62
+ and you want metrics computed directly on device.
63
+
64
+ ```python
65
+ import torch
66
+ import ttnn
67
+ from models.common.validation_tools import compare_to_ttnn
68
+
69
+
70
+ def torch_rms_norm(x, weight, eps=1e-6):
71
+ var = x.pow(2).mean(-1, keepdim=True)
72
+ return weight * x * torch.rsqrt(var + eps)
73
+
74
+
75
+ class DeviceValidatedRMSNorm:
76
+ def __init__(self, weight: torch.Tensor, eps: float, device: ttnn.MeshDevice):
77
+ self.eps = eps
78
+ self.device = device
79
+ self.weight_torch = weight
80
+ self.weight = ttnn.from_torch(
81
+ weight.unsqueeze(0).unsqueeze(0), device=device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT
82
+ )
83
+
84
+ def _reference_impl(self, x):
85
+ x_torch = ttnn.to_torch(x).squeeze(0)
86
+ y_torch = torch_rms_norm(x_torch, self.weight_torch, self.eps)
87
+ return ttnn.from_torch(
88
+ y_torch.unsqueeze(0), device=self.device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT
89
+ )
90
+
91
+ @compare_to_ttnn(reference_fn=lambda self, x: self._reference_impl(x))
92
+ def __call__(self, x):
93
+ x_sq = ttnn.mul(x, x)
94
+ mean_x_sq = ttnn.mean(x_sq, dim=-1, keepdim=True)
95
+ rms = ttnn.sqrt(ttnn.add(mean_x_sq, self.eps))
96
+ x_norm = ttnn.mul(x, ttnn.reciprocal(rms))
97
+ return ttnn.mul(x_norm, self.weight)
98
+ ```
99
+ Note:
100
+ - When the signature of the decorated function is different from the reference function, `input_to_ttnn` and `output_to_ttnn` can be used to map the inputs and outputs between the decorated function and the reference function.
101
+ - This mirrors the pattern used by `DeviceValidatedRMSNorm` in
102
+ `models/common/tests/test_validation_tools.py`.
103
+
104
+ ## Features
105
+
106
+ - **Decorator‑based validation** – Wrap TTNN functions or methods and compare
107
+ them against PyTorch or TTNN references.
108
+ - **Host and device modes** – `compare_to_torch` (PyTorch reference) and
109
+ `compare_to_ttnn` (TTNN reference).
110
+ - **TTNN‑native metrics** – When both outputs are TTNN tensors, metrics are
111
+ computed on device with minimal host transfer.
112
+ - **Configurable tolerances** – Per‑metric tolerances via the `Metric` enum,
113
+ string keys, or `MetricSpec`.
114
+ - **Custom metrics** – Inject your own metric functions.
115
+ - **Global registry** – Collects all validation runs for reporting.
116
+ - **Easy disabling** – Turn validation on/off globally without changing call
117
+ sites.
118
+
119
+ ## Core Components
120
+
121
+ ### Validation decorators
122
+
123
+ All decorators live in `models.common.validation_tools`:
124
+
125
+ - `compare_to_torch(reference_fn, *, input_to_torch=None, output_to_torch=None, metric_tolerances=None, enabled=True, raise_exceptions=False, return_reference_output=False)`
126
+ - Use when `reference_fn` is a PyTorch implementation.
127
+ - By default, all TTNN tensors in the arguments/outputs are converted to
128
+ PyTorch via `to_torch_auto_compose`.
129
+ - Optional `input_to_torch(*args, **kwargs)` lets you override how inputs
130
+ are mapped to the reference.
131
+ - Optional `output_to_torch(output)` converts the implementation output
132
+ before metrics are computed.
133
+
134
+ - `compare_to_ttnn(reference_fn, *, input_to_ttnn=None, output_to_ttnn=None, metric_tolerances=None, enabled=True, raise_exceptions=False, return_reference_output=False)`
135
+ - Use when `reference_fn` consumes and returns TTNN tensors.
136
+ - Optional `input_to_ttnn(*args, **kwargs)` lets you override how inputs
137
+ are mapped to the reference.
138
+ - Optional `output_to_ttnn(output)` converts the implementation output
139
+ before metrics are computed.
140
+ - If both implementation and reference return TTNN tensors, metrics run
141
+ entirely on device.
142
+
143
+ In both cases, decorating a function records a `ValidationResult` in the global
144
+ `ValidationRegistry` every time the function is called (unless disabled).
145
+
146
+ ### Metrics
147
+
148
+ Metric utilities are implemented in `models.common.metrics`:
149
+
150
+ - `compute_max_abs_error(impl, ref)` – max absolute error.
151
+ - `compute_mean_abs_error(impl, ref)` – mean absolute error.
152
+ - `compute_pcc(impl, ref)` – Pearson correlation coefficient; uses TTNN
153
+ operations when possible and falls back to host.
154
+ - `comp_allclose(impl, ref, rtol=..., atol=...)` – allclose check plus a
155
+ detailed delta string.
156
+ - `DEFAULT_METRICS` – dict with built‑in metrics (`"max_abs_error"`,
157
+ `"mean_abs_error"`, `"pcc"`).
158
+
159
+ Metrics support both TTNN and PyTorch tensors.
160
+
161
+ ### Registry and control functions
162
+
163
+ From `models.common.validation_tools`:
164
+
165
+ - `get_validation_registry() -> ValidationRegistry`
166
+ - Holds all `ValidationResult` objects.
167
+ - Provides `get_summary()` and `print_report(verbose: bool = False)`.
168
+
169
+ - `enable_validation(enabled: bool = True)`
170
+ - Globally enable/disable validation; when disabled, decorators become
171
+ transparent wrappers.
172
+
173
+ - `clear_validation_results()`
174
+ - Clear all accumulated validation results.
175
+
176
+ `ValidationResult` includes:
177
+
178
+ - `function_name`
179
+ - `passed` (bool)
180
+ - `metrics` – map of metric name → per‑metric result (value, passed, error)
181
+ - `execution_time_impl`, `execution_time_ref`
182
+ - `timestamp`
183
+ - `logs` – optional debug strings
184
+
185
+ ### Auto‑compose helper
186
+
187
+ `to_torch_auto_compose` lives in `models.common.auto_compose`.
188
+
189
+ It converts an arbitrary TTNN tensor (including sharded/replicated multi‑device
190
+ tensors) to a single PyTorch tensor by automatically choosing the appropriate
191
+ mesh composer.
192
+
193
+ It is heavily used in:
194
+
195
+ - `test_auto_compose.py`
196
+ - `test_distribute_as.py`
197
+ - all `compare_to_torch`‑based examples.
198
+
199
+ ## Usage Patterns
200
+
201
+ High‑level patterns illustrated in the tests:
202
+
203
+ 1. **Host reference with explicit input mapping**
204
+ - See `HostValidatedRMSNorm` in `models/common/tests/test_validation_tools.py`.
205
+ - Uses `compare_to_torch` with `input_to_torch` to map TTNN inputs and
206
+ TTNN weights to a pure‑PyTorch reference function.
207
+
208
+ 2. **TTNN reference (on‑device metrics)**
209
+ - See `DeviceValidatedRMSNorm` in `models/common/tests/test_validation_tools.py`.
210
+ - Uses `compare_to_ttnn` where both implementation and reference return
211
+ TTNN tensors; metrics run on device.
212
+
213
+ 3. **Simple library calls**
214
+ - See `ttnn_matmul` and `ttnn_matmul_reverse` in `models/common/tests/test_validation_tools.py`.
215
+ - `compare_to_torch(reference_fn=torch.matmul, ...)` with optional
216
+ `input_to_torch` remapping.
217
+
218
+ 4. **Checkpoint / `from_torch` validation**
219
+ - See `from_torch_checkpoint` in `models/common/tests/test_validation_tools.py`.
220
+ - Validates a direct `ttnn.from_torch(...)` call using `compare_to_torch`
221
+ and `output_to_torch`.
222
+
223
+ 5. **Custom metric via `MetricSpec`**
224
+ - See `ttnn_matmul_metric_spec` in `models/common/tests/test_validation_tools.py`
225
+ and `MetricSpec` usage in `models/common/tests/host/test_metrics_pytorch_only.py`.
226
+ - Use `MetricSpec(tolerance=..., higher_is_better=..., compute_fn=...)`
227
+ in `metric_tolerances`.
228
+
229
+ 6. **Non‑decorator usage**
230
+ - `test_validation_non_decorator_class_vs_class_torch` demonstrates calling
231
+ `compare_to_torch` in a more manual, non‑decorator style between two
232
+ callable classes.
233
+
234
+ ## Default Metrics and Tolerances
235
+
236
+ When `metric_tolerances` is omitted, the framework uses sensible defaults:
237
+
238
+ - `Metric.MAX_ABS_ERROR` with tolerance `1e-2`
239
+ - `Metric.PCC` with tolerance `0.99`
240
+
241
+ If you pass a `metric_tolerances` dict, keys can be:
242
+
243
+ - `Metric` enum members (recommended), e.g. `Metric.MAX_ABS_ERROR`
244
+ - strings (`"max_abs_error"`, `"mean_abs_error"`, `"pcc"`)
245
+ - arbitrary names when used with `MetricSpec`
246
+
247
+ Values can be:
248
+
249
+ - a float tolerance (uses the built‑in metric)
250
+ - a `MetricSpec` instance to define a custom metric and tolerance
251
+
252
+ Example:
253
+
254
+ ```python
255
+ from models.common.validation_tools import Metric, MetricSpec
256
+ from models.common.metrics import compute_pcc
257
+
258
+
259
+ @compare_to_torch(
260
+ reference_fn=torch.matmul,
261
+ metric_tolerances={
262
+ Metric.PCC: MetricSpec(tolerance=0.99, higher_is_better=True, compute_fn=compute_pcc),
263
+ Metric.MAX_ABS_ERROR: 1.5e-1,
264
+ },
265
+ )
266
+ def ttnn_matmul_metric_spec(a, b):
267
+ return ttnn.matmul(a, b)
268
+ ```
269
+
270
+ ## Testing
271
+
272
+ The local test suite in `models/common/tests` shows end‑to‑end usage:
273
+
274
+ - `test_validation_tools.py`
275
+ - Core decorator usage, registry behaviour, error handling, custom metrics.
276
+ - `test_metrics.py`
277
+ - Numerical correctness of device and host metric functions.
278
+ - `host/test_metrics_pytorch_only.py`
279
+ - Pure‑PyTorch metric tests.
280
+ - `test_auto_compose.py`
281
+ - Auto‑composition of sharded/replicated TTNN tensors into PyTorch.
282
+ - `test_distribute_as.py`
283
+ - Distribution helpers (`from_torch_dist_as`) that mirror an existing TTNN
284
+ tensor’s topology.
285
+
286
+ Example commands (run from the repo root, with TTNN available):
287
+
288
+ ```bash
289
+ python -m pytest models/common/tests/test_validation_tools.py -v
290
+ python -m pytest models/common/tests/test_metrics.py -v
291
+ python -m pytest models/common/tests/host/test_metrics_pytorch_only.py -v
292
+ ```
293
+
294
+ ## API Reference (public surface)
295
+
296
+ All symbols below are imported from `models.common.validation_tools` and `models.common.metrics`:
297
+
298
+ - Decorators:
299
+ - `compare_to_torch`
300
+ - `compare_to_ttnn`
301
+ - Registry and control:
302
+ - `ValidationResult`
303
+ - `ValidationRegistry`
304
+ - `get_validation_registry`
305
+ - `enable_validation`
306
+ - `clear_validation_results`
307
+ - Metrics:
308
+ - `Metric` (enum: `MAX_ABS_ERROR`, `MEAN_ABS_ERROR`, `PCC`)
309
+ - `MetricSpec`
310
+ - `compute_max_abs_error`
311
+ - `compute_mean_abs_error`
312
+ - `compute_pcc`
313
+ - `comp_allclose`
314
+ - `DEFAULT_METRICS`
315
+ - Auto‑compose:
316
+ - `to_torch_auto_compose`
317
+
318
+ For concrete, runnable examples of each API, see the tests listed at the top
319
+ of this document.
code/models/common/auto_compose.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """
5
+ Automatic composition of multi-device sharded tensors using TensorTopology.
6
+
7
+ This module provides utilities to infer the correct MeshToTensor composer from a
8
+ sharded ttnn.Tensor's topology metadata and use it to compose shards on host.
9
+ """
10
+
11
+ from typing import Optional
12
+
13
+ import torch
14
+ from loguru import logger
15
+
16
+ import ttnn
17
+
18
+ # ======================================================================================
19
+ # Public API
20
+ # ======================================================================================
21
+
22
+
23
+ def to_torch_auto_compose(tensor: ttnn.Tensor, device: Optional[ttnn.MeshDevice] = None) -> torch.Tensor:
24
+ """
25
+ Convert a (possibly multi-device) TTNN tensor to torch, automatically
26
+ composing shards based on the tensor's topology.
27
+
28
+ Args:
29
+ tensor: The distributed tensor to convert
30
+ device: Optional MeshDevice to use when the tensor lives on host
31
+
32
+ Returns:
33
+ PyTorch tensor with shards composed
34
+ """
35
+ composer = _infer_mesh_composer_from_topology(tensor, device=device)
36
+ try:
37
+ return ttnn.to_torch(tensor, mesh_composer=composer)
38
+ except Exception as e:
39
+ logger.error(f"Failed to convert tensor to torch with mesh_composer: {e}")
40
+ raise
41
+
42
+
43
+ def extract_tensor_topology_info(
44
+ tensor: ttnn.Tensor,
45
+ ) -> tuple[list[object], list[int]]:
46
+ """
47
+ Extract placements and distribution shape from a tensor's topology.
48
+
49
+ Returns:
50
+ (placements, dist_shape)
51
+ """
52
+ topology = tensor.tensor_topology()
53
+ placements = topology.placements()
54
+ dist_shape = list(topology.distribution_shape())
55
+ return placements, dist_shape
56
+
57
+
58
+ def get_device_from_tensor(tensor: ttnn.Tensor) -> Optional[ttnn.MeshDevice]:
59
+ """Get device from tensor or fallback to provided mesh_device."""
60
+ device = tensor.device()
61
+ # tensor.device() returns None if the tensor is on the host (ttnn/core/tensor/tensor.cpp --> Tensor::device())
62
+ if device is None:
63
+ logger.debug("tensor.device() returns None, tensor is on the host")
64
+ else:
65
+ logger.debug(f"tensor.device() returns {device}")
66
+
67
+ return device
68
+
69
+
70
+ # ======================================================================================
71
+ # Private Implementation
72
+ # ======================================================================================
73
+
74
+
75
+ def _infer_mesh_composer_from_topology(
76
+ tensor: ttnn.Tensor, *, device: Optional[ttnn.MeshDevice] = None
77
+ ) -> Optional[ttnn.CppMeshToTensor]:
78
+ """
79
+ Return a MeshToTensor composer inferred from the tensor's TensorTopology,
80
+ or None if no composition is needed (fully replicated, single-device).
81
+
82
+ Note: For ND meshes with replicated dimensions, the composer will concatenate
83
+ all replicas, resulting in duplicated data. Callers may want to slice the
84
+ result if only one copy is desired.
85
+
86
+ Args:
87
+ tensor: The distributed tensor to infer composer for
88
+
89
+ Returns:
90
+ MeshToTensor composer or None if no composition needed
91
+ """
92
+ placements, dist_shape = extract_tensor_topology_info(tensor)
93
+
94
+ # No distribution or trivial 1-device case
95
+ if len(dist_shape) == 0 or (len(dist_shape) == 1 and dist_shape[0] == 1):
96
+ return None
97
+
98
+ tensor_device = get_device_from_tensor(tensor)
99
+ mesh_device = tensor_device or device
100
+ if mesh_device is None:
101
+ # As a last resort, try default device for backward-compatibility
102
+ mesh_device = ttnn.GetDefaultDevice()
103
+ if mesh_device is None:
104
+ raise RuntimeError(
105
+ "Tensor is on host and no mesh_device provided. "
106
+ "Pass device=... to to_torch_auto_compose or set a default via ttnn.SetDefaultDevice(...)."
107
+ )
108
+
109
+ # Must match length (should be guaranteed by C++ TT_FATAL in ttnn/core/distributed/distributed_tensor.cpp)
110
+ assert len(dist_shape) == len(placements)
111
+
112
+ if len(dist_shape) == 1 and mesh_device.shape.dims() == 1:
113
+ return _compose_1d_sharded(mesh_device, placements, dist_shape)
114
+ else:
115
+ # N >= 2 dimensions
116
+ return _compose_nd_sharded(mesh_device, placements, dist_shape)
117
+
118
+
119
+ def _compose_1d_sharded(
120
+ device: ttnn.MeshDevice,
121
+ placements: list[object],
122
+ dist_shape: list[int],
123
+ ) -> Optional[ttnn.CppMeshToTensor]:
124
+ """Handle 1D case - returns None if fully replicated."""
125
+ p = placements[0]
126
+ if isinstance(p, ttnn.PlacementShard):
127
+ # Use ND composer with shape override to match the tensor's distribution
128
+ composer_cfg = ttnn.MeshComposerConfig(dims=[p.dim], mesh_shape_override=ttnn.MeshShape(dist_shape))
129
+ return ttnn.create_mesh_composer(device, composer_cfg)
130
+ # Fully replicated - no composition needed
131
+ return None
132
+
133
+
134
+ def _compose_nd_sharded(
135
+ device: ttnn.MeshDevice,
136
+ placements: list[object],
137
+ dist_shape: list[int],
138
+ ) -> ttnn.CppMeshToTensor:
139
+ """
140
+ Handle ND (N>=2) case.
141
+
142
+ For replicated mesh dims, we use dim 0 as convention (the composed result
143
+ will include all replicas concatenated, which is typically not desired but
144
+ is how the C++ API works).
145
+ """
146
+ dims = []
147
+ shape_override = []
148
+ for i, p in enumerate(placements):
149
+ if isinstance(p, ttnn.PlacementShard):
150
+ dims.append(p.dim)
151
+ shape_override.append(dist_shape[i])
152
+ else:
153
+ assert isinstance(p, ttnn.PlacementReplicate)
154
+ # [INFO] steal from TensorDistribution2x4Test test case in test_distributed_tensor.cpp
155
+ # Replicated: use dim 0 as convention
156
+ dims.append(0)
157
+ # Replicated: use shape 1 to skip concatenation
158
+ shape_override.append(1)
159
+
160
+ composer_cfg = ttnn.MeshComposerConfig(dims=dims, mesh_shape_override=ttnn.MeshShape(shape_override))
161
+ return ttnn.create_mesh_composer(device, composer_cfg)
code/models/common/device_utils.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Device topology naming helpers shared by TTTv2 modules."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import ttnn
9
+
10
+
11
+ def is_blackhole() -> bool:
12
+ return "blackhole" in ttnn.get_arch_name()
13
+
14
+
15
+ def get_device_name(mesh_device: ttnn.MeshDevice, num_devices: int | None = None) -> str:
16
+ """Return the product/topology name for a TT mesh device.
17
+
18
+ By default, the full mesh device count is used. CCL callers can pass a
19
+ host-local device count when they need link-count tuning for the current
20
+ process rather than for the full mesh.
21
+ """
22
+ num_devices = mesh_device.get_num_devices() if num_devices is None else num_devices
23
+ dram_grid_size = mesh_device.dram_grid_size()
24
+
25
+ if ttnn.device.is_blackhole(mesh_device):
26
+ device_names = {
27
+ 1: "P100" if dram_grid_size and dram_grid_size.x == 7 else "P150",
28
+ 2: "P300",
29
+ 4: "P150x4",
30
+ 8: "P150x8",
31
+ 32: "BHGLX",
32
+ }
33
+ elif ttnn.device.is_wormhole_b0(mesh_device):
34
+ device_names = {
35
+ 1: "N150",
36
+ 2: "N300",
37
+ 4: "N150x4",
38
+ 8: "T3K",
39
+ 32: "TG",
40
+ }
41
+ else:
42
+ raise ValueError(f"Unsupported architecture: {ttnn.get_arch_name()}")
43
+
44
+ if num_devices in device_names:
45
+ return device_names[num_devices]
46
+ raise ValueError(f"Unsupported number of devices: {num_devices} for {ttnn.get_arch_name()}")
code/models/common/distribute_as.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """
5
+ Distribute a torch.Tensor over a mesh using the same topology as a reference TTNN tensor.
6
+
7
+ This mirrors the composition logic in `auto_compose.py` but in reverse: we infer a
8
+ TensorToMesh mapper from the reference tensor's TensorTopology and use it to distribute
9
+ the torch tensor accordingly.
10
+ """
11
+
12
+ from typing import Optional
13
+
14
+ import torch
15
+
16
+ import ttnn
17
+
18
+ from .auto_compose import extract_tensor_topology_info, get_device_from_tensor
19
+
20
+ # ======================================================================================
21
+ # Public API
22
+ # ======================================================================================
23
+
24
+
25
+ def from_torch_dist_as(
26
+ from_tensor_pt: torch.Tensor, as_tensor_tt: ttnn.Tensor, device: Optional[ttnn.MeshDevice] = None
27
+ ) -> ttnn.Tensor:
28
+ """
29
+ Distribute a torch.Tensor over a mesh using the same topology as an existing TTNN tensor.
30
+
31
+ Args:
32
+ from_tensor_pt: Source PyTorch tensor on host.
33
+ as_tensor_tt: Reference TTNN tensor whose topology (placements + distribution shape) will be mirrored.
34
+ device: Optional mesh device. If omitted, inferred from ``as_tensor_tt`` when possible.
35
+
36
+ Returns:
37
+ A TTNN tensor distributed according to ``as_tensor_tt``'s topology and memory configuration
38
+ (e.g. height-sharded decode heads).
39
+ """
40
+ mapper, device = _infer_mesh_mapper_from_topology(as_tensor_tt, device=device)
41
+
42
+ # Usage Patterns: unlike ttnn.to_torch, `device` is required here!
43
+ # Pattern 1: Using mesh_mapper without device (tensor stays in host memory) Programming_Mesh_of_Devices_with_TT-NN.md:370-375
44
+ # Then transfer to device separately: Programming_Mesh_of_Devices_with_TT-NN.md:404-405
45
+ # Pattern 2: Using both mesh_mapper and device together (direct to device) llms.md:1204-1218
46
+ mem_cfg = as_tensor_tt.memory_config()
47
+ return ttnn.from_torch(
48
+ from_tensor_pt,
49
+ dtype=getattr(as_tensor_tt, "dtype", None),
50
+ layout=getattr(as_tensor_tt, "layout", None),
51
+ device=device,
52
+ memory_config=mem_cfg,
53
+ mesh_mapper=mapper,
54
+ )
55
+
56
+
57
+ # ======================================================================================
58
+ # Private Implementation
59
+ # ======================================================================================
60
+
61
+
62
+ def _infer_mesh_mapper_from_topology(
63
+ tensor: ttnn.Tensor, *, device: Optional[ttnn.MeshDevice] = None
64
+ ) -> Optional[ttnn.CppTensorToMesh]:
65
+ """
66
+ Return a TensorToMesh mapper inferred from the tensor's TensorTopology,
67
+ or (None, mesh_device) if no distribution is needed (fully replicated, single-device).
68
+ """
69
+ placements, dist_shape = extract_tensor_topology_info(tensor)
70
+
71
+ tensor_device = get_device_from_tensor(tensor)
72
+ mesh_device = tensor_device or device
73
+ if mesh_device is None:
74
+ mesh_device = ttnn.GetDefaultDevice()
75
+ if mesh_device is None:
76
+ raise RuntimeError(
77
+ "Tensor is on host and no mesh_device provided. " "Set a default via ttnn.SetDefaultDevice(...)."
78
+ )
79
+
80
+ assert len(dist_shape) == len(placements)
81
+
82
+ if len(dist_shape) == 1 and mesh_device.shape.dims() == 1:
83
+ return _map_1d(mesh_device, placements, dist_shape), mesh_device
84
+ else:
85
+ return _map_nd(mesh_device, placements, dist_shape), mesh_device
86
+
87
+
88
+ def _map_1d(
89
+ device: ttnn.MeshDevice,
90
+ placements: list[object],
91
+ dist_shape: list[int],
92
+ ) -> Optional[ttnn.CppTensorToMesh]:
93
+ """
94
+ Build a 1D TensorToMesh mapper. Returns None if fully trivial (handled earlier).
95
+ """
96
+ p = placements[0]
97
+ if isinstance(p, ttnn.PlacementShard):
98
+ mapper_cfg = ttnn.MeshMapperConfig(
99
+ placements=[ttnn.PlacementShard(p.dim)],
100
+ mesh_shape_override=ttnn.MeshShape(dist_shape),
101
+ )
102
+ return ttnn.create_mesh_mapper(device, mapper_cfg)
103
+ else:
104
+ # Replicate across the 1D mesh extent
105
+ mapper_cfg = ttnn.MeshMapperConfig(
106
+ placements=[ttnn.PlacementReplicate()],
107
+ mesh_shape_override=ttnn.MeshShape(dist_shape),
108
+ )
109
+ return ttnn.create_mesh_mapper(device, mapper_cfg)
110
+
111
+
112
+ def _map_nd(
113
+ device: ttnn.MeshDevice,
114
+ placements: list[object],
115
+ dist_shape: list[int],
116
+ ) -> ttnn.CppTensorToMesh:
117
+ """
118
+ Build an ND TensorToMesh mapper that mirrors the tensor's placements and distribution shape.
119
+ """
120
+ mapper_placements = []
121
+ for p in placements:
122
+ if isinstance(p, ttnn.PlacementShard):
123
+ mapper_placements.append(ttnn.PlacementShard(p.dim))
124
+ else:
125
+ assert isinstance(p, ttnn.PlacementReplicate)
126
+ mapper_placements.append(ttnn.PlacementReplicate())
127
+
128
+ mapper_cfg = ttnn.MeshMapperConfig(placements=mapper_placements, mesh_shape_override=ttnn.MeshShape(dist_shape))
129
+ return ttnn.create_mesh_mapper(device, mapper_cfg)
code/models/common/generation_utils.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2023 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import torch
6
+ from loguru import logger
7
+ from transformers.generation.configuration_utils import GenerationConfig
8
+ from transformers.generation.logits_process import ( # ForceTokensLogitsProcessor,
9
+ EncoderNoRepeatNGramLogitsProcessor,
10
+ EncoderRepetitionPenaltyLogitsProcessor,
11
+ ExponentialDecayLengthPenalty,
12
+ ForcedBOSTokenLogitsProcessor,
13
+ ForcedEOSTokenLogitsProcessor,
14
+ InfNanRemoveLogitsProcessor,
15
+ LogitNormalization,
16
+ LogitsProcessorList,
17
+ MinLengthLogitsProcessor,
18
+ MinNewTokensLengthLogitsProcessor,
19
+ NoBadWordsLogitsProcessor,
20
+ NoRepeatNGramLogitsProcessor,
21
+ PrefixConstrainedLogitsProcessor,
22
+ RepetitionPenaltyLogitsProcessor,
23
+ SuppressTokensAtBeginLogitsProcessor,
24
+ SuppressTokensLogitsProcessor,
25
+ )
26
+
27
+ # HammingDiversityLogitsProcessor (diverse beam search) was removed in
28
+ # transformers 5.x with no replacement. Import it optionally so this module
29
+ # still loads; it's only used when diversity_penalty > 0, which TT generation
30
+ # paths don't exercise.
31
+ try:
32
+ from transformers.generation.logits_process import HammingDiversityLogitsProcessor
33
+ except ImportError: # transformers >= 5.x
34
+ HammingDiversityLogitsProcessor = None
35
+
36
+
37
+ def _merge_criteria_processor_list(
38
+ default_list, # Union[LogitsProcessorList, StoppingCriteriaList],
39
+ custom_list, # Union[LogitsProcessorList, StoppingCriteriaList],
40
+ ): # -> Union[LogitsProcessorList, StoppingCriteriaList]:
41
+ if len(custom_list) == 0:
42
+ return default_list
43
+
44
+ for default in default_list:
45
+ for custom in custom_list:
46
+ if type(custom) is type(default):
47
+ object_type = "stopping criteria" if isinstance(custom, StoppingCriteria) else "logits processor"
48
+ raise ValueError(
49
+ f"A custom {object_type} of type {type(custom)} with values {custom} has been passed to"
50
+ f" `generate`, but it has already been created with the values {default}. {default} has been"
51
+ " created by passing the corresponding arguments to generate or by the model's config default"
52
+ f" values. If you just want to change the default values of {object_type} consider passing"
53
+ f" them as arguments to `generate` instead of using a custom {object_type}."
54
+ )
55
+ default_list.extend(custom_list)
56
+ return default_list
57
+
58
+
59
+ def _get_logits_processor(
60
+ generation_config: GenerationConfig,
61
+ input_ids_seq_length: int,
62
+ encoder_input_ids, # torch.LongTensor
63
+ prefix_allowed_tokens_fn, # Callable[[int, torch.Tensor], List[int]],
64
+ logits_processor, # Optional[LogitsProcessorList]
65
+ ): # -> LogitsProcessorList:
66
+ """
67
+ This class returns a [`LogitsProcessorList`] list object that contains all relevant [`LogitsProcessor`]
68
+ instances used to modify the scores of the language model head.
69
+ """
70
+ # instantiate processors list
71
+ processors = LogitsProcessorList()
72
+
73
+ # the following idea is largely copied from this PR: https://github.com/huggingface/transformers/pull/5420/files
74
+ # all samplers can be found in `generation_utils_samplers.py`
75
+ if generation_config.diversity_penalty is not None and generation_config.diversity_penalty > 0.0:
76
+ if HammingDiversityLogitsProcessor is None:
77
+ raise NotImplementedError(
78
+ "diversity_penalty > 0 (diverse beam search) requires HammingDiversityLogitsProcessor, "
79
+ "which was removed in transformers 5.x."
80
+ )
81
+ processors.append(
82
+ HammingDiversityLogitsProcessor(
83
+ diversity_penalty=generation_config.diversity_penalty,
84
+ num_beams=generation_config.num_beams,
85
+ num_beam_groups=generation_config.num_beam_groups,
86
+ )
87
+ )
88
+ if generation_config.encoder_repetition_penalty is not None and generation_config.encoder_repetition_penalty != 1.0:
89
+ processors.append(
90
+ EncoderRepetitionPenaltyLogitsProcessor(
91
+ penalty=generation_config.encoder_repetition_penalty,
92
+ encoder_input_ids=encoder_input_ids,
93
+ )
94
+ )
95
+ if generation_config.repetition_penalty is not None and generation_config.repetition_penalty != 1.0:
96
+ processors.append(RepetitionPenaltyLogitsProcessor(penalty=generation_config.repetition_penalty))
97
+ if generation_config.no_repeat_ngram_size is not None and generation_config.no_repeat_ngram_size > 0:
98
+ processors.append(NoRepeatNGramLogitsProcessor(generation_config.no_repeat_ngram_size))
99
+ if (
100
+ generation_config.encoder_no_repeat_ngram_size is not None
101
+ and generation_config.encoder_no_repeat_ngram_size > 0
102
+ ):
103
+ if len(encoder_input_ids.shape) == 2:
104
+ processors.append(
105
+ EncoderNoRepeatNGramLogitsProcessor(generation_config.encoder_no_repeat_ngram_size, encoder_input_ids)
106
+ )
107
+ else:
108
+ raise ValueError("It's impossible to use `encoder_no_repeat_ngram_size` with decoder-only architecture")
109
+ if generation_config.bad_words_ids is not None:
110
+ processors.append(NoBadWordsLogitsProcessor(generation_config.bad_words_ids, generation_config.eos_token_id))
111
+ if (
112
+ generation_config.min_length is not None
113
+ and generation_config.eos_token_id is not None
114
+ and generation_config.min_length > 0
115
+ ):
116
+ processors.append(MinLengthLogitsProcessor(generation_config.min_length, generation_config.eos_token_id))
117
+ if (
118
+ generation_config.min_new_tokens is not None
119
+ and generation_config.eos_token_id is not None
120
+ and generation_config.min_new_tokens > 0
121
+ ):
122
+ processors.append(
123
+ MinNewTokensLengthLogitsProcessor(
124
+ input_ids_seq_length,
125
+ generation_config.min_new_tokens,
126
+ generation_config.eos_token_id,
127
+ )
128
+ )
129
+ if prefix_allowed_tokens_fn is not None:
130
+ processors.append(
131
+ PrefixConstrainedLogitsProcessor(
132
+ prefix_allowed_tokens_fn,
133
+ generation_config.num_beams // generation_config.num_beam_groups,
134
+ )
135
+ )
136
+ if generation_config.forced_bos_token_id is not None:
137
+ processors.append(ForcedBOSTokenLogitsProcessor(generation_config.forced_bos_token_id))
138
+ if generation_config.forced_eos_token_id is not None:
139
+ processors.append(
140
+ ForcedEOSTokenLogitsProcessor(generation_config.max_length, generation_config.forced_eos_token_id)
141
+ )
142
+ if generation_config.remove_invalid_values is True:
143
+ processors.append(InfNanRemoveLogitsProcessor())
144
+ if generation_config.exponential_decay_length_penalty is not None:
145
+ processors.append(
146
+ ExponentialDecayLengthPenalty(
147
+ generation_config.exponential_decay_length_penalty,
148
+ generation_config.eos_token_id,
149
+ input_ids_seq_length,
150
+ )
151
+ )
152
+ if generation_config.suppress_tokens is not None:
153
+ processors.append(SuppressTokensLogitsProcessor(generation_config.suppress_tokens))
154
+ if generation_config.begin_suppress_tokens is not None:
155
+ begin_index = input_ids_seq_length
156
+ begin_index = (
157
+ begin_index
158
+ if (input_ids_seq_length > 1 or generation_config.forced_bos_token_id is None)
159
+ else begin_index + 1
160
+ )
161
+ processors.append(SuppressTokensAtBeginLogitsProcessor(generation_config.begin_suppress_tokens, begin_index))
162
+ processors = _merge_criteria_processor_list(processors, logits_processor)
163
+ # `LogitNormalization` should always be the last logit processor, when present
164
+ if generation_config.renormalize_logits is True:
165
+ processors.append(LogitNormalization())
166
+ return processors
167
+
168
+
169
+ def get_logits_processor(input_ids, config):
170
+ generation_config = GenerationConfig.from_model_config(config)
171
+ input_ids_seq_length = input_ids.shape[-1]
172
+
173
+ logits_processor = _get_logits_processor(
174
+ generation_config=generation_config,
175
+ input_ids_seq_length=input_ids_seq_length,
176
+ encoder_input_ids=input_ids,
177
+ prefix_allowed_tokens_fn=None,
178
+ logits_processor=LogitsProcessorList(),
179
+ )
180
+
181
+ return logits_processor
182
+
183
+
184
+ def pad_input_32(tensor, value):
185
+ len = tensor.shape[1]
186
+
187
+ if len % 32 == 0:
188
+ return tensor
189
+
190
+ padded_len = ((len // 32) + 1) * 32
191
+
192
+ pad_tensor = (value * torch.ones(tensor.shape[0], padded_len - len)).to(torch.long)
193
+ tensor = torch.cat([tensor, pad_tensor], dim=1)
194
+
195
+ return tensor
196
+
197
+
198
+ def run_generate(
199
+ input_sentance,
200
+ tokenizer,
201
+ tt_model_constructor,
202
+ device,
203
+ run_tt_model=True,
204
+ log=True,
205
+ comp_pcc=None,
206
+ ):
207
+ tt_model, hf_reference_model = tt_model_constructor(device)
208
+
209
+ # Prepare input
210
+ tokenized = tokenizer(input_sentance, return_tensors="pt") # Batch size 1
211
+
212
+ input_ids = pad_input_32(tokenized.input_ids, hf_reference_model.generation_config.pad_token_id)
213
+ attention_mask = pad_input_32(tokenized.attention_mask, 0)
214
+
215
+ if log:
216
+ logger.debug(f"input_ids {input_ids.shape} {input_ids}")
217
+ logger.debug(f"attention_mask {attention_mask.shape} {attention_mask}")
218
+
219
+ logits_processor = get_logits_processor(input_ids, hf_reference_model.config)
220
+
221
+ decoder_start_values = hf_reference_model.generation_config.pad_token_id * torch.ones(1, 32).to(torch.long)
222
+ decoder_input_ids = hf_reference_model.generation_config.pad_token_id * torch.ones(1, 64).to(torch.long)
223
+
224
+ if log:
225
+ logger.debug(f"decoder_input_ids {decoder_input_ids}")
226
+
227
+ encoder_outputs = None
228
+ use_cache = False
229
+
230
+ for i in range(64):
231
+ # PyTorch forward pass
232
+ pt_out = hf_reference_model(
233
+ input_ids=input_ids,
234
+ decoder_input_ids=decoder_input_ids,
235
+ attention_mask=attention_mask,
236
+ )
237
+
238
+ if run_tt_model:
239
+ tt_out = tt_model(
240
+ input_ids=input_ids,
241
+ decoder_input_ids=decoder_input_ids,
242
+ attention_mask=attention_mask,
243
+ encoder_outputs=encoder_outputs,
244
+ return_dict=True,
245
+ use_cache=use_cache,
246
+ )
247
+ encoder_outputs = tt_out.encoder_outputs
248
+ next_token_logits = tt_out.logits
249
+
250
+ if comp_pcc is not None:
251
+ does_pass, pcc_message = comp_pcc(pt_out.logits, tt_out.logits, 0.98)
252
+
253
+ if log:
254
+ logger.info(pcc_message)
255
+ else:
256
+ next_token_logits = pt_out.logits
257
+
258
+ # pre-process distribution
259
+ next_tokens_scores = logits_processor(input_ids, next_token_logits)
260
+
261
+ # argmax
262
+ next_tokens = torch.argmax(next_tokens_scores, dim=-1)
263
+
264
+ if log:
265
+ logger.debug(f"next_tokens {next_tokens}")
266
+
267
+ if next_tokens[0][i] == hf_reference_model.generation_config.eos_token_id:
268
+ break
269
+
270
+ # We need to expand decoder_input_ids
271
+ if (i + 1) % 32 == 0:
272
+ decoder_input_ids = torch.cat([decoder_input_ids, decoder_start_values], dim=1)
273
+
274
+ decoder_input_ids[0][i + 1] = next_tokens[0][i]
275
+
276
+ if log:
277
+ logger.debug(f"decoder_input_ids {decoder_input_ids[0]}")
278
+
279
+ return tokenizer.decode(decoder_input_ids[0], skip_special_tokens=True)
code/models/common/helper_funcs.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2023 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from typing import Optional
6
+
7
+ import ttnn
8
+
9
+
10
+ def Linear(
11
+ in_features: int,
12
+ out_features: int,
13
+ weight: ttnn.Tensor,
14
+ bias: Optional[ttnn.Tensor] = None,
15
+ output_mem_config=ttnn.DRAM_MEMORY_CONFIG,
16
+ ):
17
+ """
18
+ Returns a function that performs a Linear operation with optional bias.
19
+
20
+ ``weight`` must be tt_tensor.
21
+ """
22
+ assert weight.padded_shape == [
23
+ 1,
24
+ 1,
25
+ out_features,
26
+ in_features,
27
+ ], "weight does not have the expected shape"
28
+
29
+ if bias is not None:
30
+ assert bias.padded_shape[-1] == out_features, "bias does not have the expected shape"
31
+
32
+ weight = weight
33
+ bias = bias
34
+ weight_T = ttnn.transpose(weight, -2, -1)
35
+
36
+ def linear_(activation):
37
+ nonlocal bias
38
+ assert activation.padded_shape[-1] == in_features, "activation tensor do not have the expected shape"
39
+ if bias is not None and bias.get_layout() != ttnn.TILE_LAYOUT:
40
+ bias = ttnn.to_layout(bias, ttnn.TILE_LAYOUT)
41
+ return ttnn.linear(activation, weight_T, bias=bias, memory_config=output_mem_config)
42
+
43
+ return linear_
code/models/common/lightweightmodule.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2023 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+
6
+ class LightweightModule:
7
+ """Torch modules add a surprising amount of host overhead for attribute
8
+ access and method calls. This class is a lightweight alternative that
9
+ just wraps a forward function for now."""
10
+
11
+ def __call__(self, *args, **kwargs):
12
+ return self.forward(*args, **kwargs)
code/models/common/llama_models.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from typing import Dict, List, Optional, Union
8
+
9
+ import torch
10
+ from PIL import Image
11
+ from pydantic import BaseModel, validator
12
+
13
+ # ``AutoModelForImageTextToText`` (the replacement for ``AutoModelForVision2Seq``,
14
+ # which was removed in transformers 5.x) is only consumed by the
15
+ # ``GeneratorChat``/``GeneratorText`` constructors below — defer the imports
16
+ # so loading this module doesn't break every downstream import chain
17
+ # (e.g. ``tt_transformers.tt.generator``, used by every TT vLLM bridge)
18
+ # under transformers >= 5.
19
+
20
+
21
+ class Role(Enum):
22
+ system = "system"
23
+ user = "user"
24
+ assistant = "assistant"
25
+ ipython = "ipython"
26
+
27
+
28
+ class StopReason(Enum):
29
+ end_of_turn = "end_of_turn"
30
+ end_of_message = "end_of_message"
31
+ out_of_tokens = "out_of_tokens"
32
+
33
+
34
+ @dataclass
35
+ class TokenResult:
36
+ token: int
37
+ text: str
38
+ logprobs: Optional[List[float]] = None
39
+
40
+
41
+ @dataclass
42
+ class CompletionMessage:
43
+ content: str
44
+ role: Role = Role.assistant.value
45
+
46
+
47
+ class BuiltinTool(Enum):
48
+ brave_search = "brave_search"
49
+ wolfram_alpha = "wolfram_alpha"
50
+ photogen = "photogen"
51
+ code_interpreter = "code_interpreter"
52
+
53
+
54
+ Primitive = Union[str, int, float, bool, None]
55
+ RecursiveType = Union[Primitive, List[Primitive], Dict[str, Primitive]]
56
+
57
+
58
+ class ToolCall(BaseModel):
59
+ call_id: str
60
+ tool_name: Union[BuiltinTool, str]
61
+ arguments: Dict[str, RecursiveType]
62
+
63
+ @validator("tool_name", pre=True)
64
+ @classmethod
65
+ def validate_field(cls, v):
66
+ if isinstance(v, str):
67
+ try:
68
+ return BuiltinTool(v)
69
+ except ValueError:
70
+ return v
71
+ return v
72
+
73
+
74
+ class ChatPrediction:
75
+ generation: CompletionMessage
76
+ decoded_tokens: Optional[List[str]] = None
77
+ logprobs: Optional[List[List[float]]] = None
78
+
79
+
80
+ class CompletionPrediction:
81
+ generation: str
82
+ decoded_tokens: Optional[List[str]] = None
83
+ logprobs: Optional[List[List[float]]] = None
84
+
85
+
86
+ def sample_top_p(probs, p):
87
+ """
88
+ Perform top-p (nucleus) sampling on a probability distribution.
89
+
90
+ Args:
91
+ probs (torch.Tensor): Probability distribution tensor.
92
+ p (float): Probability threshold for top-p sampling.
93
+
94
+ Returns:
95
+ torch.Tensor: Sampled token indices.
96
+
97
+ Note:
98
+ Top-p sampling selects the smallest set of tokens whose cumulative probability mass
99
+ exceeds the threshold p. The distribution is renormalized based on the selected tokens.
100
+ From: https://github.com/meta-llama/llama-models/blob/v0.1.5/models/llama3/reference_impl/generation.py#L450-L472
101
+ """
102
+ probs_sort, probs_idx = torch.sort(probs, dim=-1, descending=True)
103
+ probs_sum = torch.cumsum(probs_sort, dim=-1)
104
+ mask = probs_sum - probs_sort > p
105
+ probs_sort[mask] = 0.0
106
+ probs_sort.div_(probs_sort.sum(dim=-1, keepdim=True))
107
+ next_token = torch.multinomial(probs_sort, num_samples=1)
108
+ next_token = torch.gather(probs_idx, -1, next_token)
109
+ return next_token
110
+
111
+
112
+ def extract_images_from_messages(messages):
113
+ images = []
114
+ for message in messages:
115
+ if "content" in message:
116
+ contents = message["content"]
117
+ for content in contents:
118
+ if (content["type"] == "image") and ("image" in content):
119
+ images.append(content["image"])
120
+ return images
121
+
122
+
123
+ def create_vision_mask(
124
+ tokens: List[int],
125
+ vision_token: int,
126
+ ) -> List[List[int]]:
127
+ """From: https://github.com/meta-llama/llama-models/blob/v0.1.5/models/llama3/api/chat_format.py#L253-L276"""
128
+ vision_token_locations = [i for i, token in enumerate(tokens) if token == vision_token]
129
+ if len(vision_token_locations) == 0:
130
+ return []
131
+
132
+ if len(vision_token_locations) == 1:
133
+ # only one image present, unmask until end of sequence
134
+ return [[vision_token_locations[0], -1]]
135
+ vision_masks = [[loc1, loc2] for loc1, loc2 in zip(vision_token_locations[:-1], vision_token_locations[1:])]
136
+ # last image will attend to all subsequent text
137
+ vision_masks.append([vision_token_locations[-1], len(tokens)])
138
+
139
+ # if there are two or more consecutive vision tokens,
140
+ # they should all attend to all subsequent
141
+ # text present
142
+ last_mask_end = vision_masks[-1][1]
143
+ for vision_mask in vision_masks[::-1]:
144
+ if vision_mask[0] == vision_mask[1] - 1:
145
+ vision_mask[1] = last_mask_end
146
+ last_mask_end = vision_mask[1]
147
+ return vision_masks
148
+
149
+
150
+ def encode_content(content, images, image_token):
151
+ if isinstance(content, Image):
152
+ images.append(content)
153
+ assert image_token is not None
154
+ return image_token
155
+ if isinstance(content, str):
156
+ return content
157
+ if isinstance(content, (list, tuple)):
158
+ return "\n".join(encode_content(item, images) for item in content)
159
+ if isinstance(content, dict):
160
+ content_type = content.get("type")
161
+ if content_type == "text":
162
+ return content["text"]
163
+ if content_type == "image":
164
+ # TBD: support url
165
+ images.append(content["image"])
166
+ assert image_token is not None
167
+ return image_token
168
+ raise ValueError(f"Unknown content format: {content}")
169
+
170
+
171
+ class GeneratorChat:
172
+ def __init__(self, model_name, max_batch_size=1):
173
+ from transformers import pipeline
174
+
175
+ self.pipe = pipeline("image-text-to-text", model=model_name, batch_size=max_batch_size)
176
+
177
+ def chat_completion(
178
+ self,
179
+ messages,
180
+ temperature=0.6,
181
+ top_p: float = 0.9,
182
+ max_gen_len=None,
183
+ ):
184
+ generation_output = self.pipe(
185
+ text=messages, temperature=temperature, top_p=top_p, max_new_tokens=max_gen_len, return_full_text=False
186
+ )
187
+
188
+ if len(generation_output) == 1:
189
+ return CompletionMessage(content=generation_output[0]["generated_text"])
190
+ return [CompletionMessage(content=output[0]["generated_text"]) for output in generation_output]
191
+
192
+
193
+ class GeneratorText:
194
+ def __init__(self, model_name):
195
+ from transformers import AutoModelForImageTextToText, AutoProcessor
196
+
197
+ self.processor = AutoProcessor.from_pretrained(model_name)
198
+ self.model = AutoModelForImageTextToText.from_pretrained(model_name)
199
+
200
+ def text_completion(
201
+ self,
202
+ content: Union[str, Image.Image, Dict, List[Dict]],
203
+ temperature: float = 0.6,
204
+ top_p: float = 0.9,
205
+ max_gen_len=None,
206
+ ):
207
+ images = []
208
+ text = encode_content(content, images, self.processor.image_token)
209
+ model_input = self.processor(text=text, images=images or None, return_tensors="pt", add_special_tokens=False)
210
+ tokens = self.model.generate(**model_input, temperature=temperature, top_p=top_p, max_new_tokens=max_gen_len)[0]
211
+ tokens = tokens[model_input["input_ids"].shape[-1] :]
212
+ return self.processor.decode(tokens, skip_special_tokens=True)
code/models/common/metrics.py ADDED
@@ -0,0 +1,488 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """
5
+ TTNN Metric Functions
6
+
7
+ Metric functions for comparing TTNN implementations against reference implementations.
8
+ All metrics support both PyTorch and TTNN tensors, with TTNN-native computation
9
+ that stays on device until the final scalar result.
10
+
11
+ Key Features:
12
+ - Automatic detection of tensor type (PyTorch vs TTNN)
13
+ - TTNN-native computation using device operations
14
+ - Minimal host transfers (only final scalar)
15
+ - Graceful fallback to PyTorch for non-tensor inputs
16
+ """
17
+
18
+ import numpy as np
19
+ import torch
20
+ from loguru import logger
21
+
22
+ import ttnn
23
+
24
+ from .auto_compose import to_torch_auto_compose
25
+
26
+ # ======================================================================================
27
+ # Public API
28
+ # ======================================================================================
29
+
30
+
31
+ def compute_max_abs_error(impl, ref):
32
+ """
33
+ Compute maximum absolute error between two tensors.
34
+
35
+ Supports both PyTorch and TTNN tensors. For TTNN tensors, computation
36
+ stays on device until the final scalar value.
37
+
38
+ Args:
39
+ impl: Implementation output (PyTorch or TTNN tensor)
40
+ ref: Reference output (PyTorch or TTNN tensor)
41
+
42
+ Returns:
43
+ float: Maximum absolute difference between tensors
44
+
45
+ Examples:
46
+ >>> a = torch.tensor([1.0, 2.0, 3.0])
47
+ >>> b = torch.tensor([1.1, 2.0, 2.9])
48
+ >>> _compute_max_abs_error(a, b)
49
+ 0.10000002384185791
50
+ """
51
+ try:
52
+ if _is_ttnn_tensor(impl) and _is_ttnn_tensor(ref):
53
+ # TTNN path - stay on device
54
+ diff = ttnn.subtract(impl, ref)
55
+ abs_diff = _ttnn_op_layout_invariant(diff, ttnn.abs)
56
+ return _ttnn_max_scalar_all_dtype(abs_diff)
57
+ elif _is_ttnn_tensor(impl):
58
+ return (to_torch_auto_compose(impl) - ref).abs().max().item()
59
+ elif _is_ttnn_tensor(ref):
60
+ return (impl - to_torch_auto_compose(ref)).abs().max().item()
61
+ else:
62
+ # PyTorch path - both must be torch tensors
63
+ return (impl - ref).abs().max().item()
64
+ except Exception as e:
65
+ return float("inf")
66
+
67
+
68
+ def compute_mean_abs_error(impl, ref):
69
+ """
70
+ Compute mean absolute error between two tensors.
71
+
72
+ Supports both PyTorch and TTNN tensors. For TTNN tensors, computation
73
+ stays on device until the final scalar value.
74
+
75
+ Args:
76
+ impl: Implementation output (PyTorch or TTNN tensor)
77
+ ref: Reference output (PyTorch or TTNN tensor)
78
+
79
+ Returns:
80
+ float: Mean absolute difference between tensors
81
+
82
+ Examples:
83
+ >>> a = torch.tensor([1.0, 2.0, 3.0])
84
+ >>> b = torch.tensor([1.1, 2.0, 2.9])
85
+ >>> _compute_mean_abs_error(a, b)
86
+ 0.06666667014360428
87
+ """
88
+ try:
89
+ if _is_ttnn_tensor(impl) and _is_ttnn_tensor(ref):
90
+ # TTNN path - stay on device
91
+ diff = ttnn.subtract(impl, ref)
92
+ abs_diff = _ttnn_op_layout_invariant(diff, ttnn.abs)
93
+ return _ttnn_mean_scalar_all_dtype(abs_diff)
94
+ elif _is_ttnn_tensor(impl):
95
+ return (to_torch_auto_compose(impl) - ref).abs().mean().item()
96
+ elif _is_ttnn_tensor(ref):
97
+ return (impl - to_torch_auto_compose(ref)).abs().mean().item()
98
+ else:
99
+ # PyTorch path - both must be torch tensors
100
+ return (impl - ref).abs().mean().item()
101
+ except Exception as e:
102
+ return float("inf")
103
+
104
+
105
+ def compute_pcc(impl, ref):
106
+ """
107
+ Compute Pearson Correlation Coefficient (PCC) between two tensors.
108
+
109
+ PCC measures the linear correlation between two tensors. Returns 1.0 for
110
+ perfect positive correlation, 0.0 for no correlation, and -1.0 for perfect
111
+ negative correlation. This is a common metric used in tt-metal for validating
112
+ tensor computations.
113
+
114
+ For TTNN tensors: Uses on-device computation (100-1000× faster). If edge cases
115
+ produce non-finite values (NaN/inf), raises ValueError with instructions to use
116
+ Pattern 2 (PyTorch reference with output_map for robust CPU-based PCC).
117
+
118
+ For PyTorch tensors: Uses robust numpy implementation (from tt-metal
119
+ comparison_funcs.py) that handles complex, NaN, inf, and constant tensors.
120
+
121
+ Args:
122
+ impl: Implementation output (PyTorch or TTNN tensor)
123
+ ref: Reference output (PyTorch or TTNN tensor)
124
+
125
+ Returns:
126
+ float: PCC value in range [-1.0, 1.0], or 0.0 on error
127
+
128
+ Raises:
129
+ ValueError: If TTNN-native computation produces non-finite values
130
+
131
+ Examples:
132
+ >>> a = torch.tensor([1.0, 2.0, 3.0])
133
+ >>> b = torch.tensor([1.0, 2.0, 3.0])
134
+ >>> _compute_pcc(a, b)
135
+ 1.0
136
+
137
+ >>> a = torch.tensor([1.0, 2.0, 3.0])
138
+ >>> b = torch.tensor([3.0, 2.0, 1.0])
139
+ >>> _compute_pcc(a, b)
140
+ -1.0
141
+ """
142
+ try:
143
+ # TTNN fast path - compute on device
144
+ if _is_ttnn_tensor(impl) and _is_ttnn_tensor(ref):
145
+ return compute_pcc_device(impl, ref)
146
+ elif _is_ttnn_tensor(impl):
147
+ return compute_pcc_host(to_torch_auto_compose(impl), ref)
148
+ elif _is_ttnn_tensor(ref):
149
+ return compute_pcc_host(impl, to_torch_auto_compose(ref))
150
+ else:
151
+ return compute_pcc_host(impl, ref)
152
+ except Exception:
153
+ return 0.0
154
+
155
+
156
+ # Default metrics dictionary for easy import
157
+ DEFAULT_METRICS = {
158
+ "max_abs_error": compute_max_abs_error,
159
+ "mean_abs_error": compute_mean_abs_error,
160
+ "pcc": compute_pcc,
161
+ }
162
+
163
+
164
+ # Allclose comparison with detailed delta string
165
+ def comp_allclose(impl, ref, rtol=1e-05, atol=1e-08):
166
+ """
167
+ Compare two tensors using an allclose criterion and return (passing, details).
168
+
169
+ Provides both a TTNN-native on-device implementation and a PyTorch fallback.
170
+ Mirrors semantics of torch.allclose(..., equal_nan=True) and reports the
171
+ maximum absolute and relative deltas similar to comparison_funcs.py.
172
+
173
+ Args:
174
+ impl: Implementation output (PyTorch or TTNN tensor)
175
+ ref: Reference output (PyTorch or TTNN tensor)
176
+ rtol (float): Relative tolerance
177
+ atol (float): Absolute tolerance
178
+
179
+ Returns:
180
+ tuple[bool, str]: (passing, "Max ATOL Delta: x, Max RTOL Delta: y[,...]")
181
+ """
182
+ try:
183
+ # TTNN-native path: compute deltas and allclose on device, then transfer final scalars
184
+ if _is_ttnn_tensor(impl) and _is_ttnn_tensor(ref):
185
+ # Compute deltas (device)
186
+ diff = _ttnn_op_layout_invariant(ttnn.subtract(impl, ref), ttnn.abs)
187
+ cal_atol = _ttnn_max_scalar_all_dtype(diff)
188
+ # For rtol delta, divide by abs(ref) (may produce inf for zeros; acceptable for reporting)
189
+ divided_by_ref = ttnn.divide(diff, _ttnn_op_layout_invariant(ref, ttnn.abs))
190
+ cal_rtol = _ttnn_max_scalar_all_dtype(divided_by_ref)
191
+
192
+ # equal_nan=True semantics and finite/infinite handling
193
+ isnan_impl = _ttnn_op_layout_invariant(impl, ttnn.isnan)
194
+ isnan_ref = _ttnn_op_layout_invariant(ref, ttnn.isnan)
195
+ both_nan = ttnn.logical_and(isnan_impl, isnan_ref)
196
+
197
+ isinf_impl = _ttnn_op_layout_invariant(impl, ttnn.isinf)
198
+ isinf_ref = _ttnn_op_layout_invariant(ref, ttnn.isinf)
199
+ impl_sign = _ttnn_op_layout_invariant(impl, ttnn.sign)
200
+ ref_sign = _ttnn_op_layout_invariant(ref, ttnn.sign)
201
+ same_sign_inf = ttnn.eq(impl_sign, ref_sign)
202
+ both_inf_same_sign = ttnn.logical_and(ttnn.logical_and(isinf_impl, isinf_ref), same_sign_inf)
203
+
204
+ # Finite elements where numeric closeness applies
205
+ any_nan = ttnn.logical_or(isnan_impl, isnan_ref)
206
+ any_inf = ttnn.logical_or(isinf_impl, isinf_ref)
207
+ finite_both = _ttnn_op_layout_invariant(ttnn.logical_or(any_nan, any_inf), ttnn.logical_not)
208
+
209
+ # |impl - ref| <= atol + rtol * |ref|
210
+ bound = ttnn.add(ttnn.mul(_ttnn_op_layout_invariant(ref, ttnn.abs), rtol, dtype=ttnn.bfloat16), atol)
211
+ close_numeric = ttnn.le(diff, bound)
212
+ finite_and_close = ttnn.logical_and(finite_both, close_numeric)
213
+
214
+ ok_mask = ttnn.logical_or(ttnn.logical_or(both_nan, both_inf_same_sign), finite_and_close)
215
+ fail_mask = _ttnn_op_layout_invariant(ok_mask, ttnn.logical_not)
216
+
217
+ # Reduce to scalar: any failure -> 1.0 else 0.0
218
+ fail_indicator = _ttnn_op_layout_invariant(fail_mask, ttnn.where, true_value=1.0, false_value=0.0)
219
+ any_fail = _ttnn_max_scalar_all_dtype(fail_indicator)
220
+ passing = any_fail == 0.0
221
+
222
+ output_str = f"Max ATOL Delta: {cal_atol}, Max RTOL Delta: {cal_rtol}"
223
+ if not passing:
224
+ output_str += ", Allclose check failed"
225
+ return passing, output_str
226
+
227
+ # Fallback: compute with PyTorch (handles mixed inputs by converting TTNN -> torch)
228
+ impl_torch = to_torch_auto_compose(impl) if _is_ttnn_tensor(impl) else impl
229
+ ref_torch = to_torch_auto_compose(ref) if _is_ttnn_tensor(ref) else ref
230
+
231
+ if torch.is_tensor(impl_torch) and torch.is_tensor(ref_torch):
232
+ # Match dtype for fair comparison
233
+ if impl_torch.dtype != ref_torch.dtype:
234
+ ref_torch = ref_torch.to(impl_torch.dtype)
235
+
236
+ atol_delta = torch.max(torch.abs(impl_torch - ref_torch)).item()
237
+ # May produce inf where ref == 0; this mirrors comparison_funcs.py behavior
238
+ rtol_delta = torch.max(torch.abs(impl_torch - ref_torch) / torch.abs(ref_torch)).item()
239
+ passing = torch.allclose(impl_torch, ref_torch, rtol, atol, True)
240
+ output_str = f"Max ATOL Delta: {atol_delta}, Max RTOL Delta: {rtol_delta}"
241
+ if not passing:
242
+ output_str += ", Allclose check failed"
243
+ return passing, output_str
244
+
245
+ # Unsupported types
246
+ return False, "Unsupported input types for comp_allclose"
247
+ except Exception as e:
248
+ return False, f"Error computing comp_allclose: {e}"
249
+
250
+
251
+ def compute_pcc_device(impl, ref):
252
+ """Fast on-device PCC for TTNN tensors only."""
253
+ try:
254
+ # Early edge-case handling to mirror CPU semantics
255
+ # - All NaNs → 1.0; mixed NaNs → 0.0
256
+ # - One tensor all zero and the other not → 0.0
257
+ # - Both constant → 1.0 if equal, else 0.0
258
+
259
+ # Any nonzero check (all-zero detection)
260
+ impl_abs_max = _ttnn_max_scalar_all_dtype(_ttnn_op_layout_invariant(impl, ttnn.abs))
261
+ ref_abs_max = _ttnn_max_scalar_all_dtype(_ttnn_op_layout_invariant(ref, ttnn.abs))
262
+
263
+ impl_has_any = impl_abs_max != 0.0
264
+ ref_has_any = ref_abs_max != 0.0
265
+ if impl_has_any != ref_has_any:
266
+ return 0.0
267
+
268
+ # Min/Max scalars for constant and NaN detection
269
+ impl_min = _ttnn_min_scalar_all_dtype(impl)
270
+ impl_max = _ttnn_max_scalar_all_dtype(impl)
271
+ ref_min = _ttnn_min_scalar_all_dtype(ref)
272
+ ref_max = _ttnn_max_scalar_all_dtype(ref)
273
+
274
+ impl_min_finite = np.isfinite(impl_min)
275
+ impl_max_finite = np.isfinite(impl_max)
276
+ ref_min_finite = np.isfinite(ref_min)
277
+ ref_max_finite = np.isfinite(ref_max)
278
+
279
+ impl_all_nan = (not impl_min_finite) and (not impl_max_finite)
280
+ ref_all_nan = (not ref_min_finite) and (not ref_max_finite)
281
+ if impl_all_nan and ref_all_nan:
282
+ return 1.0
283
+ if impl_all_nan != ref_all_nan:
284
+ return 0.0
285
+
286
+ # Constant tensors
287
+ if impl_min_finite and impl_max_finite and ref_min_finite and ref_max_finite:
288
+ if impl_min == impl_max and ref_min == ref_max:
289
+ return (
290
+ 1.0
291
+ if torch.isclose(
292
+ torch.tensor(impl_max, dtype=torch.float32), torch.tensor(ref_max, dtype=torch.float32)
293
+ )
294
+ else 0.0
295
+ )
296
+
297
+ # Standard PCC formula on device
298
+ mean_impl = _ttnn_mean_scalar_all_dtype(impl)
299
+ mean_ref = _ttnn_mean_scalar_all_dtype(ref)
300
+
301
+ impl_centered = ttnn.subtract(impl, mean_impl)
302
+ ref_centered = ttnn.subtract(ref, mean_ref)
303
+
304
+ # todo)) ttnn.sum() does local reduction only; need CCL reduction for global sum when adding support for multiple-devices
305
+ # [INFO] we cast to float32 to avoid overflow when impl and ref are in bfloat8_b or bfloat4_b
306
+ numerator = ttnn.sum(ttnn.mul(impl_centered, ref_centered, dtype=ttnn.float32))
307
+ impl_sq_sum = ttnn.sum(ttnn.mul(impl_centered, impl_centered, dtype=ttnn.float32))
308
+ ref_sq_sum = ttnn.sum(ttnn.mul(ref_centered, ref_centered, dtype=ttnn.float32))
309
+ denominator = ttnn.sqrt(ttnn.mul(impl_sq_sum, ref_sq_sum, dtype=ttnn.float32))
310
+
311
+ # Safe divide
312
+ denom_scalar = denominator.item()
313
+ if denom_scalar == 0.0 or not np.isfinite(denom_scalar):
314
+ return 0.0
315
+
316
+ pcc = numerator.item() / denom_scalar
317
+ if not np.isfinite(pcc):
318
+ return 0.0
319
+ return pcc
320
+ except Exception as e:
321
+ # todo)) maybe return a string for logging: f"Error computing PCC on device: {e}, impl: {impl}, ref: {ref}"
322
+ return 0.0
323
+
324
+
325
+ # code stolen from tests/tt_eager/python_api_testing/sweep_tests/comparison_funcs.py
326
+ # and models/common/utility_functions.py
327
+ def compute_pcc_host(impl, ref):
328
+ """Robust CPU PCC for PyTorch tensors only."""
329
+ try:
330
+ calculated = impl
331
+ golden = ref
332
+ if golden.dtype != calculated.dtype:
333
+ calculated = calculated.type(golden.dtype)
334
+
335
+ # Handle complex tensors
336
+ if golden.is_complex() and calculated.is_complex():
337
+ golden = torch.view_as_real(golden.clone())
338
+ calculated = torch.view_as_real(calculated.clone())
339
+
340
+ # Convert to float if needed
341
+ if not (golden.is_floating_point() or calculated.is_floating_point()):
342
+ golden = golden.to(torch.float)
343
+ calculated = calculated.to(torch.float)
344
+
345
+ # Both tensors are nan
346
+ if torch.all(torch.isnan(golden)) and torch.all(torch.isnan(calculated)):
347
+ return 1.0
348
+
349
+ # One tensor is all nan, the other is not
350
+ if torch.all(torch.isnan(golden)) or torch.all(torch.isnan(calculated)):
351
+ return 0.0
352
+
353
+ # One tensor is all zero, the other is not — also a zero-variance case.
354
+ if torch.any(golden.bool()) != torch.any(calculated.bool()):
355
+ logger.warning("One tensor is all zero. PCC undefined; falling back to allclose.")
356
+ return float(torch.allclose(golden, calculated, rtol=1e-05, atol=1e-04))
357
+
358
+ # Mask all infs and nans
359
+ golden = golden.clone()
360
+ golden[
361
+ torch.logical_or(
362
+ torch.isnan(golden),
363
+ torch.logical_or(torch.isinf(golden), torch.isneginf(golden)),
364
+ )
365
+ ] = 0
366
+ calculated = calculated.clone()
367
+ calculated[
368
+ torch.logical_or(
369
+ torch.isnan(calculated),
370
+ torch.logical_or(torch.isinf(calculated), torch.isneginf(calculated)),
371
+ )
372
+ ] = 0
373
+
374
+ if torch.equal(golden, calculated):
375
+ return 1.0
376
+
377
+ if golden.dtype == torch.bfloat16:
378
+ golden = golden.type(torch.float32)
379
+ calculated = calculated.type(torch.float32)
380
+
381
+ # Single element or constant tensor: PCC is undefined.
382
+ if golden.numel() == 1:
383
+ return float(torch.allclose(golden, calculated, rtol=1e-05, atol=1e-04))
384
+
385
+ if torch.max(golden) == torch.min(golden) or torch.max(calculated) == torch.min(calculated):
386
+ logger.warning("One or both tensors are constant (zero std dev). PCC undefined; falling back to allclose.")
387
+ return float(torch.allclose(golden, calculated, rtol=1e-05, atol=1e-04))
388
+
389
+ # Compute PCC using numpy's corrcoef
390
+ cal_pcc = np.ma.corrcoef(
391
+ np.ma.masked_invalid(torch.squeeze(golden).detach().numpy()).flatten(),
392
+ np.ma.masked_invalid(torch.squeeze(calculated).detach().numpy()).flatten(),
393
+ )
394
+ # Read off-diagonal directly to avoid diagonal contamination.
395
+ cal_pcc = cal_pcc[0, 1]
396
+
397
+ if isinstance(cal_pcc, np.ma.core.MaskedConstant) or np.isnan(float(cal_pcc)):
398
+ logger.warning("PCC returned NaN/masked. Falling back to allclose.")
399
+ return float(torch.allclose(golden, calculated, rtol=1e-05, atol=1e-04))
400
+
401
+ return float(cal_pcc)
402
+ except Exception:
403
+ return 0.0
404
+
405
+
406
+ # ======================================================================================
407
+ # Private Implementation
408
+ # ======================================================================================
409
+
410
+
411
+ def _is_ttnn_tensor(x):
412
+ """Safely detect TTNN tensors even if ttnn.Tensor is not defined in this environment."""
413
+ return isinstance(x, ttnn.Tensor)
414
+
415
+
416
+ def _ttnn_op_layout_invariant(x, op_func, **kwargs):
417
+ """
418
+ Generic helper for TTNN operations that require TILE layout for non-sharded tensors.
419
+
420
+ Args:
421
+ x: TTNN tensor
422
+ op_func: TTNN operation function to call (e.g., ttnn.sign, ttnn.abs, ttnn.typecast)
423
+ **kwargs: Additional keyword arguments to pass to op_func (e.g., dtype=ttnn.bfloat16 for typecast)
424
+
425
+ Returns:
426
+ Result of op_func applied to x, with layout preserved.
427
+ """
428
+ # [ttnn contract] all TTNN operations require tensors to be in TILE layout when working with non-sharded tensors:
429
+ # ttnn.sign(), ttnn.abs(), ttnn.isinf(), ttnn.isnan(), ttnn.typecast(), ttnn.where(), ttnn.logical_not()
430
+ layout = x.get_layout()
431
+ if layout == ttnn.TILE_LAYOUT or x.is_sharded(): # sharded tensors can use either layout
432
+ return op_func(x, **kwargs)
433
+ else:
434
+ return ttnn.to_layout(op_func(ttnn.to_layout(x, ttnn.TILE_LAYOUT), **kwargs), layout)
435
+
436
+
437
+ def _ttnn_max_scalar_all_dtype(x):
438
+ x_bf16 = _ttnn_op_layout_invariant(x, ttnn.typecast, dtype=ttnn.bfloat16)
439
+
440
+ # [ttnn contract] ttnn.max() internally calls a FillPad operation, which only supports the following dtypes:
441
+ # BFLOAT16
442
+ # FLOAT32
443
+ # UINT16, UINT32, INT32
444
+ # UINT8
445
+ # see ttnn/cpp/ttnn/operations/data_movement/fill_pad/device/fill_pad_program_factory.hpp for more details
446
+ # [INFO] we cast to bfloat16 to avoid the limitation of the FillPad operation when dealing with e.g., bfloat8_b and bfloat4_b dtypes
447
+
448
+ # [ttnn contract] When called without a dim parameter, ttnn.max() returns the maximum value across the entire tensor as a scalar.
449
+ max_val_tensor = ttnn.max(x_bf16)
450
+
451
+ # [ttnn contract] When called without a dim parameter, ttnn.max() returns the maximum value across the entire tensor as a scalar.
452
+ # The method supports multiple data types:
453
+ # FLOAT32 → Python float
454
+ # BFLOAT16 → Python float (cast from bfloat16)
455
+ # BFLOAT8_B and BFLOAT4_B → Python float
456
+ return max_val_tensor.item()
457
+
458
+
459
+ def _ttnn_mean_scalar_all_dtype(x):
460
+ x_bf16 = _ttnn_op_layout_invariant(x, ttnn.typecast, dtype=ttnn.bfloat16)
461
+
462
+ # [ttnn contract] ttnn.mean() internally calls a FillPad operation, which only supports the following dtypes:
463
+ # BFLOAT16
464
+ # FLOAT32
465
+ # UINT16, UINT32, INT32
466
+ # UINT8
467
+ # see ttnn/cpp/ttnn/operations/data_movement/fill_pad/device/fill_pad_program_factory.hpp for more details
468
+ # [INFO] we cast to bfloat16 to avoid the limitation of the FillPad operation when dealing with e.g., bfloat8_b and bfloat4_b dtypes
469
+
470
+ # [ttnn contract] When called without a dim parameter, ttnn.mean() returns the mean value across the entire tensor as a scalar.
471
+ mean_val_tensor = ttnn.mean(x_bf16)
472
+ return mean_val_tensor.item()
473
+
474
+
475
+ def _ttnn_min_scalar_all_dtype(x):
476
+ x_bf16 = _ttnn_op_layout_invariant(x, ttnn.typecast, dtype=ttnn.bfloat16)
477
+
478
+ # [ttnn contract] ttnn.mean() internally calls a FillPad operation, which only supports the following dtypes:
479
+ # BFLOAT16
480
+ # FLOAT32
481
+ # UINT16, UINT32, INT32
482
+ # UINT8
483
+ # see ttnn/cpp/ttnn/operations/data_movement/fill_pad/device/fill_pad_program_factory.hpp for more details
484
+ # [INFO] we cast to bfloat16 to avoid the limitation of the FillPad operation when dealing with e.g., bfloat8_b and bfloat4_b dtypes
485
+
486
+ # [ttnn contract] When called without a dim parameter, ttnn.mean() returns the mean value across the entire tensor as a scalar.
487
+ min_val_tensor = ttnn.min(x_bf16)
488
+ return min_val_tensor.item()
code/models/common/model_capabilities.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ # Maximum number of tokens that can simultaneously occupy the KV cache
6
+ # across all concurrent users. This fallback applies to model/device
7
+ # configurations not covered by a model-specific override.
8
+ # Derived from the default branch of the per-model KV-cache rules in
9
+ # the TT vLLM worker (tenstorrent/vllm#315).
10
+ # See also: https://github.com/tenstorrent/vllm/issues/315
11
+ FALLBACK_MAX_TOKENS_ALL_USERS = 131_072
12
+
13
+
14
+ class ModelCapabilitiesMixin:
15
+ """Defines interface for hardware- or model-specific configurations.
16
+
17
+ NOTE: The default values here and per-model overrides will eventually be
18
+ unified with the corresponding vLLM scheduler configuration so that both
19
+ paths derive from the same source of truth.
20
+ """
21
+
22
+ @classmethod
23
+ def get_max_tokens_all_users(cls, **kwargs) -> int:
24
+ """Returns the fallback all-user KV-cache token capacity.
25
+
26
+ Used when no model- or device-specific override applies.
27
+ """
28
+ return FALLBACK_MAX_TOKENS_ALL_USERS
code/models/common/rmsnorm.py ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2023 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ import ttnn
5
+ from models.common.lightweightmodule import LightweightModule
6
+ from models.common.utility_functions import copy_to_buffer
7
+ from models.tt_transformers.tt.common import Mode
8
+
9
+ TILE = 32
10
+ SHARD_HEIGHT = TILE # Current ttnn.rms_norm implementation requires shard height to be a single tile
11
+
12
+
13
+ class RMSNorm(LightweightModule):
14
+ """
15
+ RMSNorm supporting replication over a MeshDevice and sharding within devices.
16
+
17
+ This class implements a Root Mean Square Normalization (RMSNorm) that can be
18
+ distributed across multiple devices and cores. If the `device` parameter is a
19
+ MeshDevice, the weights and computations are replicated across all devices in
20
+ the mesh. Expects an interleaved input tensor, can optionally output a sharded tensor.
21
+
22
+ Args:
23
+ device: The device or MeshDevice on which to perform the computations.
24
+ state_dict: The state dictionary containing the model parameters.
25
+ dim: Input dimension (e.g. model hidden dimension size).
26
+ layer_num: The layer number to determine the weight key in the state dictionary.
27
+ weight_key: The key for retrieving the weight from the state dictionary.
28
+ weight_cache_path: Optional path for caching the tilized weights.
29
+ weight_memory_config: Configuration for the weight memory, default is DRAM_MEMORY_CONFIG.
30
+ weight_dtype: The data type for the tensors, bfp8_b hits >0.999 PCC in the models we tested.
31
+ model_config: Optional configuration dictionary for the model.
32
+ eps (float): Small value to avoid division by zero in normalization, default is 1e-05.
33
+
34
+ If model_config is provided, it must specify SHARDED_NORM_INPUT_MEMCFG, SHARDED_NORM_PRGM_CFG
35
+ and SHARDED_NORM_OUTPUT_MEMCFG. If not provided, default configurations will be generated.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ device,
41
+ dim,
42
+ state_dict,
43
+ weight_key,
44
+ layer_num=None,
45
+ state_dict_prefix=None,
46
+ weight_cache_path=None,
47
+ weight_memory_config=ttnn.DRAM_MEMORY_CONFIG,
48
+ weight_dtype=ttnn.bfloat16,
49
+ is_distributed=None,
50
+ eps: float = 1e-05,
51
+ add_unit_offset=False,
52
+ sharded_program_config=None,
53
+ sharded_output_config=None,
54
+ output_mem_config=None,
55
+ ccl_topology=ttnn.Topology.Ring,
56
+ tt_ccl=None,
57
+ fp32_dest_acc_en=True,
58
+ ):
59
+ super().__init__()
60
+ self.device = device
61
+ self.eps = eps
62
+ self.is_distributed = is_distributed
63
+ self.ccl_topology = ccl_topology
64
+ self.tt_ccl = tt_ccl
65
+ self.add_unit_offset = add_unit_offset
66
+
67
+ if state_dict_prefix:
68
+ weight_name = f"{state_dict_prefix}{weight_key}.weight"
69
+ else:
70
+ if layer_num is None:
71
+ weight_name = f"{weight_key}.weight"
72
+ else:
73
+ weight_name = f"layers.{layer_num}.{weight_key}.weight"
74
+
75
+ torch_weight = (
76
+ state_dict[weight_name].unsqueeze(0).view(1, 1, dim).reshape([1, 1, dim // SHARD_HEIGHT, SHARD_HEIGHT])
77
+ )
78
+
79
+ # Add offset before caching
80
+ if add_unit_offset:
81
+ torch_weight = torch_weight + 1.0
82
+
83
+ # Compatibility with models that don't use mesh devices (e.g. single-chip Mistral-7b)
84
+ is_mesh_device = device.__class__.__name__ == "MeshDevice"
85
+
86
+ self.weight = ttnn.as_tensor(
87
+ torch_weight,
88
+ device=device,
89
+ dtype=weight_dtype,
90
+ layout=ttnn.ROW_MAJOR_LAYOUT,
91
+ memory_config=weight_memory_config,
92
+ cache_file_name=None if weight_cache_path is None else weight_cache_path / weight_name,
93
+ mesh_mapper=ttnn.ReplicateTensorToMesh(device) if is_mesh_device else None,
94
+ )
95
+
96
+ if self.is_distributed:
97
+ self.weight_distributed = ttnn.as_tensor(
98
+ torch_weight,
99
+ device=device,
100
+ dtype=weight_dtype,
101
+ layout=ttnn.ROW_MAJOR_LAYOUT,
102
+ memory_config=weight_memory_config,
103
+ cache_file_name=(
104
+ None if weight_cache_path is None else weight_cache_path / (weight_name + "_distributed")
105
+ ),
106
+ mesh_mapper=(
107
+ ttnn.ShardTensor2dMesh(device, dims=(None, 2), mesh_shape=list(device.shape))
108
+ if is_mesh_device
109
+ else None
110
+ ),
111
+ )
112
+
113
+ self.sharded_output_config = sharded_output_config
114
+ self.sharded_program_config = sharded_program_config
115
+ self.output_mem_config = output_mem_config
116
+
117
+ self.compute_kernel_config_hifi2 = ttnn.WormholeComputeKernelConfig(
118
+ math_fidelity=ttnn.MathFidelity.HiFi2,
119
+ math_approx_mode=False,
120
+ fp32_dest_acc_en=fp32_dest_acc_en,
121
+ packer_l1_acc=True,
122
+ )
123
+
124
+ def update(self, *, weight: ttnn.Tensor) -> None:
125
+ """In-place replace the RMSNorm gamma via ``ttnn.copy``.
126
+
127
+ HF-format input: ``weight`` is HF ``...norm.weight``, shape
128
+ ``(1, 1, 1, dim)``, bf16, TILE, DRAM-interleaved, replicated.
129
+
130
+ ``copy_to_buffer`` reshapes to the storage shape
131
+ ``(1, 1, dim // SHARD_HEIGHT, SHARD_HEIGHT)`` and TILE -> ROW_MAJOR to
132
+ match ``self.weight``. ``add_unit_offset`` is not supported (see
133
+ assert): the caller must ship a gamma that already includes the +1.
134
+
135
+ When ``self.weight_distributed`` (the column-sharded mirror) exists it's
136
+ kept in sync on device: project ``self.weight`` into the sharded layout
137
+ via ``ttnn.mesh_partition`` (the inverse of the constructor's
138
+ ``ShardTensor2dMesh(dims=(None, 2))``, hence ``dim=2, cluster_axis=1``)
139
+ and ``ttnn.copy`` into it. Both buffers keep their address, so captured
140
+ traces and the prefetcher's recorded addresses stay valid.
141
+ """
142
+ assert not self.add_unit_offset, "RMSNorm.update does not support add_unit_offset=True"
143
+ copy_to_buffer(weight, self.weight, self.weight.dtype)
144
+
145
+ if getattr(self, "weight_distributed", None) is not None:
146
+ partitioned = ttnn.mesh_partition(
147
+ self.weight,
148
+ memory_config=self.weight_distributed.memory_config(),
149
+ dim=2,
150
+ cluster_axis=1,
151
+ )
152
+ copy_to_buffer(partitioned, self.weight_distributed, self.weight_distributed.dtype)
153
+
154
+ def forward(
155
+ self,
156
+ x: ttnn.Tensor,
157
+ mode: Mode | str,
158
+ in_sharded=False,
159
+ out_sharded=False,
160
+ norm_config=None,
161
+ ) -> ttnn.Tensor:
162
+ if isinstance(mode, str):
163
+ try:
164
+ mode = Mode(mode)
165
+ except ValueError:
166
+ raise ValueError(f"Invalid mode: {mode}")
167
+ elif not isinstance(mode, Mode):
168
+ raise ValueError(f"Invalid mode: {mode}")
169
+
170
+ sharded_program_config = norm_config.get("sharded_program_config") if norm_config else None
171
+ sharded_output_config = norm_config.get("sharded_output_config") if norm_config else None
172
+ output_mem_config = norm_config.get("output_mem_config") if norm_config else None
173
+ # Optional L1 placement for the distributed 3-op outputs (pre/gather/post); None -> DRAM default.
174
+ distributed_out_mc = norm_config.get("distributed_output_mem_config") if norm_config else None
175
+
176
+ # If input is sharded do sharded RMSNorm and optionally return sharded output
177
+ program_config = sharded_program_config if in_sharded else None
178
+ memory_config = sharded_output_config if out_sharded else None
179
+ distributed = self.is_distributed and self.is_distributed(mode)
180
+ weight = self.weight_distributed if distributed else self.weight
181
+
182
+ if in_sharded:
183
+ assert not distributed, "Distributed RMSNorm does not support sharded inputs"
184
+ else:
185
+ assert not out_sharded, "Non-sharded version of RMSNorm cannot output a sharded tensor"
186
+
187
+ if distributed:
188
+ x = self._distributed_rmsnorm(
189
+ x,
190
+ epsilon=self.eps,
191
+ weight=weight,
192
+ compute_kernel_config=self.compute_kernel_config_hifi2,
193
+ output_memory_config=distributed_out_mc,
194
+ )
195
+ else:
196
+ x = ttnn.rms_norm(
197
+ x,
198
+ epsilon=self.eps,
199
+ weight=weight,
200
+ program_config=program_config,
201
+ memory_config=memory_config,
202
+ compute_kernel_config=self.compute_kernel_config_hifi2,
203
+ )
204
+
205
+ if in_sharded and not out_sharded:
206
+ return ttnn.sharded_to_interleaved(x)
207
+ else:
208
+ if output_mem_config is not None:
209
+ x = ttnn.to_memory_config(x, output_mem_config)
210
+ return x
211
+
212
+ def _distributed_rmsnorm(
213
+ self,
214
+ inp,
215
+ epsilon=None,
216
+ weight=None,
217
+ program_config=None,
218
+ memory_config=None,
219
+ compute_kernel_config=None,
220
+ output_memory_config=None,
221
+ ):
222
+ assert program_config is None, "Distributed RMSNorm does not support sharded inputs"
223
+ assert memory_config is None, "Distributed RMSNorm does not support sharded outputs"
224
+ assert self.tt_ccl is not None, "Distributed RMSNorm requires tt_ccl"
225
+
226
+ # Interleaved output placement for the 3 ops; default DRAM (matches the prior hardcoded behavior).
227
+ mc = output_memory_config if output_memory_config is not None else ttnn.DRAM_MEMORY_CONFIG
228
+
229
+ # Run distributed rmsnorm part 1
230
+ tt_stats = ttnn.rms_norm_pre_all_gather(
231
+ inp, compute_kernel_config=compute_kernel_config, dtype=ttnn.bfloat16, memory_config=mc
232
+ )
233
+ # AllGather stats
234
+ tt_stats = ttnn.experimental.all_gather_async(
235
+ tt_stats,
236
+ persistent_output_buffer=None,
237
+ dim=3,
238
+ multi_device_global_semaphore=self.tt_ccl.get_and_cycle_ag_semaphore_handles(),
239
+ num_links=1,
240
+ topology=self.ccl_topology,
241
+ memory_config=mc,
242
+ barrier_semaphore=self.tt_ccl.get_and_cycle_barrier_semaphore_handle(),
243
+ chunks_per_sync=10,
244
+ num_workers_per_link=2,
245
+ num_buffers_per_channel=2,
246
+ )
247
+ # Run distributed rmsnorm part 2
248
+ tt_out = ttnn.rms_norm_post_all_gather(
249
+ inp,
250
+ tt_stats,
251
+ epsilon=epsilon,
252
+ weight=weight,
253
+ compute_kernel_config=compute_kernel_config,
254
+ memory_config=mc,
255
+ )
256
+ tt_stats.deallocate(True)
257
+
258
+ return tt_out
code/models/common/tensor_utils.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """
5
+ Tensor utility functions for TTTv2 modules.
6
+ """
7
+
8
+ import json
9
+ import math
10
+ import re
11
+
12
+ import torch
13
+
14
+ import ttnn
15
+
16
+ # Standard tile size - hardware constant
17
+ TILE_SIZE = ttnn.TILE_SIZE # 32
18
+
19
+
20
+ def nearest_multiple(value: int, multiple: int) -> int:
21
+ return math.ceil(value / multiple) * multiple
22
+
23
+
24
+ def nearest_32(value: int) -> int:
25
+ return nearest_multiple(value, TILE_SIZE)
26
+
27
+
28
+ def num_to_core_range_set(num_cores: int):
29
+ assert num_cores < 8 or num_cores % 8 == 0
30
+ num_x = min(num_cores, 8)
31
+ num_y = num_cores // num_x
32
+ assert num_x * num_y == num_cores
33
+ return ttnn.CoreRangeSet(
34
+ {
35
+ ttnn.CoreRange(
36
+ ttnn.CoreCoord(0, 0),
37
+ ttnn.CoreCoord(num_x - 1, num_y - 1),
38
+ )
39
+ }
40
+ )
41
+
42
+
43
+ def get_out_subblock_w(per_core_n: int, out_subblock_h: int = 1) -> int:
44
+ out_subblock_w = 4
45
+ while out_subblock_w > 1:
46
+ if out_subblock_w * out_subblock_h <= 4 and per_core_n % out_subblock_w == 0:
47
+ break
48
+ out_subblock_w -= 1
49
+ return out_subblock_w
50
+
51
+
52
+ def align_shape_to_tile(shape, tile_size: int = TILE_SIZE):
53
+ """Round up the last two dimensions of *shape* to multiples of *tile_size*.
54
+
55
+ This is the recommended replacement for the deprecated ``ttnn.pad_to_tile_shape``.
56
+
57
+ Args:
58
+ shape: An iterable of dimension sizes (list, tuple, or ttnn.Shape).
59
+ tile_size: Tile dimension to align to (default 32).
60
+
61
+ Returns:
62
+ List[int]: A new shape with the last two dims tile-aligned.
63
+ """
64
+ import math
65
+
66
+ result = list(shape)
67
+ if len(result) >= 1:
68
+ result[-1] = math.ceil(result[-1] / tile_size) * tile_size
69
+ if len(result) >= 2:
70
+ result[-2] = math.ceil(result[-2] / tile_size) * tile_size
71
+ return result
72
+
73
+
74
+ def get_rot_transformation_mat(dhead: int = TILE_SIZE) -> torch.Tensor:
75
+ """
76
+ Create rotation transformation matrix for RoPE.
77
+
78
+ Constructs a permutation matrix that pairs adjacent dimensions with
79
+ signs (+1, -1) for the RoPE rotation:
80
+ [0, 1] → +1 at (0,1), -1 at (1,0)
81
+ [2, 3] → +1 at (2,3), -1 at (3,2)
82
+ ...
83
+
84
+ Used by ttnn.experimental.rotary_embedding_llama.
85
+
86
+ Args:
87
+ dhead: Matrix dimension. Must equal TILE_SIZE. Use TILE_SIZE for decode.
88
+
89
+ Returns:
90
+ torch.Tensor of shape [1, 1, dhead, dhead].
91
+ """
92
+ rot_emb_matrix = torch.zeros(1, 1, dhead, dhead)
93
+ rot_emb_matrix[..., torch.arange(0, dhead, 2), torch.arange(1, dhead, 2)] = 1
94
+ rot_emb_matrix[..., torch.arange(1, dhead, 2), torch.arange(0, dhead, 2)] = -1
95
+ return rot_emb_matrix
96
+
97
+
98
+ def zeros_like_kv_cache(batch_size: int, n_kv_heads: int, max_seq_len: int, head_dim: int) -> torch.Tensor:
99
+ """Create zeros tensor for standard KV cache."""
100
+ return torch.zeros((batch_size, n_kv_heads, max_seq_len, head_dim))
101
+
102
+
103
+ def zeros_like_paged_cache(paged_config, n_kv_heads: int, head_dim: int) -> torch.Tensor:
104
+ """Create zeros tensor for paged KV cache."""
105
+ return torch.zeros((paged_config.max_num_blocks, n_kv_heads, paged_config.block_size, head_dim))
106
+
107
+
108
+ # todo)) add a on-device pad_dim_to_size function?
109
+ def pad_dim_to_size(x: "torch.Tensor", dim: int, size: int) -> "torch.Tensor":
110
+ """Pads the specified dimension of the input tensor with zeros."""
111
+ if dim < 0:
112
+ dim = x.dim() + dim
113
+ current_size = x.size(dim)
114
+ pad_size = size - current_size
115
+
116
+ if pad_size < 0:
117
+ raise ValueError(f"Target size {size} is smaller than current size {current_size} on dim {dim}")
118
+
119
+ if pad_size == 0:
120
+ return x
121
+
122
+ pad = [0] * (2 * x.dim())
123
+ pad_index = 2 * (x.dim() - dim - 1)
124
+ pad[pad_index + 1] = pad_size
125
+
126
+ return torch.nn.functional.pad(x, pad, mode="constant", value=0)
127
+
128
+
129
+ def pad_to_shape(x: "torch.Tensor", target_shape: tuple[int, ...], pad_value: float = 0.0) -> "torch.Tensor":
130
+ """Pad tensor to target_shape in a single F.pad call (more efficient than per-dim padding)."""
131
+ if x.shape == target_shape:
132
+ return x
133
+
134
+ # F.pad expects: (left_last, right_last, left_second_last, right_second_last, ...)
135
+ pad = []
136
+ for orig, target in zip(reversed(x.shape), reversed(target_shape)):
137
+ if target < orig:
138
+ raise ValueError(f"Target size {target} is smaller than current size {orig}")
139
+ pad.extend([0, target - orig])
140
+
141
+ return torch.nn.functional.pad(x, pad, mode="constant", value=pad_value)
142
+
143
+
144
+ def get_padded_hidden_dim(hidden_dim: int, num_devices: int, tile_size: int = 32) -> int:
145
+ """
146
+ Compute padded hidden_dim to satisfy ttnn.from_torch's tile alignment constraint.
147
+
148
+ ttnn.from_torch requires physical shard shapes to be tile-aligned. When sharding
149
+ a tensor across devices, each shard_dim = hidden_dim / num_devices must be
150
+ divisible by tile_size.
151
+
152
+ We pad the global tensor first, then shard evenly so only the last shard has padding.
153
+ """
154
+ shard_dim = hidden_dim // num_devices
155
+ padded_shard = ((shard_dim + tile_size - 1) // tile_size) * tile_size
156
+ return padded_shard * num_devices
157
+
158
+
159
+ def parse_shard_dims_from_mesh_mapper_config(mesh_mapper_config: ttnn.MeshMapperConfig) -> list[int]:
160
+ """
161
+ Parse shard dimensions from MeshMapperConfig's repr.
162
+
163
+ MeshMapperConfig doesn't expose .placements directly, but repr shows them:
164
+ 'MeshMapperConfig(placements: [PlacementShard(-1)], mesh_shape_override=MeshShape([8]))'
165
+
166
+ This parses out the shard dimensions (e.g., [-1]) from PlacementShard entries.
167
+ Returns empty list if no PlacementShard found (e.g., replicated).
168
+
169
+ Note: This is a workaround until TTNN exposes .placements directly.
170
+ """
171
+ config_repr = repr(mesh_mapper_config)
172
+ matches = re.findall(r"PlacementShard\((-?\d+)\)", config_repr)
173
+ return [int(d) for d in matches]
174
+
175
+
176
+ def memory_config_to_dict(memory_config: ttnn.MemoryConfig):
177
+ # Convert to plain types for deterministic serialization.
178
+ return {
179
+ "memory_layout": str(memory_config.memory_layout),
180
+ "buffer_type": str(memory_config.buffer_type),
181
+ "shard_spec": str(memory_config.shard_spec),
182
+ "is_sharded": bool(memory_config.is_sharded()),
183
+ "interleaved": bool(memory_config.interleaved),
184
+ "hash": int(memory_config.__hash__()),
185
+ }
186
+
187
+
188
+ def compute_kernel_config_to_str(compute_kernel_config: ttnn.WormholeComputeKernelConfig):
189
+ # Backward compat shim; prefer compute_kernel_config_to_dict + serialize_config.
190
+ cfg = compute_kernel_config_to_dict(compute_kernel_config)
191
+ return serialize_config(cfg)
192
+
193
+
194
+ def compute_kernel_config_to_dict(compute_kernel_config: ttnn.WormholeComputeKernelConfig):
195
+ return {
196
+ "math_fidelity": str(compute_kernel_config.math_fidelity),
197
+ "math_approx_mode": str(compute_kernel_config.math_approx_mode),
198
+ "fp32_dest_acc_en": bool(compute_kernel_config.fp32_dest_acc_en),
199
+ "packer_l1_acc": bool(compute_kernel_config.packer_l1_acc),
200
+ "dst_full_sync_en": bool(compute_kernel_config.dst_full_sync_en),
201
+ "throttle_level": str(compute_kernel_config.throttle_level),
202
+ }
203
+
204
+
205
+ def program_config_to_str(program_config: ttnn.MatmulMultiCoreReuseMultiCastDRAMShardedProgramConfig):
206
+ # Backward compat shim; prefer program_config_to_dict + serialize_config.
207
+ cfg = program_config_to_dict(program_config)
208
+ return serialize_config(cfg)
209
+
210
+
211
+ def program_config_to_dict(program_config):
212
+ if hasattr(program_config, "to_json"):
213
+ d = json.loads(program_config.to_json())
214
+ d["type"] = type(program_config).__name__
215
+ return d
216
+ else:
217
+ return {"type": type(program_config).__name__, "repr": repr(program_config)}
218
+
219
+
220
+ def serialize_config(cfg_dict: dict, fmt: str = "json") -> str:
221
+ if fmt == "json":
222
+ return json.dumps(cfg_dict, sort_keys=True)
223
+ if fmt == "yaml":
224
+ try:
225
+ import yaml
226
+ except ImportError as exc: # pragma: no cover - optional dependency
227
+ raise RuntimeError("PyYAML is required for yaml serialization") from exc
228
+ return yaml.safe_dump(cfg_dict, sort_keys=True)
229
+ raise ValueError(f"Unsupported format: {fmt}")
code/models/common/utility_functions.py ADDED
@@ -0,0 +1,1266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2023 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import math
6
+ import os
7
+ import struct
8
+ import time
9
+ from typing import Union
10
+
11
+ import numpy as np
12
+ import pytest
13
+ import torch
14
+ from loguru import logger
15
+ from ttnn.device import Arch
16
+ from typing_extensions import deprecated
17
+
18
+ import ttnn
19
+
20
+
21
+ def get_mesh_device():
22
+ """Fixture to provide mesh device configuration."""
23
+ mesh_device = os.environ.get("MESH_DEVICE", "N150")
24
+ mesh_config = {
25
+ "N150": (1, 1),
26
+ "N300": (2, 1),
27
+ "T3K": (8, 1),
28
+ "TG": (8, 4),
29
+ }.get(mesh_device, (ttnn.get_num_devices(), 1))
30
+ return mesh_config
31
+
32
+
33
+ ### Math operations ###
34
+ def _nearest_32(x):
35
+ return math.ceil(x / 32) * 32
36
+
37
+
38
+ def nearest_32(
39
+ x,
40
+ ): # needs refctoring; to match alias called in some scripts (e.g. test_padding_test in unit tests)
41
+ return _nearest_32(x)
42
+
43
+
44
+ def _nearest_y(x, y):
45
+ return math.ceil(x / y) * y
46
+
47
+
48
+ def nearest_y(x, y):
49
+ return _nearest_y(x, y)
50
+
51
+
52
+ def divup(a, b):
53
+ return (a + b - 1) // b
54
+
55
+
56
+ def roundup(a, b):
57
+ result = divup(a, b) * b
58
+ return result
59
+
60
+
61
+ def roundup32(a):
62
+ return roundup(a, 32)
63
+
64
+
65
+ def float_to_bits(x):
66
+ s = struct.pack(">f", x)
67
+ return struct.unpack(">l", s)[0]
68
+
69
+
70
+ def torch_random(shape, low, high, dtype):
71
+ if dtype in [torch.int64, torch.int32, torch.int16, torch.int8]:
72
+ return torch.randint(low, high, shape, dtype=dtype)
73
+ return torch.zeros(shape, dtype=dtype).uniform_(low, high)
74
+
75
+
76
+ def torch_random_with_zeros(shape, low, high, dtype, zero_fraction=0.1):
77
+ total_elements = torch.prod(torch.tensor(shape)).item()
78
+ num_zeros = int(total_elements * zero_fraction)
79
+ num_random = total_elements - num_zeros
80
+
81
+ # Generate random values between low and high
82
+ random_values = torch.empty(num_random).uniform_(low, high)
83
+ zeros = torch.zeros(num_zeros)
84
+
85
+ # Combine zeros and random values
86
+ combined = torch.cat([zeros, random_values])
87
+
88
+ # Shuffle the tensor
89
+ shuffled = combined[torch.randperm(combined.size(0))]
90
+
91
+ # Reshape to the desired shape
92
+ result_tensor = shuffled.view(shape)
93
+ result_tensor.to(dtype)
94
+ return result_tensor
95
+
96
+
97
+ ### Profiling ###
98
+ class Profiler:
99
+ def __init__(self):
100
+ self.start_times = dict()
101
+ self.times = dict()
102
+ self.disabled = False
103
+
104
+ def clear(self):
105
+ self.start_times = dict()
106
+ self.times = dict()
107
+ self.disabled = False
108
+
109
+ def enable(self):
110
+ self.disabled = False
111
+
112
+ def disable(self):
113
+ self.disabled = True
114
+
115
+ def start(self, key, force_enable=False):
116
+ if self.disabled and not force_enable:
117
+ return
118
+
119
+ self.start_times[key] = time.time()
120
+
121
+ def end(self, key, PERF_CNT=1, force_enable=False):
122
+ if self.disabled and not force_enable:
123
+ return
124
+
125
+ if key not in self.start_times:
126
+ return
127
+
128
+ diff = time.time() - self.start_times[key]
129
+
130
+ if key not in self.times:
131
+ self.times[key] = []
132
+
133
+ self.times[key].append(diff / PERF_CNT)
134
+
135
+ def get(self, key):
136
+ if key not in self.times:
137
+ return 0
138
+
139
+ return sum(self.times[key]) / len(self.times[key])
140
+
141
+ def print(self, units="s"):
142
+ for key in self.times:
143
+ average = self.get(key)
144
+ if units == "s":
145
+ pass
146
+ elif units == "ms":
147
+ average *= 1000
148
+ elif units == "us":
149
+ average *= 1000000
150
+ elif units == "ns":
151
+ average *= 1000000000
152
+ else:
153
+ raise ValueError(f"Invalid units: {units}")
154
+ print(f"{key}: {average:.3f}{units}")
155
+
156
+
157
+ profiler = Profiler()
158
+
159
+
160
+ ### Turn flags on/off ###
161
+ def enable_memory_reports():
162
+ """
163
+ Enables generating reports of memory allocation statistics in .reports/tt_metal dir
164
+ """
165
+ return ttnn.device.EnableMemoryReports()
166
+
167
+
168
+ def disable_memory_reports():
169
+ """
170
+ Disables generating reports of memory allocation statistics
171
+ """
172
+ return ttnn.device.DisableMemoryReports()
173
+
174
+
175
+ ### Tensor conversion ###
176
+ def torch2tt_tensor(
177
+ py_tensor: torch.Tensor,
178
+ tt_device,
179
+ tt_layout=ttnn.TILE_LAYOUT,
180
+ tt_memory_config=ttnn.MemoryConfig(ttnn.TensorMemoryLayout.INTERLEAVED),
181
+ tt_dtype=ttnn.bfloat16,
182
+ ):
183
+ size = list(py_tensor.size())
184
+
185
+ while len(size) < 4:
186
+ size.insert(0, 1)
187
+
188
+ tt_tensor = ttnn.Tensor(py_tensor.reshape(size), tt_dtype)
189
+ tt_tensor = tt_tensor.to(tt_layout)
190
+
191
+ if tt_device is not None:
192
+ tt_tensor = tt_tensor.to(tt_device, tt_memory_config)
193
+ else:
194
+ tt_tensor = tt_tensor.cpu()
195
+
196
+ return tt_tensor
197
+
198
+
199
+ def tt_tensors_to_torch_tensors(
200
+ tt_tensors_device: ttnn.Tensor, mesh_device: Union[ttnn.MeshDevice, ttnn.Device], concat_dim: int = 0
201
+ ):
202
+ # Convert tensors to interleaved
203
+ if tt_tensors_device.is_sharded():
204
+ tt_tensors_device = ttnn.sharded_to_interleaved(tt_tensors_device)
205
+
206
+ # Convert tensors to RM layout
207
+ if tt_tensors_device.layout == ttnn.TILE_LAYOUT:
208
+ # Convert to bfloat16 to ensure untilize works
209
+ if tt_tensors_device.dtype != ttnn.bfloat16:
210
+ tt_tensors_device = ttnn.clone(
211
+ tt_tensors_device, dtype=ttnn.bfloat16, memory_config=ttnn.DRAM_MEMORY_CONFIG
212
+ )
213
+ # Untilize using singlecore since multicore version runs out of l1 memory (Issue #9022)
214
+ tt_tensors_device = ttnn.untilize(tt_tensors_device, use_multicore=False)
215
+
216
+ return torch.cat([t.to_torch() for t in ttnn.get_device_tensors(tt_tensors_device.cpu())], dim=concat_dim)
217
+
218
+
219
+ def tt2torch_tensor(tt_tensor):
220
+ tt_output = tt_tensor.cpu()
221
+ if tt_output.get_layout() != ttnn.ROW_MAJOR_LAYOUT:
222
+ tt_output = tt_output.to(ttnn.ROW_MAJOR_LAYOUT)
223
+ return tt_output.to_torch()
224
+
225
+
226
+ def tt_to_torch_tensor(tt_tensor):
227
+ tt_output = tt_tensor.cpu().to(ttnn.ROW_MAJOR_LAYOUT)
228
+ return tt_output.to_torch()
229
+
230
+
231
+ def torch_to_tt_tensor_rm(py_tensor, device, shape=None, put_on_device=True):
232
+ if shape is None:
233
+ shape = list(py_tensor.size())
234
+ while len(shape) < 4:
235
+ shape.insert(0, 1)
236
+
237
+ tt_tensor = ttnn.Tensor(py_tensor.reshape(shape), ttnn.bfloat16)
238
+ if put_on_device:
239
+ tt_tensor = tt_tensor.to(device)
240
+ return tt_tensor
241
+
242
+
243
+ def torch_to_tt_tensor(py_tensor, device):
244
+ shape = list(py_tensor.size())
245
+ while len(shape) < 4:
246
+ shape.insert(0, 1)
247
+
248
+ tt_tensor = (
249
+ ttnn.Tensor(py_tensor.reshape(shape), ttnn.bfloat16)
250
+ .to(
251
+ ttnn.TILE_LAYOUT
252
+ ) # change memory layout of TT Tensor to TILE (as operation that will use it expects TILE layout)
253
+ .to(device) # move TT Tensor from host to TT accelerator device (device is of type ttnn.device.Device)
254
+ )
255
+
256
+ return tt_tensor
257
+
258
+
259
+ def unpad_from_zero(x, desired_shape):
260
+ if x.padded_shape[-1] == desired_shape[-1] and x.padded_shape[-2] == desired_shape[-2]:
261
+ x = tt2torch_tensor(x)
262
+ else:
263
+ x = x.cpu()
264
+ if x.get_layout() != ttnn.ROW_MAJOR_LAYOUT:
265
+ x = x.to(ttnn.ROW_MAJOR_LAYOUT)
266
+ x = x.unpad(
267
+ (0, 0, 0, 0),
268
+ (
269
+ desired_shape[0],
270
+ desired_shape[1],
271
+ desired_shape[2],
272
+ desired_shape[3],
273
+ ),
274
+ )
275
+
276
+ x = x.to_torch()
277
+ return x
278
+
279
+
280
+ def pad_activation(x):
281
+ """
282
+ This function pads an activation with 0s as a pre-preprocessing step to tilization.
283
+
284
+ In the 2d case, it pads a vector to the right with 0s, and in the 2+d case,
285
+ it pads the bottom and right corners of the last two dimensions.
286
+
287
+ :param x: Input PyTorch Tensor
288
+ :type x: class:`torch.Tensor`
289
+
290
+ WARNING: This function should eventually be retired in favour of padding on device
291
+ """
292
+ nearest_32 = _nearest_32
293
+
294
+ assert isinstance(x, torch.Tensor), "Input to this function must be an instance of torch.Tensor"
295
+ assert len(x.shape) >= 1 and len(x.shape) <= 4, "Only tensors with dimension 1-4 supported"
296
+ if len(x.shape) == 1: # (num_features,)
297
+ padded_tensor = torch.zeros(1, 1, 32, nearest_32(x.shape[0]))
298
+ padded_tensor[:, 0, 0, : x.shape[0]] = x
299
+ elif len(x.shape) == 2: # (batch, num features)
300
+ padded_tensor = torch.zeros(x.shape[0], 1, 32, nearest_32(x.shape[1]))
301
+ padded_tensor[:, 0, 0, : x.shape[1]] = x
302
+ elif len(x.shape) == 3: # (batch, num features y, num features x)
303
+ padded_tensor = torch.zeros(x.shape[0], 1, nearest_32(x.shape[-2]), nearest_32(x.shape[-1]))
304
+ padded_tensor[..., 0, : x.shape[-2], : x.shape[-1]] = x
305
+ else: # (batch, num channels, num features y, num features x)
306
+ padded_tensor = torch.zeros(*x.shape[:-2], nearest_32(x.shape[-2]), nearest_32(x.shape[-1]))
307
+ padded_tensor[..., : x.shape[-2], : x.shape[-1]] = x
308
+ return padded_tensor
309
+
310
+
311
+ def pad_weight(x):
312
+ """
313
+ This function pads a weight/bias with 0s as a pre-preprocessing step to tilization.
314
+
315
+ tt_tensor = ttnn.Tensor(
316
+ py_tensor.reshape(shape), ttnn.bfloat16
317
+ In the 2d case, it pads a vector to the right with 0s, and in the 2+d case,
318
+ it pads the bottom and right corners of the last two dimensions.
319
+
320
+ :param x: Input PyTorch Tensor
321
+ :type x: class:`torch.Tensor`
322
+
323
+ WARNING: This function should eventually be retired in favour of padding on device
324
+ """
325
+ nearest_32 = _nearest_32
326
+
327
+ assert isinstance(x, torch.Tensor), "Input to this function must be an instance of torch.Tensor"
328
+ assert len(x.shape) >= 1 and len(x.shape) <= 4, "Only tensors with dimension 1-4 supported"
329
+
330
+ if len(x.shape) == 1: # (num_features,)
331
+ padded_tensor = torch.zeros(1, 1, 32, nearest_32(x.shape[0]))
332
+ padded_tensor[:, 0, 0, : x.shape[0]] = x
333
+ elif len(x.shape) == 2: # (r_features, c_features)
334
+ padded_tensor = torch.zeros(1, 1, nearest_32(x.shape[0]), nearest_32(x.shape[1]))
335
+ padded_tensor[:, 0, : x.shape[0], : x.shape[1]] = x
336
+ else:
337
+ padded_tensor = torch.zeros(*x.shape[:-2], nearest_32(x.shape[-2]), nearest_32(x.shape[-1]))
338
+ padded_tensor[..., : x.shape[-2], : x.shape[-1]] = x
339
+
340
+ return padded_tensor
341
+
342
+
343
+ def convert_weights_2d_matrix(weights, w_shape):
344
+ """
345
+ :param weights: Input PyTorch Tensor
346
+ :type weights: class:`torch.Tensor`
347
+ """
348
+ ret_shape = [1, 1, w_shape[0], w_shape[1] * w_shape[2] * w_shape[3]]
349
+ if isinstance(weights, torch.Tensor):
350
+ ret = torch.zeros(np.prod(ret_shape))
351
+ else:
352
+ ret = np.zeros(np.prod(ret_shape))
353
+ idx = 0
354
+ for k in range(w_shape[0]):
355
+ for r in range(w_shape[2]):
356
+ for s in range(w_shape[3]):
357
+ for c in range(w_shape[1]):
358
+ ret[idx] = weights[k][c][r][s]
359
+ idx += 1
360
+ assert idx == np.prod(ret_shape)
361
+ return ret.reshape(ret_shape).transpose(2, 3)
362
+
363
+
364
+ def convert_act_2d_matrix(activation, kernel_y, kernel_x, stride_y, stride_x, pad_y, pad_x):
365
+ """
366
+ :param activation: Input PyTorch Tensor
367
+ :type activation: class:`torch.Tensor`
368
+ """
369
+ N = activation.shape[0]
370
+ C = activation.shape[1]
371
+ H = activation.shape[2]
372
+ W = activation.shape[3]
373
+
374
+ OH = (int)((H - kernel_y + 2 * pad_y) // stride_y) + 1
375
+ OW = ((W - kernel_x + 2 * pad_x) // stride_x) + 1
376
+ nrows = OH * OW
377
+ ncols = C * kernel_x * kernel_y
378
+ ret_shape = [1, N, nrows, ncols]
379
+ if isinstance(activation, torch.Tensor):
380
+ ret = torch.zeros(np.prod(ret_shape))
381
+ else:
382
+ ret = np.zeros(np.prod(ret_shape))
383
+ idx = 0
384
+ for n in range(N):
385
+ for h in range(-1 * pad_y, H + pad_y - kernel_y + 1, stride_y):
386
+ for w in range(-1 * pad_x, W + pad_x - kernel_x + 1, stride_x):
387
+ for r in range(kernel_y):
388
+ for s in range(kernel_x):
389
+ for c in range(C):
390
+ h_offs = h + r
391
+ w_offs = w + s
392
+ pad = h_offs < 0 or h_offs >= H or w_offs < 0 or w_offs >= W
393
+ ret[idx] = 0 if pad else activation[n][c][h_offs][w_offs]
394
+ idx += 1
395
+ assert idx == np.prod(ret_shape)
396
+ return ret.reshape(ret_shape)
397
+
398
+
399
+ ### Tilizing / Untilizing ###
400
+ @deprecated("PyTorch data is handled automatically in tensor infra. This function does nothing now:")
401
+ def tilize(x):
402
+ return x
403
+
404
+
405
+ @deprecated("PyTorch data is handled automatically in tensor infra. This function does nothing now:")
406
+ def tilize_to_list(x):
407
+ """
408
+ Returns a flattened list of the tensor
409
+ """
410
+ return tilize(x).reshape(-1).tolist()
411
+
412
+
413
+ @deprecated("PyTorch data is handled automatically in tensor infra. This function does nothing now:")
414
+ def untilize(x):
415
+ return x
416
+
417
+
418
+ ### Measuring accuracy and other metrics ###
419
+ def is_close(a, b, rtol=1e-2, atol=1e-2, max_mag=2.0, max_mag_fraction=0.02):
420
+ """
421
+ A variant of np.isclose with logging.
422
+ """
423
+ absdiff = (a - b).abs()
424
+ reldiff1 = (a.abs() / b.abs()) - 1.0
425
+ reldiff2 = (a.abs() + 1.0) / (b.abs() + 1.0) - 1.0 # in case b.abs() is 0
426
+ reldiff_or = torch.logical_or(reldiff1.abs() < rtol, reldiff2.abs() < rtol)
427
+ max_mag_ok = absdiff < max_mag * max_mag_fraction
428
+
429
+ or_abs_rel = torch.logical_or(absdiff < atol, reldiff_or)
430
+ or_abs_rel = torch.logical_or(or_abs_rel, max_mag_ok)
431
+ debug_index = or_abs_rel.to(torch.int32).argmin().item()
432
+
433
+ if not or_abs_rel.reshape(-1)[debug_index]:
434
+ logger.info(f"isclose mismatch at index={debug_index}")
435
+ logger.info(a.reshape(-1)[debug_index])
436
+ logger.info(b.reshape(-1)[debug_index])
437
+ logger.info(f"reldiff1={reldiff1.reshape(-1)[debug_index]}")
438
+ logger.info(f"reldiff2={reldiff2.reshape(-1)[debug_index]}")
439
+ logger.info(f"absdiff={absdiff.reshape(-1)[debug_index]}")
440
+
441
+ HT = a.shape[-2] // 32
442
+ WT = a.shape[-1] // 32
443
+ hwt = debug_index // 1024
444
+ wt = hwt % WT
445
+ ht = hwt // WT
446
+ h = (debug_index % 1024) // 32
447
+ w = (debug_index % 1024) % 32
448
+
449
+ logger.info(f"**** at {debug_index} --- HTWT={ht} {wt} HW={h} {w}")
450
+
451
+ return torch.all(or_abs_rel)
452
+
453
+
454
+ def _comp_nonfinite(golden, calculated):
455
+ """
456
+ Returns True if tensors contain the same non-finite values (nan, inf, -inf) at the same positions. Also returns True if all elements are finite.
457
+ Returns False if non-finite values differ between both tensors.
458
+ """
459
+
460
+ # torch.equal(['nan'], ['nan']] => False
461
+ # For this reason, we check for nan and inf separately
462
+ if torch.not_equal(torch.isnan(golden), torch.isnan(calculated)).any():
463
+ return False
464
+
465
+ golden_inf_mask = torch.isinf(golden)
466
+ calculated_inf_mask = torch.isinf(calculated)
467
+
468
+ if torch.not_equal(golden_inf_mask, calculated_inf_mask).any():
469
+ return False
470
+
471
+ golden_inf = golden[golden_inf_mask]
472
+ calculated_inf = calculated[calculated_inf_mask]
473
+ return torch.equal(golden_inf, calculated_inf)
474
+
475
+
476
+ def comp_allclose(golden, calculated, rtol=1e-05, atol=1e-08):
477
+ if golden.dtype != calculated.dtype:
478
+ calculated = calculated.type(golden.dtype)
479
+
480
+ atol_delta = torch.max(torch.abs(golden - calculated)).item()
481
+ rtol_delta = torch.max(torch.abs(golden - calculated) / torch.abs(calculated)).item()
482
+ return (
483
+ torch.allclose(golden, calculated, rtol, atol, True),
484
+ f"Max ATOL Delta: {atol_delta}, Max RTOL Delta: {rtol_delta}",
485
+ )
486
+
487
+
488
+ def comp_pcc(golden, calculated, pcc=0.99, rtol=1e-05, atol=1e-04):
489
+ golden = torch.Tensor(golden)
490
+ calculated = torch.Tensor(calculated)
491
+
492
+ if golden.dtype != calculated.dtype:
493
+ calculated = calculated.type(golden.dtype)
494
+
495
+ if torch.all(torch.isnan(golden)) and torch.all(torch.isnan(calculated)):
496
+ logger.warning("Both tensors are 'nan'")
497
+ return True, 1.0
498
+
499
+ if torch.all(torch.isnan(golden)) or torch.all(torch.isnan(calculated)):
500
+ logger.error("One tensor is all nan, the other is not.")
501
+ return False, 0.0
502
+
503
+ # Test if either is completely zero — but a zero tensor is also a constant tensor,
504
+ # so fall back to allclose instead of a hard 0.0: zero-vs-small-constant may be
505
+ # within the caller's tolerances.
506
+ if torch.any(golden.bool()) != torch.any(calculated.bool()):
507
+ logger.warning("One tensor is all zero. PCC undefined; falling back to allclose.")
508
+ result = torch.allclose(golden, calculated, rtol=rtol, atol=atol)
509
+ return result, float(result)
510
+
511
+ golden = torch.squeeze(golden).flatten()
512
+ calculated = torch.squeeze(calculated).flatten()
513
+
514
+ # For now, mask all infs and nans (to zero) so that we check the rest... TODO
515
+ # Skip this for integer types which don't have NaN/Inf values.
516
+ if golden.dtype.is_floating_point:
517
+ # FP8 doesn't support isfinite/nan_to_num and bfloat16 products lose precision,
518
+ # so correlate these in float32.
519
+ if golden.dtype in (torch.float8_e4m3fn, torch.float8_e5m2, torch.bfloat16):
520
+ golden = golden.to(torch.float32)
521
+ calculated = calculated.to(torch.float32)
522
+
523
+ # Zero out NaN/Inf, preserving the historical PCC values. nan_to_num allocates a
524
+ # full-size copy of each tensor, so only do it when invalid values are actually
525
+ # present; on the common all-finite path the tensors stay as views and no copy is
526
+ # made (this short-circuit is what keeps peak memory near 1x of one input).
527
+ if not bool((torch.isfinite(golden) & torch.isfinite(calculated)).all()):
528
+ golden = torch.nan_to_num(golden, nan=0.0, posinf=0.0, neginf=0.0)
529
+ calculated = torch.nan_to_num(calculated, nan=0.0, posinf=0.0, neginf=0.0)
530
+
531
+ if torch.equal(golden, calculated):
532
+ return True, 1.0
533
+
534
+ # Integer tensors must be correlated in floating point (centering/products would
535
+ # otherwise truncate/overflow). float32 keeps the working set small.
536
+ if not golden.dtype.is_floating_point:
537
+ golden = golden.to(torch.float32)
538
+ calculated = calculated.to(torch.float32)
539
+
540
+ # Pearson r with float64 *accumulation* (dtype= on the reductions) over the float32
541
+ # data: no float64 copy of either tensor is materialized, so peak memory stays near
542
+ # 1x of one input on large tensors while matching a full-float64 correlation to
543
+ # |Δ|<1e-9 across the high-PCC (>=0.999) range.
544
+ n = golden.numel()
545
+ g_centered = golden - (golden.sum(dtype=torch.float64) / n).to(golden.dtype)
546
+ c_centered = calculated - (calculated.sum(dtype=torch.float64) / n).to(calculated.dtype)
547
+ cov = (g_centered * c_centered).sum(dtype=torch.float64)
548
+ g_sq_sum = g_centered.pow(2).sum(dtype=torch.float64)
549
+ c_sq_sum = c_centered.pow(2).sum(dtype=torch.float64)
550
+ denom = torch.sqrt(g_sq_sum * c_sq_sum)
551
+ # pow/sum stay in float32 before the reduction; large-magnitude tensors (e.g. ldexp)
552
+ # can overflow to inf here even though float64 accumulation would be finite.
553
+ if not math.isfinite(denom.item()) or not math.isfinite(cov.item()):
554
+ g_centered64 = g_centered.to(torch.float64)
555
+ c_centered64 = c_centered.to(torch.float64)
556
+ cov = (g_centered64 * c_centered64).sum()
557
+ denom = torch.sqrt(g_centered64.pow(2).sum() * c_centered64.pow(2).sum())
558
+ cal_pcc = (cov / denom).item()
559
+
560
+ # Zero variance -> denom == 0 -> cal_pcc is nan: PCC is undefined for constant tensors.
561
+ # Fall back to allclose rather than returning a misleading 1.0.
562
+ if math.isnan(cal_pcc):
563
+ logger.warning("PCC is NaN (zero variance / constant tensor). Falling back to allclose check.")
564
+ result = torch.allclose(golden, calculated, rtol=rtol, atol=atol)
565
+ return result, float(result)
566
+
567
+ return cal_pcc >= pcc, cal_pcc
568
+
569
+
570
+ def ulp(x: Union[ttnn.Tensor, torch.Tensor]) -> Union[ttnn.Tensor, torch.Tensor]:
571
+ "Return Unit of Least Precision for each element of a given tensor"
572
+
573
+ received_ttnn_input = False
574
+ if isinstance(x, ttnn.Tensor):
575
+ x = ttnn.to_torch(x)
576
+ received_ttnn_input = True
577
+
578
+ # Notes:
579
+ # - This should be identical to the definition of ULP by Goldberg
580
+ # "What every computer scientist should know about floating-point arithmetic"
581
+ # https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
582
+ # - We use torch.abs(x) to ensure symmetry ULP(-x) == ULP(x)
583
+ # - For x powers of 2, x + ULP(x) is not closest number but second closest (previous number is 2x closer)
584
+ # However, this avoids rounding-to-nearest-tie-to-even issues on addition (i.e. x + ULP(x) != x)
585
+ abs_x = torch.abs(x)
586
+ next = torch.nextafter(
587
+ abs_x, torch.tensor(math.inf, dtype=x.dtype)
588
+ ) # 1 ULP ~ Difference between two consecutive floating point numbers
589
+ ulp_value = next - abs_x
590
+
591
+ # Special case: if abs_x == torch.finfo(x.dtype).max, then next == math.inf, which leads to ULP(x) == inf rather than finite number
592
+ # We fix this problem by manually calculating ULP at max value, and masking tensor when input == max
593
+ dtype_max = torch.finfo(x.dtype).max
594
+ max_epsilon = dtype_max - torch.nextafter(
595
+ torch.tensor(dtype_max, dtype=x.dtype), torch.tensor(-math.inf, dtype=x.dtype)
596
+ )
597
+ ulp_value = torch.where(abs_x == dtype_max, max_epsilon, ulp_value)
598
+
599
+ if received_ttnn_input: # Ensures that type(input) == type(output)
600
+ ulp_value = ttnn.from_torch(ulp_value)
601
+
602
+ return ulp_value
603
+
604
+
605
+ def comp_ulp(golden, calculated, ulp_threshold, allow_nonfinite=False):
606
+ """
607
+ Compute absolute error between two tensors in Units of Least Precision (ULP)
608
+ """
609
+
610
+ # If both tensors are empty, then we can return True
611
+ if torch.numel(golden) == 0 and torch.numel(calculated) == 0:
612
+ return True, "Both tensors are empty"
613
+
614
+ if not allow_nonfinite and not torch.all(torch.isfinite(calculated)):
615
+ return False, "Calculated tensor contains non-finite values"
616
+
617
+ if not _comp_nonfinite(golden, calculated):
618
+ return False, "Tensors are not finite at the same positions"
619
+ # nonfinite elements can interfere with ULP error calculation
620
+ # To avoid this, replace nan, +inf, -inf with 0
621
+ # (we have already checked that both tensors have the same nonfinite elements)
622
+ mask_finite = ~torch.isfinite(golden)
623
+ golden = golden.clone()
624
+ calculated = calculated.clone()
625
+ golden[mask_finite] = 0
626
+ calculated[mask_finite] = 0
627
+
628
+ # ULP is measured according to the golden tensor
629
+ # In most cases, data type of golden tensor should be the same as calculated tensor.
630
+ # However, in some cases, we may want to measure < 1 ULP differences, which requires golden tensor
631
+ # to have higher precision than calculated tensor.
632
+ # If we passed golden tensor to ulp() as is, we would get ULP of higher precision.
633
+ # e.g. ulp of float32 rather bfloat16 calculation, which would give us a wrong value.
634
+ ulp_value = ulp(golden.type(calculated.dtype))
635
+
636
+ if golden.dtype != calculated.dtype: # Note: assumes that golden has higher precision than calculated tensor
637
+ calculated = calculated.type(golden.dtype)
638
+ ulp_value = ulp_value.type(golden.dtype) # Convert ULP to higher precision (for sub-1 ULP measurements)
639
+
640
+ ulp_tensor = torch.abs(calculated - golden) / ulp_value
641
+ ulp_delta = torch.max(ulp_tensor)
642
+ within_threshold = ulp_delta <= ulp_threshold
643
+ message = f"Max ULP Delta: {ulp_delta}"
644
+ if not within_threshold:
645
+ ulp_index = torch.argmax(ulp_tensor)
646
+ ulp_index_tuple = tuple(int(idx) for idx in torch.unravel_index(ulp_index, golden.shape))
647
+ message += (
648
+ f" @ {list(ulp_index_tuple)} = "
649
+ f"|calculated {calculated[ulp_index_tuple]} - golden {golden[ulp_index_tuple]}| "
650
+ f"/ ULP(golden) {ulp_value[ulp_index_tuple]}"
651
+ )
652
+ return (within_threshold, message)
653
+
654
+
655
+ def calculate_detailed_ulp_stats(expected, actual):
656
+ """
657
+ Calculate detailed ULP statistics for analysis.
658
+
659
+ Returns:
660
+ dict: Dictionary with ULP statistics including max, mean, std, and percentiles
661
+ """
662
+ if isinstance(actual, ttnn.Tensor):
663
+ actual = ttnn.to_torch(actual)
664
+ if isinstance(expected, ttnn.Tensor):
665
+ expected = ttnn.to_torch(expected)
666
+
667
+ # Convert to bfloat16 if not already
668
+ expected = expected.to(torch.bfloat16)
669
+ actual = actual.to(torch.bfloat16)
670
+
671
+ # Handle special cases
672
+ if torch.allclose(expected, actual, rtol=0, atol=0, equal_nan=True):
673
+ return {
674
+ "max_ulp": 0.0,
675
+ "mean_ulp": 0.0,
676
+ "median_ulp": 0.0,
677
+ "std_ulp": 0.0,
678
+ "p95_ulp": 0.0,
679
+ "p99_ulp": 0.0,
680
+ "perfect_matches": 1.0,
681
+ }
682
+
683
+ # Convert bfloat16 to uint16 representation for bit manipulation
684
+ expected_bits = expected.view(torch.int16).to(torch.int32)
685
+ actual_bits = actual.view(torch.int16).to(torch.int32)
686
+
687
+ # Handle sign differences
688
+ expected_sign = expected_bits < 0
689
+ actual_sign = actual_bits < 0
690
+ same_sign = expected_sign == actual_sign
691
+
692
+ # Calculate ULP differences
693
+ expected_abs_bits = torch.where(expected_sign, -expected_bits, expected_bits)
694
+ actual_abs_bits = torch.where(actual_sign, -actual_bits, actual_bits)
695
+
696
+ ulp_diff = torch.where(same_sign, torch.abs(expected_bits - actual_bits), expected_abs_bits + actual_abs_bits)
697
+
698
+ # Handle non-finite values
699
+ expected_finite = torch.isfinite(expected)
700
+ actual_finite = torch.isfinite(actual)
701
+ both_finite = expected_finite & actual_finite
702
+
703
+ ulp_diff = torch.where(both_finite, ulp_diff, torch.tensor(float("inf")))
704
+
705
+ # Handle same non-finite values
706
+ both_nan = torch.isnan(expected) & torch.isnan(actual)
707
+ both_posinf = torch.isposinf(expected) & torch.isposinf(actual)
708
+ both_neginf = torch.isneginf(expected) & torch.isneginf(actual)
709
+ same_nonfinite = both_nan | both_posinf | both_neginf
710
+
711
+ ulp_diff = torch.where(same_nonfinite, torch.tensor(0.0), ulp_diff)
712
+
713
+ # Calculate statistics only on finite ULP differences
714
+ finite_ulp = ulp_diff[torch.isfinite(ulp_diff)]
715
+
716
+ if len(finite_ulp) == 0:
717
+ return {
718
+ "max_ulp": float("inf"),
719
+ "mean_ulp": float("inf"),
720
+ "median_ulp": float("inf"),
721
+ "std_ulp": float("inf"),
722
+ "p95_ulp": float("inf"),
723
+ "p99_ulp": float("inf"),
724
+ "perfect_matches": 0.0,
725
+ }
726
+
727
+ finite_ulp_float = finite_ulp.float()
728
+ perfect_matches = (finite_ulp == 0).float().mean().item()
729
+
730
+ return {
731
+ "max_ulp": torch.max(finite_ulp).item(),
732
+ "mean_ulp": torch.mean(finite_ulp_float).item(),
733
+ "median_ulp": torch.median(finite_ulp_float).item(),
734
+ "std_ulp": torch.std(finite_ulp_float).item(),
735
+ "p95_ulp": torch.quantile(finite_ulp_float, 0.95).item(),
736
+ "p99_ulp": torch.quantile(finite_ulp_float, 0.99).item(),
737
+ "perfect_matches": perfect_matches,
738
+ }
739
+
740
+
741
+ def comp_allclose_and_pcc(golden, calculated, rtol=1e-05, atol=1e-08, pcc=0.99):
742
+ # 0-volume tensors are special because they don't have elements, so we can't compute PCC, etc.
743
+ # If one of the tensors is a 0-volume tensor, simply call torch.equal to check if they are equal
744
+ # (i.e. that both are 0-volume tensors and they have equal shapes).
745
+ if golden.numel() == 0 or calculated.numel() == 0:
746
+ return torch.equal(golden, calculated), f"{golden} != {calculated}"
747
+
748
+ if golden.dtype != calculated.dtype:
749
+ calculated = calculated.type(golden.dtype)
750
+
751
+ passing = True
752
+ output = ""
753
+ passing_allclose, output_allclose = comp_allclose(golden, calculated, rtol, atol)
754
+ passing &= passing_allclose
755
+ output += output_allclose
756
+ if torch.numel(golden) != 1:
757
+ passing_pcc, output_pcc = comp_pcc(golden, calculated, pcc, rtol=rtol, atol=atol)
758
+ passing &= passing_pcc
759
+ output += f", pcc={output_pcc}"
760
+
761
+ return passing, output
762
+
763
+
764
+ def comp_equal(golden, calculated):
765
+ if golden.dtype != calculated.dtype:
766
+ calculated = calculated.type(golden.dtype)
767
+
768
+ # If either tensor is zero-volume, broadcasting can still yield an empty delta and
769
+ # crash torch.max(); defer entirely to torch.equal (False on shape mismatch).
770
+ if golden.numel() == 0 or calculated.numel() == 0:
771
+ return torch.equal(golden, calculated), f"{golden} != {calculated}"
772
+
773
+ atol_delta = torch.max(torch.abs(golden - calculated)).item()
774
+ rtol_delta = torch.max(torch.abs(golden - calculated) / torch.abs(calculated)).item()
775
+ return (
776
+ torch.equal(golden, calculated),
777
+ f"Max ATOL Delta: {atol_delta}, Max RTOL Delta: {rtol_delta}",
778
+ )
779
+
780
+
781
+ def get_oom_of_float(float_lst):
782
+ """
783
+ Given a list of floats, returns a list of the order or magnitudes
784
+ of the floats. Useful when you want to make sure that even if your
785
+ tt outputs don't match pytorch all that well, they are at least
786
+ on the same order of magnitude
787
+ """
788
+ ooms = []
789
+ for el in float_lst:
790
+ str_el = str(el)
791
+ if "e" in str_el:
792
+ oom = int(str_el.split("e")[1])
793
+ elif str_el[:2] == "0.":
794
+ str_el = str_el.split(".")[1]
795
+
796
+ oom = -1
797
+ for e in str_el:
798
+ if e != "0":
799
+ break
800
+ oom -= 1
801
+ else:
802
+ oom = len(str_el.split(".")[0])
803
+
804
+ ooms.append(oom)
805
+
806
+ return ooms
807
+
808
+
809
+ def print_diff_argmax(a, b, annotation=""):
810
+ """
811
+ Prints out the value of both tensors at a point where the absolute difference is the largest.
812
+ """
813
+ absdiff = (a - b).abs()
814
+ argmax = absdiff.argmax().item()
815
+ diff = absdiff.reshape(-1)[argmax]
816
+ rela = a.abs() / (torch.max(a.abs(), b.abs()))
817
+ relb = b.abs() / (torch.max(a.abs(), b.abs()))
818
+ HT = a.shape[-2] // 32
819
+ WT = a.shape[-1] // 32
820
+ hwt = argmax // 1024
821
+ wt = hwt % WT
822
+ ht = hwt // WT
823
+ h = (argmax % 1024) // 32
824
+ w = (argmax % 1024) % 32
825
+ print(
826
+ "Abs diff=",
827
+ diff,
828
+ " at ",
829
+ argmax,
830
+ " --- ",
831
+ annotation,
832
+ "HTWT=",
833
+ ht,
834
+ wt,
835
+ "HW=",
836
+ h,
837
+ w,
838
+ )
839
+ print(" (a=", a.reshape(-1)[argmax].item(), ")")
840
+ print(" (b=", b.reshape(-1)[argmax].item(), ")")
841
+ print(" Rel a=", rela.reshape(-1)[argmax], " at ", argmax)
842
+ print(" Rel b=", relb.reshape(-1)[argmax], " at ", argmax)
843
+ return diff.item()
844
+
845
+
846
+ def print_diff_tt_pyt(a, b, annotation=""):
847
+ # first convert a pytorch tensor argument b to tt
848
+ padded_b = pad_weight(b)
849
+ pyt_a = tt2torch(a) # untilizes also
850
+ return print_diff_argmax(pyt_a, padded_b, annotation)
851
+
852
+
853
+ def ttP(x, count=4, offset=0, stride=1):
854
+ if type(x) == torch.Tensor:
855
+ t1 = x.reshape(-1)
856
+ else:
857
+ tt_out = x.cpu()
858
+ torch_out = untilize(tt_out.to_torch())
859
+ t1 = torch_out.reshape(-1)
860
+ print("Tensor vals: (", end="")
861
+ for j in range(offset, offset + count * stride, stride):
862
+ print(t1[j].item(), " ", end="")
863
+ print(")")
864
+
865
+
866
+ ### Conv related helpers ###
867
+ def read_conv_act_into_mm_act_block(
868
+ conv_act,
869
+ act_address_map_index,
870
+ address_map,
871
+ address_map_this_block_size,
872
+ act_block_h,
873
+ act_block_w,
874
+ ):
875
+ mm_act_block_shape = [1, 1, act_block_h * 32, act_block_w * 32]
876
+ mm_act_block_size = act_block_h * act_block_w * 1024
877
+ mm_act_block = torch.zeros(mm_act_block_size, dtype=torch.bfloat16).float()
878
+ for i in range(0, address_map_this_block_size, 4):
879
+ src_address = address_map[act_address_map_index]
880
+ dst_address = address_map[act_address_map_index + 1]
881
+ read_size = address_map[act_address_map_index + 2]
882
+ pad = address_map[act_address_map_index + 3]
883
+ for s in range(read_size):
884
+ assert dst_address + s < mm_act_block_size
885
+ if pad:
886
+ mm_act_block[dst_address + s] = 0
887
+ else:
888
+ assert src_address + s < len(conv_act)
889
+ mm_act_block[dst_address + s] = conv_act[src_address + s]
890
+ act_address_map_index += 4
891
+ return (mm_act_block.reshape(mm_act_block_shape), act_address_map_index)
892
+
893
+
894
+ def read_conv_weight_into_mm_weight_block(
895
+ conv_weight,
896
+ weight_address_map_index,
897
+ weight_address_map,
898
+ weight_address_map_this_block_size,
899
+ weight_block_h,
900
+ weight_block_w,
901
+ ):
902
+ mm_weight_block_shape = [1, 1, weight_block_h * 32, weight_block_w * 32]
903
+ mm_weight_block_size = weight_block_h * weight_block_w * 1024
904
+ mm_weight_block = torch.zeros(mm_weight_block_size, dtype=torch.bfloat16).float()
905
+ for i in range(0, weight_address_map_this_block_size, 4):
906
+ src_address = weight_address_map[weight_address_map_index]
907
+ dst_address = weight_address_map[weight_address_map_index + 1]
908
+ read_size = weight_address_map[weight_address_map_index + 2]
909
+ pad = weight_address_map[weight_address_map_index + 3]
910
+ for s in range(read_size):
911
+ assert dst_address + s < mm_weight_block_size
912
+ if pad:
913
+ mm_weight_block[dst_address + s] = 0
914
+ else:
915
+ assert src_address + s < len(conv_weight)
916
+ mm_weight_block[dst_address + s] = conv_weight[src_address + s]
917
+ weight_address_map_index += 4
918
+ return (mm_weight_block.reshape(mm_weight_block_shape), weight_address_map_index)
919
+
920
+
921
+ def blocked_mm_with_conv_act(
922
+ conv_act,
923
+ mm_weight,
924
+ act_address_map,
925
+ weight_address_map,
926
+ num_blocks_act_h,
927
+ num_blocks_act_w,
928
+ num_blocks_weight_w,
929
+ act_block_h,
930
+ act_block_w,
931
+ weight_block_w,
932
+ ):
933
+ # act refers to conv activation tensor
934
+ # weight refers to conv weight tensor
935
+ mm_output_shape = [
936
+ 1,
937
+ 1,
938
+ num_blocks_act_h * act_block_h * 32,
939
+ num_blocks_weight_w * weight_block_w * 32,
940
+ ]
941
+ ret = torch.zeros(mm_output_shape, dtype=torch.bfloat16).float()
942
+ mm_output_block_shape = [1, 1, act_block_h * 32, weight_block_w * 32]
943
+ act_address_map_index = 0
944
+ weight_address_map_index = 0
945
+ weight_block_h = act_block_w
946
+ num_groups = act_address_map[act_address_map_index]
947
+ assert num_groups == num_blocks_act_h * num_blocks_act_w * num_blocks_weight_w
948
+ weight_num_groups = act_address_map[weight_address_map_index]
949
+ assert weight_num_groups == num_groups
950
+ act_address_map_index += 1
951
+ weight_address_map_index += 1
952
+ for block_act_h in range(num_blocks_act_h):
953
+ # Reset weight (weight) to the starting tile in this column
954
+ for block_weight_w in range(num_blocks_weight_w):
955
+ output_block = torch.zeros(mm_output_block_shape, dtype=torch.bfloat16).float()
956
+ for block_act_w in range(num_blocks_act_w):
957
+ address_map_this_block_size = act_address_map[act_address_map_index]
958
+ act_address_map_index += 1
959
+ weight_address_map_this_block_size = weight_address_map[weight_address_map_index]
960
+ weight_address_map_index += 1
961
+ (mm_act_block, act_address_map_index) = read_conv_act_into_mm_act_block(
962
+ conv_act,
963
+ act_address_map_index,
964
+ act_address_map,
965
+ address_map_this_block_size,
966
+ act_block_h,
967
+ act_block_w,
968
+ )
969
+ (
970
+ mm_weight_block,
971
+ weight_address_map_index,
972
+ ) = read_conv_weight_into_mm_weight_block(
973
+ mm_weight,
974
+ weight_address_map_index,
975
+ weight_address_map,
976
+ weight_address_map_this_block_size,
977
+ weight_block_h,
978
+ weight_block_w,
979
+ )
980
+ # Untilize weight block (this CPU reference does matmul on untilized blocks)
981
+ mm_weight_block = untilize(mm_weight_block)
982
+ for out_h_block in range(act_block_h * 32):
983
+ for out_w_block in range(weight_block_w * 32):
984
+ output_block[0][0][out_h_block][out_w_block] += torch.dot(
985
+ mm_act_block[0, 0, out_h_block, :].reshape(-1),
986
+ mm_weight_block[0, 0, :, out_w_block].reshape(-1),
987
+ )
988
+ start_oh = block_act_h * act_block_h * 32
989
+ start_ow = block_weight_w * weight_block_w * 32
990
+ end_oh = start_oh + (act_block_h * 32)
991
+ end_ow = start_ow + (weight_block_w * 32)
992
+ ret[0, 0, start_oh:end_oh, start_ow:end_ow] = output_block
993
+
994
+ return ret
995
+
996
+
997
+ def is_conv_supported_on_device(conv_params):
998
+ K, C, R, S, U, V, P_H, P_W, dilation, groups = [conv_params[i] for i in range(10)]
999
+
1000
+ if K % 32 != 0 or dilation != 1 or groups != 1:
1001
+ logger.warning("DOES NOT HAVE SUPPORT FOR Conv with following parameters -")
1002
+ logger.warning(
1003
+ "K="
1004
+ + str(K)
1005
+ + " C="
1006
+ + str(C)
1007
+ + " R="
1008
+ + str(R)
1009
+ + " S="
1010
+ + str(S)
1011
+ + " U="
1012
+ + str(U)
1013
+ + " V="
1014
+ + str(V)
1015
+ + " PH="
1016
+ + str(P_H)
1017
+ + " PW="
1018
+ + str(P_W)
1019
+ + " dilation="
1020
+ + str(dilation)
1021
+ + " groups="
1022
+ + str(groups)
1023
+ )
1024
+ return False
1025
+
1026
+ return True
1027
+
1028
+
1029
+ def is_x2_harvested(device):
1030
+ grid = device.compute_with_storage_grid_size()
1031
+ return device.arch() == Arch.WORMHOLE_B0 and (grid.x, grid.y) == (8, 7)
1032
+
1033
+
1034
+ def is_single_chip():
1035
+ return ttnn.GetNumAvailableDevices() == 1
1036
+
1037
+
1038
+ def is_quasar():
1039
+ ARCH_NAME = ttnn.get_arch_name()
1040
+ return "quasar" in ARCH_NAME
1041
+
1042
+
1043
+ def is_blackhole():
1044
+ ARCH_NAME = ttnn.get_arch_name()
1045
+ return "blackhole" in ARCH_NAME
1046
+
1047
+
1048
+ def is_wormhole_b0():
1049
+ ARCH_NAME = ttnn.get_arch_name()
1050
+ return "wormhole_b0" in ARCH_NAME
1051
+
1052
+
1053
+ def is_watcher_enabled():
1054
+ watcher = os.environ.get("TT_METAL_WATCHER")
1055
+ lightweight_asserts = os.environ.get("TT_METAL_LIGHTWEIGHT_KERNEL_ASSERTS")
1056
+ return (watcher is not None and watcher != "") or lightweight_asserts == "1"
1057
+
1058
+
1059
+ def is_llk_assert_enabled():
1060
+ llk_assert = os.environ.get("TT_METAL_LLK_ASSERTS")
1061
+ return llk_assert == "1"
1062
+
1063
+
1064
+ def is_n300():
1065
+ return os.environ.get("MESH_DEVICE", "N150") == "N300"
1066
+
1067
+
1068
+ def is_slow_dispatch():
1069
+ return os.environ.get("TT_METAL_SLOW_DISPATCH_MODE") == "1"
1070
+
1071
+
1072
+ def ti_skip(condition, reason="Invalid test parameters"):
1073
+ return pytest.mark.skipif(condition, reason="Skipping unsupported case: " + reason)
1074
+
1075
+
1076
+ def skip_for_blackhole(reason_str="not a blackhole test"):
1077
+ return ti_skip(is_blackhole(), reason=reason_str)
1078
+
1079
+
1080
+ def skip_for_wormhole_b0(reason_str="not a wormhole test"):
1081
+ return ti_skip(is_wormhole_b0(), reason=reason_str)
1082
+
1083
+
1084
+ def skip_with_watcher(reason_str="Test is not passing with watcher enabled"):
1085
+ return ti_skip(is_watcher_enabled(), reason=reason_str)
1086
+
1087
+
1088
+ def skip_with_llk_assert(reason_str="Test is not passing with LLK asserts enabled"):
1089
+ return ti_skip(is_llk_assert_enabled(), reason=reason_str)
1090
+
1091
+
1092
+ def run_for_blackhole(reason_str="only runs for Blackhole"):
1093
+ return ti_skip(not is_blackhole(), reason=reason_str)
1094
+
1095
+
1096
+ def run_for_wormhole_b0(reason_str="only runs for Wormhole B0"):
1097
+ return ti_skip(not is_wormhole_b0(), reason=reason_str)
1098
+
1099
+
1100
+ def run_for_wormhole_b0_or_blackhole(reason_str="only runs for Wormhole B0 or Blackhole"):
1101
+ return ti_skip(not (is_wormhole_b0() or is_blackhole()), reason=reason_str)
1102
+
1103
+
1104
+ def run_for_n_dev(n, reason_str="Test is not meant for this number of devices"):
1105
+ return ti_skip(ttnn.get_num_devices() != n, reason=reason_str)
1106
+
1107
+
1108
+ def skip_for_n_dev(n, reason_str="Test is not meant for this number of devices"):
1109
+ return ti_skip(ttnn.get_num_devices() == n, reason=reason_str)
1110
+
1111
+
1112
+ def skip_for_n_or_less_dev(n, reason_str="Test is not meant for this number of devices"):
1113
+ return ti_skip(ttnn.get_num_devices() <= n, reason=reason_str)
1114
+
1115
+
1116
+ def skip_for_slow_dispatch(reason_str="not working for slow dispatch"):
1117
+ return ti_skip(is_slow_dispatch(), reason=reason_str)
1118
+
1119
+
1120
+ def ttl_complex_2_torch_complex(tt_tensor):
1121
+ torch_tensor = tt2torch_tensor(tt_tensor)
1122
+
1123
+ # extract real and imag parts of the complex tensor
1124
+ real = torch_tensor[:, :, :, : torch_tensor.shape[-1] // 2].to(torch.bfloat16).to(torch.float)
1125
+ imag = torch_tensor[:, :, :, torch_tensor.shape[-1] // 2 :].to(torch.bfloat16).to(torch.float)
1126
+
1127
+ # create torch complex tensor
1128
+ result = torch.complex(real, imag)
1129
+ return result
1130
+
1131
+
1132
+ def pad_and_fold_conv_filters_for_unity_stride(filter_pyt_nchw_tensor, stride_h, stride_w, align_c=4):
1133
+ assert stride_h == stride_w
1134
+ assert filter_pyt_nchw_tensor.shape[2] == filter_pyt_nchw_tensor.shape[3]
1135
+ assert isinstance(align_c, int) and align_c > 0
1136
+ # Fold activation for unity stride
1137
+ # Pad channel size to align_c. This keeps L1 read addresses aligned; extra channels become
1138
+ # zero-valued weights that contribute nothing to the convolution. align_c=4 is the WH/BH default
1139
+ # (16B alignment for bf16 gives C a multiple of 4 with a tiled conv reader). Quasar's row-major
1140
+ # fold needs align_c=8 (bf16 row-major shard width must be a multiple of 8) so the first conv
1141
+ # folds to groups*8 input channels and consumes the aligned output without per-group padding strip.
1142
+ C = _nearest_y(filter_pyt_nchw_tensor.shape[1], align_c)
1143
+ # Pad filter to nearest stride
1144
+ Padded_filter_height = _nearest_y(filter_pyt_nchw_tensor.shape[2], stride_h)
1145
+ Padded_filter_width = _nearest_y(filter_pyt_nchw_tensor.shape[3], stride_w)
1146
+ filter_pyt_padded = torch.nn.functional.pad(
1147
+ filter_pyt_nchw_tensor,
1148
+ (
1149
+ 0,
1150
+ Padded_filter_width - filter_pyt_nchw_tensor.shape[3],
1151
+ 0,
1152
+ Padded_filter_height - filter_pyt_nchw_tensor.shape[2],
1153
+ 0,
1154
+ C - filter_pyt_nchw_tensor.shape[1],
1155
+ ),
1156
+ )
1157
+ # Fold filter for unity stride.
1158
+ filter_pyt_padded_folded = torch.zeros(
1159
+ [
1160
+ filter_pyt_padded.shape[0],
1161
+ C * stride_h * stride_w,
1162
+ (int)(filter_pyt_padded.shape[2] / stride_h),
1163
+ (int)(filter_pyt_padded.shape[3] / stride_w),
1164
+ ]
1165
+ )
1166
+ for h in range(0, filter_pyt_padded.shape[2], stride_h):
1167
+ for w in range(0, filter_pyt_padded.shape[3], stride_w):
1168
+ folded_h = (int)(h / stride_h)
1169
+ folded_w = (int)(w / stride_w)
1170
+ for i in range(4):
1171
+ start_c = i * C
1172
+ filter_pyt_padded_folded[:, start_c : start_c + C, folded_h, folded_w] = filter_pyt_padded[
1173
+ :, :, h + (int)(i / stride_w), w + (int)(i % stride_w)
1174
+ ]
1175
+ return filter_pyt_padded_folded
1176
+
1177
+
1178
+ # produces a tensor where each element in a page is the page number
1179
+ # this tensor is easy to debug and visualize
1180
+ def get_debug_tensor(num_pages_width, num_pages_height, dtype, page_width=32, page_height=32):
1181
+ torch_tensor = None
1182
+ for row_idx in range(0, int(num_pages_height)):
1183
+ tile_row = None
1184
+ for col_idx in range(0, int(num_pages_width)):
1185
+ tile_idx = col_idx + num_pages_width * row_idx
1186
+ tile = torch.full((1, 1, page_width, page_height), tile_idx + 1, dtype=dtype)
1187
+ if tile_row == None:
1188
+ tile_row = tile
1189
+ else:
1190
+ tile_row = torch.cat((tile_row, tile), 3)
1191
+ if torch_tensor == None:
1192
+ torch_tensor = tile_row
1193
+ else:
1194
+ torch_tensor = torch.cat((torch_tensor, tile_row), 2)
1195
+
1196
+ return torch_tensor
1197
+
1198
+
1199
+ # ── transformers 5.x Cache API compatibility ────────────────────────────────
1200
+ # transformers 5.x removed the legacy Cache API: DynamicCache no longer exposes
1201
+ # from_legacy_cache / to_legacy_cache / key_cache / value_cache (per-layer KV now
1202
+ # lives at cache.layers[i].keys/.values). These helpers work on both 4.x and 5.x.
1203
+ def hf_cache_layer_kv(cache, layer_idx):
1204
+ """Return (key, value) tensors for a layer of a transformers Cache.
1205
+
1206
+ Handles the legacy tuple-of-tuples past_key_values, transformers <5 Cache
1207
+ (key_cache/value_cache), and transformers >=5 Cache (layers[i].keys/.values).
1208
+ """
1209
+ if isinstance(cache, (tuple, list)): # legacy tuple-of-tuples past_key_values
1210
+ return cache[layer_idx][0], cache[layer_idx][1]
1211
+ if hasattr(cache, "key_cache"): # transformers < 5.x Cache
1212
+ return cache.key_cache[layer_idx], cache.value_cache[layer_idx]
1213
+ layer = cache.layers[layer_idx] # transformers >= 5.x Cache
1214
+ return layer.keys, layer.values
1215
+
1216
+
1217
+ def hf_cache_to_legacy(cache):
1218
+ """Export a transformers Cache to the legacy tuple-of-(key, value) format."""
1219
+ if hasattr(cache, "to_legacy_cache"): # transformers < 5.x
1220
+ return cache.to_legacy_cache()
1221
+ return tuple((layer.keys, layer.values) for layer in cache.layers) # transformers >= 5.x
1222
+
1223
+
1224
+ def hf_dynamic_cache_from_legacy(layer_kvs):
1225
+ """Build a transformers DynamicCache from per-layer (key, value) tuples."""
1226
+ from transformers import DynamicCache
1227
+
1228
+ layer_kvs = tuple(layer_kvs)
1229
+ if hasattr(DynamicCache, "from_legacy_cache"): # transformers < 5.x
1230
+ return DynamicCache.from_legacy_cache(layer_kvs)
1231
+ return DynamicCache(layer_kvs) # transformers >= 5.x
1232
+
1233
+
1234
+ def hf_cache_num_layers(cache):
1235
+ """Number of populated layers in a transformers Cache (version-tolerant)."""
1236
+ return len(cache.key_cache) if hasattr(cache, "key_cache") else len(cache.layers)
1237
+
1238
+
1239
+ def hf_empty_encoder_decoder_cache():
1240
+ """Create an empty transformers EncoderDecoderCache (version-tolerant)."""
1241
+ from transformers import DynamicCache, EncoderDecoderCache
1242
+
1243
+ if hasattr(EncoderDecoderCache, "from_legacy_cache"): # transformers < 5.x
1244
+ return EncoderDecoderCache.from_legacy_cache(None)
1245
+ return EncoderDecoderCache(DynamicCache(), DynamicCache()) # transformers >= 5.x
1246
+
1247
+
1248
+ def copy_to_buffer(src: "ttnn.Tensor", dst: "ttnn.Tensor", target_dtype) -> None:
1249
+ """Convert ``src`` to ``dst``'s layout/dtype/shape/memcfg and write it into
1250
+ ``dst``. ``dst``'s device buffer is preserved (no reallocation) so any
1251
+ captured trace and the DRAM prefetcher's recorded buffer addresses remain
1252
+ valid. The final ``ttnn.to_memory_config`` with ``output_tensor=dst`` both
1253
+ reshards to ``dst``'s memory config and copies into ``dst``'s buffer.
1254
+ """
1255
+ converted = src
1256
+
1257
+ if converted.layout != dst.layout:
1258
+ converted = ttnn.to_layout(converted, layout=dst.layout)
1259
+
1260
+ if converted.dtype != target_dtype:
1261
+ converted = ttnn.typecast(converted, dtype=target_dtype)
1262
+
1263
+ if tuple(converted.shape) != tuple(dst.shape):
1264
+ converted = ttnn.reshape(converted, list(dst.shape))
1265
+
1266
+ ttnn.to_memory_config(converted, dst.memory_config(), output_tensor=dst)
code/models/common/utils.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import torch
5
+ import torch.nn.functional as F
6
+ from torch import Tensor
7
+
8
+ # Backward compatibility: filter_none moved to models.common.sampling._utils
9
+ from models.common.sampling._utils import filter_none # noqa: F401
10
+
11
+ # Backward compatibility: LogProbsCalculator moved to models.common.sampling.tt_log_probs
12
+ from models.common.sampling.tt_log_probs import LogProbsCalculator # noqa: F401
13
+
14
+
15
+ def top_k_top_p_filtering(
16
+ logits: Tensor,
17
+ top_k: int = 0,
18
+ top_p: float = 1.0,
19
+ filter_value: float = -float("Inf"),
20
+ min_tokens_to_keep: int = 1,
21
+ ) -> Tensor:
22
+ """Filter a distribution of logits using top-k and/or nucleus (top-p) filtering
23
+ Args:
24
+ logits: logits distribution shape (batch size, vocabulary size)
25
+ if top_k > 0: keep only top k tokens with highest probability (top-k filtering).
26
+ if top_p < 1.0: keep the top tokens with cumulative probability >= top_p (nucleus filtering).
27
+ Nucleus filtering is described in Holtzman et al. (http://arxiv.org/abs/1904.09751)
28
+ Make sure we keep at least min_tokens_to_keep per batch example in the output
29
+ From: https://gist.github.com/thomwolf/1a5a29f6962089e871b94cbd09daf317
30
+ """
31
+ if top_k > 0:
32
+ top_k = min(max(top_k, min_tokens_to_keep), logits.size(-1)) # Safety check
33
+ # Remove all tokens with a probability less than the last token of the top-k
34
+ indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None]
35
+ logits[indices_to_remove] = filter_value
36
+
37
+ if top_p < 1.0:
38
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True)
39
+ cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
40
+
41
+ # Remove tokens with cumulative probability above the threshold (token with 0 are kept)
42
+ sorted_indices_to_remove = cumulative_probs > top_p
43
+ if min_tokens_to_keep > 1:
44
+ # Keep at least min_tokens_to_keep (set to min_tokens_to_keep-1 because we add the first one below)
45
+ sorted_indices_to_remove[..., :min_tokens_to_keep] = 0
46
+ # Shift the indices to the right to keep also the first token above the threshold
47
+ sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
48
+ sorted_indices_to_remove[..., 0] = 0
49
+
50
+ # scatter sorted tensors to original indexing
51
+ indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove)
52
+ logits[indices_to_remove] = filter_value
53
+ return logits
code/models/common/validation_tools.py ADDED
@@ -0,0 +1,719 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """
5
+ TTNN Validation Framework
6
+
7
+ A decorator-based validation system for comparing TTNN implementations against
8
+ reference implementations (in PyTorch). Supports automatic input/output
9
+ mapping, metric computation, and result collection.
10
+
11
+ Key Features:
12
+ - Automatic comparison of TTNN vs reference implementations
13
+ - TTNN-native metric computation (stays on device until final scalar)
14
+ - Flexible input/output mapping
15
+ - Built-in metrics: max_abs_error, mean_abs_error, cosine_similarity
16
+ - Performance tracking
17
+ - Result registry for batch reporting
18
+ """
19
+
20
+ import time
21
+ from dataclasses import dataclass, field
22
+ from enum import Enum
23
+ from functools import wraps
24
+ from typing import Any, Callable, Dict, List, Optional
25
+
26
+ import torch
27
+
28
+ import ttnn
29
+
30
+ from .auto_compose import to_torch_auto_compose
31
+ from .distribute_as import from_torch_dist_as
32
+ from .metrics import DEFAULT_METRICS
33
+
34
+ # ============================================================================
35
+ # Public API
36
+ # ============================================================================
37
+
38
+ # Module exports are defined at the package level in __init__.py
39
+
40
+
41
+ def get_validation_registry() -> "ValidationRegistry":
42
+ """Get the global validation registry"""
43
+ return _validation_registry
44
+
45
+
46
+ def enable_validation(enabled: bool = True):
47
+ """Enable or disable validation globally"""
48
+ _validation_registry.enabled = enabled
49
+
50
+
51
+ def clear_validation_results():
52
+ """Clear all validation results"""
53
+ _validation_registry.results.clear()
54
+
55
+
56
+ def compare_to_ttnn(
57
+ reference_fn: Callable,
58
+ *,
59
+ input_to_ttnn: Optional[Callable] = None,
60
+ output_to_ttnn: Optional[Callable] = None,
61
+ metric_tolerances: Optional[Dict[Any, Any]] = None,
62
+ enabled: bool = True,
63
+ raise_exceptions: bool = False,
64
+ return_reference_output: bool = False,
65
+ ):
66
+ """
67
+ Convenience wrapper for TTNN-on-device comparison. Provides useful visual cue to users that the reference function is a TTNN-native function.
68
+
69
+ Args:
70
+ reference_fn: Reference function to compare against
71
+ input_to_ttnn: Maps decorated function inputs to reference function inputs
72
+ output_to_ttnn: Maps decorated function outputs to reference function outputs
73
+ metric_tolerances: Dictionary specifying tolerances and optionally custom metrics.
74
+ enabled: Whether validation is enabled (can disable globally via registry)
75
+ raise_exceptions: When True, re-raise any exceptions encountered during
76
+ reference execution, output mapping, or metric computation instead
77
+ of logging them into validation results.
78
+
79
+ Examples:
80
+ @compare_to_ttnn(
81
+ reference_fn=lambda self, x: ttnn.matmul(x, self.weight),
82
+ input_to_ttnn=lambda self, x: (self, x),
83
+ )
84
+ def __call__(self, x):
85
+ return torch.matmul(x, self.torch_weight)
86
+ # alternatively, the decorated function can return a TTNN tensor: return ttnn.from_torch(x) @ self.weight
87
+
88
+ NOTES:
89
+ - The reference function is expected to accepts TTNN tensors and returns a TTNN tensor
90
+ - The decorated function inputs/outputs TTNN tensors, Torch tensors, or mixed TTNN and Torch tensors
91
+ - When decorated function returns torch tensors:
92
+ - the reference function's inputs will be constructed through either input_to_ttnn or from_torch(decorated function inputs, device=ttnn.GetDefaultDevice())
93
+ - the metric on output tensor will be computed on the host
94
+ - Experimental support for on-device metric computation is provided and used when both the decorated function and the reference function return TTNN tensors
95
+ """
96
+
97
+ # Default converters: recursively convert any TTNN tensors to torch, auto-compose shards.
98
+ # Non-tensor objects are passed through unchanged.
99
+
100
+ def _to_ttnn_auto(x: Any) -> Any:
101
+ if torch.is_tensor(x):
102
+ # Use auto-compose; relies on tensor.device() or a globally-set default device
103
+ assert (
104
+ ttnn.GetDefaultDevice() is not None
105
+ ), "Default device is not set. It is required by compare_to_ttnn. Please set it via ttnn.SetDefaultDevice(...)."
106
+ return ttnn.from_torch(x, device=ttnn.GetDefaultDevice())
107
+ return x
108
+
109
+ def _default_input_map(*args, **kwargs):
110
+ ref_args = _map_structure(args, _to_ttnn_auto)
111
+ ref_kwargs = _map_structure(kwargs, _to_ttnn_auto)
112
+ return ref_args, ref_kwargs
113
+
114
+ map_fn_to_match_sig = lambda tt_tensor, filler: to_torch_auto_compose(tt_tensor)
115
+
116
+ return __validate_against(
117
+ reference_fn=reference_fn,
118
+ input_map=input_to_ttnn or _default_input_map,
119
+ output_map=output_to_ttnn,
120
+ metric_tolerances=metric_tolerances,
121
+ enabled=enabled,
122
+ raise_exceptions=raise_exceptions,
123
+ reference_output_map_fn=map_fn_to_match_sig if return_reference_output else None,
124
+ )
125
+
126
+
127
+ def compare_to_torch(
128
+ reference_fn: Callable,
129
+ *,
130
+ input_to_torch: Optional[Callable] = None,
131
+ output_to_torch: Optional[Callable] = None,
132
+ metric_tolerances: Optional[Dict[Any, Any]] = None,
133
+ enabled: bool = True,
134
+ raise_exceptions: bool = False,
135
+ return_reference_output: Optional[Callable[..., bool] | bool] = False,
136
+ ):
137
+ """
138
+ Convenience wrapper for host/CPU comparison using torch.
139
+
140
+ # Args:
141
+ # reference_fn: Reference function to compare against
142
+ # input_to_torch: Maps decorated function inputs to reference function inputs
143
+ # output_to_torch: Maps decorated function outputs to reference function outputs
144
+ # metric_tolerances: Dictionary specifying tolerances and optionally custom metrics.
145
+ # enabled: Whether validation is enabled (can disable globally via registry)
146
+ # raise_exceptions: When True, re-raise any exceptions encountered during
147
+ # reference execution, output mapping, or metric computation instead
148
+ # of logging them into validation results.
149
+ #
150
+ # Notes:
151
+ # - compare_to_torch is used when the reference function is a PyTorch function
152
+ # - the reference function takes as inputs to_torch_auto_compose(decorated function inputs) and compares the outputs with to_torch_auto_compose(decorated function outputs)
153
+ # - the decorated function inputs/outputs TTNN tensors, Torch tensors, or mixed TTNN and Torch tensors
154
+ """
155
+
156
+ # Default converters: recursively convert any TTNN tensors to torch, auto-compose shards.
157
+ # Non-tensor objects are passed through unchanged.
158
+
159
+ def _to_torch_auto(x: Any) -> Any:
160
+ if isinstance(x, ttnn.Tensor):
161
+ # Use auto-compose; relies on tensor.device() or a globally-set default device
162
+ return to_torch_auto_compose(x)
163
+ return x
164
+
165
+ def _default_input_map(*args, **kwargs):
166
+ ref_args = _map_structure(args, _to_torch_auto)
167
+ ref_kwargs = _map_structure(kwargs, _to_torch_auto)
168
+ return ref_args, ref_kwargs
169
+
170
+ def _default_output_map(output):
171
+ return _map_structure(output, _to_torch_auto)
172
+
173
+ return __validate_against(
174
+ reference_fn=reference_fn,
175
+ input_map=input_to_torch or _default_input_map,
176
+ output_map=output_to_torch or _default_output_map,
177
+ metric_tolerances=metric_tolerances,
178
+ enabled=enabled,
179
+ raise_exceptions=raise_exceptions,
180
+ reference_output_map_fn=from_torch_dist_as if return_reference_output else None,
181
+ )
182
+
183
+
184
+ # ============================================================================
185
+ # Data Structures
186
+ # ============================================================================
187
+
188
+
189
+ @dataclass
190
+ class MetricResult:
191
+ """Per-metric validation outcome"""
192
+
193
+ value: float = float("inf")
194
+ passed: bool = False
195
+ error: str = ""
196
+
197
+
198
+ @dataclass
199
+ class ValidationResult:
200
+ """Results from a single validation run"""
201
+
202
+ function_name: str
203
+ passed: bool
204
+ # Map of metric name to its result (value/pass/fail/error)
205
+ metrics: Dict[Any, MetricResult] = field(default_factory=dict)
206
+ execution_time_impl: float = 0.0
207
+ execution_time_ref: float = 0.0
208
+ timestamp: float = field(default_factory=time.time)
209
+ logs: List[str] = field(default_factory=list)
210
+
211
+
212
+ class ValidationRegistry:
213
+ """Global registry for validation results"""
214
+
215
+ def __init__(self):
216
+ self.results: List[ValidationResult] = []
217
+ self.enabled = True
218
+
219
+ def add_result(self, result: ValidationResult):
220
+ self.results.append(result)
221
+
222
+ def get_summary(self) -> Dict[str, Any]:
223
+ """Get summary statistics of all validations"""
224
+ if not self.results:
225
+ return {"total": 0, "passed": 0, "failed": 0}
226
+
227
+ passed = sum(1 for r in self.results if r.passed)
228
+ failed = len(self.results) - passed
229
+
230
+ return {
231
+ "total": len(self.results),
232
+ "passed": passed,
233
+ "failed": failed,
234
+ "pass_rate": passed / len(self.results) if self.results else 0.0,
235
+ "avg_speedup": (
236
+ sum(r.execution_time_ref / r.execution_time_impl for r in self.results if r.execution_time_impl > 0)
237
+ / len(self.results)
238
+ if self.results
239
+ else 0.0
240
+ ),
241
+ }
242
+
243
+ def print_report(self, verbose: bool = False):
244
+ """Print detailed validation report"""
245
+ summary = self.get_summary()
246
+ print("\n" + "=" * 80)
247
+ print("VALIDATION REPORT")
248
+ print("=" * 80)
249
+ print()
250
+
251
+ for result in self.results:
252
+ status = "✓ PASS" if result.passed else "✗ FAIL"
253
+ print(f"{status} - {result.function_name}")
254
+ print(
255
+ f" Execution time: impl={result.execution_time_impl*1000:.2f}ms, ref={result.execution_time_ref*1000:.2f}ms"
256
+ )
257
+
258
+ if result.metrics:
259
+ print(f" Metrics:")
260
+ for metric_name, mres in result.metrics.items():
261
+ # Use enum value for readability if metric is an Enum
262
+ name_str = metric_name.value if hasattr(metric_name, "value") else str(metric_name)
263
+ if mres.value is not None:
264
+ try:
265
+ val_str = f"{mres.value:.6f}"
266
+ except Exception:
267
+ val_str = str(mres.value)
268
+ else:
269
+ val_str = "-"
270
+ status = "PASS" if mres.passed else "FAIL"
271
+ print(f" {name_str}: {val_str} — {status}")
272
+ if mres.error:
273
+ print(f" error: {mres.error}")
274
+
275
+ # Print any collected logs for this validation
276
+ if result.logs and verbose:
277
+ print(" Logs:")
278
+ for entry in result.logs:
279
+ try:
280
+ msg = str(entry)
281
+ except Exception:
282
+ msg = "<unprintable log entry>"
283
+ print(f" {msg}")
284
+
285
+ # All errors are reported via per-metric entries
286
+ print()
287
+
288
+ print("-" * 36 + "Summary:" + "-" * 36)
289
+ print(f"Total validations: {summary['total']}")
290
+ print(f"Passed: {summary['passed']} ({summary['pass_rate']*100:.1f}%)")
291
+ print(f"Failed: {summary['failed']}")
292
+ print(f"Average speedup: {summary['avg_speedup']:.2f}x")
293
+ print()
294
+ print("=" * 80 + "\n")
295
+
296
+
297
+ # Global validation registry
298
+ _validation_registry = ValidationRegistry()
299
+
300
+
301
+ # ============================================================================
302
+ # Validation Decorator
303
+ # ============================================================================
304
+
305
+
306
+ class Metric(str, Enum):
307
+ """Enumeration of supported metric names, values match current string keys."""
308
+
309
+ MAX_ABS_ERROR = "max_abs_error"
310
+ MEAN_ABS_ERROR = "mean_abs_error"
311
+ PCC = "pcc"
312
+
313
+
314
+ @dataclass
315
+ class MetricSpec:
316
+ """Metric specification: name, tolerance, direction, and compute function."""
317
+
318
+ tolerance: float
319
+ higher_is_better: bool
320
+ compute_fn: Callable[[Any, Any], float]
321
+ name: str = field(default="")
322
+
323
+
324
+ # Registry of built-in metrics with defaults. Tolerances here are sensible
325
+ # defaults; callers can override per-validation via `tolerances`.
326
+ METRIC_SPECS: Dict[Metric, MetricSpec] = {
327
+ Metric.MAX_ABS_ERROR: MetricSpec(
328
+ name=Metric.MAX_ABS_ERROR.value,
329
+ tolerance=0.0,
330
+ higher_is_better=False,
331
+ compute_fn=DEFAULT_METRICS[Metric.MAX_ABS_ERROR.value],
332
+ ),
333
+ Metric.MEAN_ABS_ERROR: MetricSpec(
334
+ name=Metric.MEAN_ABS_ERROR.value,
335
+ tolerance=0.0,
336
+ higher_is_better=False,
337
+ compute_fn=DEFAULT_METRICS[Metric.MEAN_ABS_ERROR.value],
338
+ ),
339
+ Metric.PCC: MetricSpec(
340
+ name=Metric.PCC.value,
341
+ tolerance=0.0,
342
+ higher_is_better=True,
343
+ compute_fn=DEFAULT_METRICS[Metric.PCC.value],
344
+ ),
345
+ }
346
+
347
+ # Convenience groupings for quick checks
348
+ HIGHER_IS_BETTER_METRICS = {m.value for m, spec in METRIC_SPECS.items() if spec.higher_is_better}
349
+ LOWER_IS_BETTER_METRICS = {m.value for m, spec in METRIC_SPECS.items() if not spec.higher_is_better}
350
+
351
+
352
+ # Helper: prefer Metric enum as dict key when possible
353
+ def _metric_key(key: Any) -> Any:
354
+ try:
355
+ return Metric(key)
356
+ except Exception:
357
+ return key
358
+
359
+
360
+ # Helper: Build active metrics map (name -> compute fn). Accept Metric enum keys for tolerances.
361
+ def _normalize_key(k: Any) -> str:
362
+ try:
363
+ # Enum or similar objects with .value as canonical string
364
+ return k.value if hasattr(k, "value") else str(k)
365
+ except Exception:
366
+ return str(k)
367
+
368
+
369
+ # Helper: Prepare metrics, tolerances, and directionality
370
+ def _prepare_metric_config(metric_tolerances_input):
371
+ metrics_map = {name: fn for name, fn in DEFAULT_METRICS.items()}
372
+ hib = set(HIGHER_IS_BETTER_METRICS)
373
+ logs_local: List[str] = []
374
+ tol_map: Dict[str, float] = {}
375
+
376
+ if not isinstance(metric_tolerances_input, dict):
377
+ logs_local.append(f"metric_tolerances_input must be a dict, got {type(metric_tolerances_input)}")
378
+ metric_tolerances_input = dict()
379
+
380
+ if not metric_tolerances_input:
381
+ logs_local.append("no metric tolerances provided")
382
+ metric_tolerances_input = dict()
383
+
384
+ for raw_key, spec in metric_tolerances_input.items():
385
+ name = _normalize_key(raw_key)
386
+ if isinstance(spec, MetricSpec):
387
+ tol_map[name] = float(spec.tolerance)
388
+ metrics_map[name] = spec.compute_fn
389
+ spec.name = name if spec.name == "" else spec.name
390
+ if spec.higher_is_better:
391
+ hib.add(name)
392
+ else:
393
+ hib.discard(name)
394
+ continue
395
+ try:
396
+ tol_map[name] = float(spec)
397
+ except Exception:
398
+ logs_local.append(f"unrecognized tolerance: {raw_key}: {spec}")
399
+
400
+ return metrics_map, hib, tol_map, logs_local
401
+
402
+
403
+ # todo)) also allow raise an exception from the a failed metric!
404
+
405
+ # todo)) add support for multiple outputs from the reference function and the decorated function!
406
+ # e.g., return logits, past_key_values, etc.
407
+
408
+ # todo)) make sure the dtypes are taken care of in the validate_against decorator!
409
+ # e.g., if the decorated function is of dtype bfp4, what is the dtype of the to_torch_auto_compose output?
410
+
411
+ # todo)) add file line number to the validation results!
412
+
413
+ # todo)) add function to export the validation results to a csv file!
414
+
415
+ # todo)) enhance report to use file line number as index to summarize the validation results
416
+ # e.g., ✗ FAIL - __main__.Attention.__call__ (line 100) -> 100 failed validations
417
+
418
+ # todo)) remove compile time from speed up calculation -- e.g., 9118.15ms should be removed in the example below:
419
+ # ================================================================================
420
+ # VALIDATION REPORT
421
+ # ================================================================================
422
+ # Total validations: 1400
423
+ # Passed: 1400 (100.0%)
424
+ # Failed: 0
425
+ # Average speedup: 0.97x
426
+
427
+ # ✓ PASS - __main__.TransformerBlock.__call__
428
+ # Execution time: impl=9118.15ms, ref=14.84ms
429
+ # Metrics:
430
+ # pcc: 0.999743 — PASS
431
+
432
+ # ✓ PASS - __main__.TransformerBlock.__call__
433
+ # Execution time: impl=3.05ms, ref=12.73ms
434
+ # Metrics:
435
+ # pcc: 0.999913 — PASS
436
+
437
+ # ✓ PASS - __main__.TransformerBlock.__call__
438
+ # Execution time: impl=3.31ms, ref=12.48ms
439
+ # Metrics:
440
+ # pcc: 0.999962 — PASS
441
+
442
+ # ✓ PASS - __main__.TransformerBlock.__call__
443
+ # Execution time: impl=3.11ms, ref=12.89ms
444
+ # Metrics:
445
+ # pcc: 1.000000 — PASS
446
+
447
+ # ✓ PASS - __main__.TransformerBlock.__call__
448
+ # Execution time: impl=3.16ms, ref=12.97ms
449
+ # Metrics:
450
+ # pcc: 0.999998 — PASS
451
+
452
+
453
+ # todo)) stretch goals:
454
+ # - generate unit test automatically from the failed validations
455
+ def __validate_against(
456
+ reference_fn: Callable,
457
+ *,
458
+ input_map: Optional[Callable] = None,
459
+ output_map: Optional[Callable] = None,
460
+ metric_tolerances: Optional[Dict[Any, Any]] = None,
461
+ enabled: bool = True,
462
+ raise_exceptions: bool = False,
463
+ reference_output_map_fn: Optional[Callable] = None,
464
+ ):
465
+ """
466
+ Decorator to validate a function against a reference implementation.
467
+
468
+ Args:
469
+ reference_fn: Reference function to compare against
470
+ input_map: Maps decorated function inputs to reference function inputs
471
+ Signature: (args, kwargs) -> (ref_args, ref_kwargs)
472
+ If None, inputs are passed as-is
473
+ output_map: Converts impl output to match ref output's type
474
+ Signature: (output) -> comparable_output
475
+ Applied ONLY to impl_output to convert it to ref_output's type
476
+ Common use: lambda x: ttnn.to_torch(x).squeeze() to convert ttnn → torch
477
+ If None, outputs are used as-is (both must already be same type)
478
+ metric_tolerances: Dictionary specifying tolerances and optionally custom metrics.
479
+ Accepts the following per metric key (str or Metric):
480
+ - float: tolerance only (uses built-in compute + direction)
481
+ - MetricSpec instance
482
+ Validation fails if any metric exceeds its tolerance
483
+ enabled: Whether validation is enabled (can disable globally via registry)
484
+ raise_exceptions: When True, re-raise any exceptions encountered during
485
+ reference execution, output mapping, or metric computation instead
486
+ of logging them into validation results.
487
+
488
+ Examples:
489
+ # Pattern 1: TTNN-native metrics (recommended, 100-1000× faster!)
490
+ # Both impl and ref return ttnn.Tensor, no output_map needed
491
+ def _reference_impl(self, x):
492
+ x_torch = ttnn.to_torch(x).squeeze(0)
493
+ result_torch = torch.matmul(x_torch, self.weight_torch)
494
+ # Convert back to TTNN for on-device metrics!
495
+ return ttnn.from_torch(result_torch.unsqueeze(0), device=self.device, ...)
496
+
497
+ @validate_against(
498
+ reference_fn=lambda self, x: self._reference_impl(x),
499
+ tolerances={'max_abs_error': 1e-3}
500
+ )
501
+ def __call__(self, x):
502
+ return ttnn.matmul(x, self.weight)
503
+
504
+ # Pattern 2: PyTorch metrics (when reference returns torch.Tensor)
505
+ # Use output_map to convert impl output (ttnn.Tensor) to match ref (torch.Tensor)
506
+ @validate_against(
507
+ reference_fn=torch.nn.functional.rms_norm,
508
+ input_map=lambda args, kwargs: (
509
+ (ttnn.to_torch(args[1]).squeeze(),),
510
+ {'eps': args[0].eps}
511
+ ),
512
+ output_map=lambda x: ttnn.to_torch(x).squeeze(), # Convert impl: ttnn → torch
513
+ tolerances={'max_abs_error': 1e-3}
514
+ )
515
+ def __call__(self, x):
516
+ return ttnn.rms_norm(x, self.weight, self.eps) # Returns ttnn.Tensor
517
+ """
518
+
519
+ if metric_tolerances is None:
520
+ metric_tolerances = {
521
+ Metric.MAX_ABS_ERROR: 1e-2,
522
+ Metric.PCC: 0.99,
523
+ }
524
+
525
+ metrics_to_use, higher_is_better_effective, tolerances_map, pre_logs = _prepare_metric_config(metric_tolerances)
526
+
527
+ def decorator(func):
528
+ @wraps(func)
529
+ def wrapper(*args, **kwargs):
530
+ # Check if validation is enabled
531
+ if not enabled or not _validation_registry.enabled:
532
+ return func(*args, **kwargs)
533
+
534
+ # Execute implementation
535
+ start_time = time.perf_counter()
536
+ impl_output = func(*args, **kwargs)
537
+ impl_time = time.perf_counter() - start_time
538
+ logs: List[str] = pre_logs.copy()
539
+
540
+ # Map inputs for reference function: prefer input_map, else pass-through
541
+ if input_map:
542
+ _nm = getattr(input_map, "__name__", None) or type(input_map).__name__
543
+ logs.append(f"input_map={_nm}")
544
+ try:
545
+ mapped = input_map(*args, **kwargs)
546
+ except Exception as e:
547
+ # If input mapping fails, log error, record result, and return impl output
548
+ logs.append(f"input_mapping_error={str(e)}")
549
+ result = ValidationResult(
550
+ function_name=f"{func.__module__}.{func.__qualname__}",
551
+ passed=False,
552
+ metrics={
553
+ "input_mapping": MetricResult(
554
+ value=None, passed=False, error=f"Input mapping failed: {str(e)}"
555
+ )
556
+ },
557
+ execution_time_impl=impl_time,
558
+ execution_time_ref=0.0,
559
+ logs=logs,
560
+ )
561
+ _validation_registry.add_result(result)
562
+ # Re-raise exception if raise_exceptions is True
563
+ if raise_exceptions:
564
+ raise
565
+ return impl_output
566
+ # Normalize mapper output:
567
+ # - If (ref_args, ref_kwargs) with kwargs as dict, use directly
568
+ # - Otherwise, treat return as positional args and use empty kwargs
569
+ if isinstance(mapped, tuple) and len(mapped) == 2 and isinstance(mapped[1], dict):
570
+ ref_args, ref_kwargs = mapped
571
+ else:
572
+ ref_args = mapped if isinstance(mapped, (list, tuple)) else (mapped,)
573
+ ref_kwargs = {}
574
+ else:
575
+ logs.append("input_map=pass-through")
576
+ ref_args, ref_kwargs = args, kwargs
577
+
578
+ # Execute reference
579
+ try:
580
+ start_time = time.perf_counter()
581
+ ref_output = reference_fn(*ref_args, **ref_kwargs)
582
+ ref_time = time.perf_counter() - start_time
583
+ except Exception as e:
584
+ # If reference fails, just return impl output and log error via metrics
585
+ logs.append(f"reference_execution_error={str(e)}")
586
+ # Record elapsed time until failure
587
+ ref_time = time.perf_counter() - start_time
588
+ result = ValidationResult(
589
+ function_name=f"{func.__module__}.{func.__qualname__}",
590
+ passed=False,
591
+ metrics={
592
+ "reference_execution": MetricResult(
593
+ value=None, passed=False, error=f"Reference execution failed: {str(e)}"
594
+ )
595
+ },
596
+ execution_time_impl=impl_time,
597
+ execution_time_ref=ref_time,
598
+ logs=logs,
599
+ )
600
+ _validation_registry.add_result(result)
601
+ # Re-raise exception if raise_exceptions is True
602
+ if raise_exceptions:
603
+ raise
604
+ return impl_output
605
+
606
+ # Map outputs for comparison
607
+ # Note: output_map only applies to impl_output to convert it to match ref_output's type
608
+ try:
609
+ _nm = getattr(output_map, "__name__", None) or type(output_map).__name__
610
+ logs.append(f"output_map={_nm}")
611
+ impl_comparable = output_map(impl_output) if output_map else impl_output
612
+ ref_comparable = ref_output # Reference output is always used as-is
613
+ except Exception as e:
614
+ logs.append(f"output_mapping_error={str(e)}")
615
+ result = ValidationResult(
616
+ function_name=f"{func.__module__}.{func.__qualname__}",
617
+ passed=False,
618
+ metrics={
619
+ "output_mapping": MetricResult(
620
+ value=None, passed=False, error=f"Output mapping failed: {str(e)}"
621
+ )
622
+ },
623
+ execution_time_impl=impl_time,
624
+ execution_time_ref=ref_time,
625
+ logs=logs,
626
+ )
627
+ _validation_registry.add_result(result)
628
+ # Re-raise exception if raise_exceptions is True
629
+ if raise_exceptions:
630
+ raise
631
+ return impl_output
632
+
633
+ # Compute metrics
634
+ computed_metrics: Dict[Any, MetricResult] = {}
635
+ passed = True
636
+
637
+ for metric_name, threshold in tolerances_map.items():
638
+ try:
639
+ metric_fn = metrics_to_use.get(metric_name)
640
+
641
+ # Store results keyed by enum when available
642
+ metric_key = _metric_key(metric_name)
643
+ # If metric function isn't known, record an error
644
+ if metric_fn is None:
645
+ computed_metrics[metric_key] = MetricResult(
646
+ value=None, passed=False, error=f"Unknown metric: {metric_name}"
647
+ )
648
+ passed = False
649
+ continue
650
+
651
+ value = metric_fn(impl_comparable, ref_comparable)
652
+
653
+ # Determine direction using registry when available
654
+ if metric_name in higher_is_better_effective:
655
+ ok = value >= threshold
656
+ err = None
657
+ if not ok:
658
+ passed = False
659
+ err = f"{metric_name}={value:.6e} below threshold {threshold:.6e}"
660
+ computed_metrics[metric_key] = MetricResult(value=value, passed=ok, error=err)
661
+ else:
662
+ ok = value <= threshold
663
+ err = None
664
+ if not ok:
665
+ passed = False
666
+ err = f"{metric_name}={value:.6e} exceeds tolerance {threshold:.6e}"
667
+ computed_metrics[metric_key] = MetricResult(value=value, passed=ok, error=err)
668
+ except Exception as e:
669
+ msg = f"Metric {metric_name} failed: {str(e)}"
670
+ computed_metrics[metric_key] = MetricResult(value=None, passed=False, error=msg)
671
+ passed = False
672
+ if raise_exceptions:
673
+ raise
674
+
675
+ # Optionally return the (aligned) reference output instead of impl output
676
+ backup_impl_output = impl_output
677
+ try:
678
+ if reference_output_map_fn:
679
+ impl_output = reference_output_map_fn(ref_output, impl_output)
680
+ logs.append(f"reference_output_mapping_fn={reference_output_map_fn.__name__}")
681
+ except Exception as e:
682
+ # If alignment fails, fall back to impl output
683
+ impl_output = backup_impl_output
684
+ # Re-raise exception if raise_exceptions is True after logging the error
685
+ logs.append(f"reference_output_mapping_error={str(e)}")
686
+ if raise_exceptions:
687
+ raise
688
+
689
+ # Record results
690
+ pass_count = sum(1 for v in computed_metrics.values() if v.passed)
691
+ fail_count = sum(1 for v in computed_metrics.values() if not v.passed)
692
+ logs.append(f"metrics={pass_count}_pass,{fail_count}_fail")
693
+ result = ValidationResult(
694
+ function_name=f"{func.__module__}.{func.__qualname__}",
695
+ passed=passed,
696
+ metrics=computed_metrics,
697
+ execution_time_impl=impl_time,
698
+ execution_time_ref=ref_time,
699
+ logs=logs,
700
+ )
701
+ _validation_registry.add_result(result)
702
+
703
+ return impl_output
704
+
705
+ return wrapper
706
+
707
+ return decorator
708
+
709
+
710
+ def _map_structure(obj: Any, fn: Callable[[Any], Any]) -> Any:
711
+ """
712
+ Map a structure of objects to a new structure using a function.
713
+ """
714
+ if isinstance(obj, (list, tuple)):
715
+ mapped = [_map_structure(x, fn) for x in obj]
716
+ return type(obj)(mapped)
717
+ if isinstance(obj, dict):
718
+ return {k: _map_structure(v, fn) for k, v in obj.items()}
719
+ return fn(obj)
code/models/common/weight_cache.py ADDED
@@ -0,0 +1,426 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """
6
+ Shared, ModelArgs-agnostic warm ttnn weight-cache helpers (generalizes PR #50550 / #48531
7
+ to forked model loaders — issue #45400 follow-up).
8
+
9
+ On a warm cache, a model can be built from its on-disk ``.tensorbin`` files without the
10
+ expensive host-side HF ``from_pretrained`` load (the load that OOMs/hangs during prefill,
11
+ #48509): ``ttnn.as_tensor(torch_weight, cache_file_name=...)`` loads the cached tensor and
12
+ ignores ``torch_weight`` on a cache hit (see ttnn/operations/core.py). So most weights only
13
+ need a dataless placeholder (``torch.empty`` of the right shape/dtype) to satisfy the modules'
14
+ host-side reshape ops before ``as_tensor``.
15
+
16
+ Some forks (e.g. gemma4) additionally consume a *small* set of weights on the host — token
17
+ embeddings used via ``F.embedding``, per-layer scalars read via ``.item()``, etc. Those must be
18
+ real. ``mark_weight_cache_complete`` persists exactly those tensors to a sidecar at cold-build
19
+ time (write-access run), and ``build_cached_state_dict`` serves them real on later warm runs
20
+ while placeholdering the rest — a HYBRID state_dict. The host subset is a tiny fraction of the
21
+ weight bytes, so the full from_pretrained (and its OOM) is still avoided.
22
+ """
23
+
24
+ import collections.abc
25
+ import hashlib
26
+ import json
27
+ import os
28
+ from pathlib import Path
29
+
30
+ import torch
31
+ from loguru import logger
32
+
33
+ WEIGHT_CACHE_MARKER = ".weights_complete"
34
+ HOST_WEIGHTS_SIDECAR = ".host_weights.pt"
35
+ # Bump when the set/naming/layout of cached weights, or this marker schema, changes such that an
36
+ # existing cache would not satisfy a new build. A marker written by an older format is rejected,
37
+ # so the run cold-loads and regenerates rather than building from an incompatible cache.
38
+ # v2: model/n_layers/mesh_shape validation + a {key: [shape, dtype]} manifest.
39
+ # v3: canonical mesh_shape encoding shared with ModelArgs (the two writers previously encoded it
40
+ # differently and each rejected the other's marker), a `components` field so a text-only seed
41
+ # cannot certify a cache for a build that also needs the vision tower, and `cache_files` --
42
+ # the recursive list of .tensorbin files the completed build actually produced, verified
43
+ # per-file on read. That last one is load-bearing: ttnn.as_tensor PERSISTS whatever tensor it
44
+ # is handed on a cache miss, so a marker that outlives some of its tensorbins would otherwise
45
+ # dump placeholders to disk as real cache entries -- silent, permanent corruption. Verifying
46
+ # the recorded file set turns every such case back into a plain cold load. Also `build_variant`
47
+ # -- the build options (prefetcher, precision) that change an as_tensor cache FILENAME, matched
48
+ # exactly, because a different variant needs different files rather than fewer.
49
+ WEIGHT_CACHE_FORMAT_VERSION = 3
50
+
51
+ DEFAULT_FORCE_ENV = "TT_TRANSFORMERS_FORCE_MODEL_LOAD"
52
+
53
+
54
+ def _variant_digest(build_variant):
55
+ """Stable short digest of a build_variant dict ("none" for None)."""
56
+ if build_variant is None:
57
+ return "none"
58
+ return hashlib.sha1(json.dumps(build_variant, sort_keys=True, default=str).encode()).hexdigest()[:12]
59
+
60
+
61
+ def _variant_unverifiable(build_variant):
62
+ return bool(build_variant) and bool(build_variant.get("unverifiable"))
63
+
64
+
65
+ def marker_path(cache_path, build_variant=None):
66
+ """The marker file for one (cache dir, build variant).
67
+
68
+ The variant digest is part of the FILENAME, not just a field compared inside one shared
69
+ marker. A cache dir legitimately serves several build variants (the Llama CI job runs
70
+ eval-32 with and without the DRAM prefetcher against the same instruct cache), and a single
71
+ marker matched exactly would make each variant's seed evict the other's on every run -- both
72
+ then cold-load forever with nothing going red. One marker per variant lets them coexist.
73
+ (#45400 review, finding B3)"""
74
+ return Path(cache_path) / f"{WEIGHT_CACHE_MARKER}.{_variant_digest(build_variant)}"
75
+
76
+
77
+ def _dtype_from_str(s):
78
+ return getattr(torch, s.rsplit(".", 1)[-1])
79
+
80
+
81
+ def normalize_mesh_shape(mesh_shape):
82
+ """Canonical marker encoding for a mesh shape.
83
+
84
+ ``ttnn.MeshShape`` stringifies as ``MeshShape([1, 8])`` while callers that pass a plain tuple
85
+ stringify as ``(1, 8)``. Both writers must agree or each rejects the other's marker and the
86
+ model cold-loads forever (gemma3 inherits ModelArgs but its demos call this module). Normalize
87
+ everything to a plain tuple-of-ints string."""
88
+ try:
89
+ return str(tuple(int(d) for d in mesh_shape))
90
+ except TypeError:
91
+ return str(mesh_shape)
92
+
93
+
94
+ def _normalize_components(components):
95
+ """Canonical component list. ``None`` means "the whole model as this loader builds it" and is
96
+ encoded as a single implicit component so old-style callers stay self-consistent."""
97
+ if components is None:
98
+ return ["all"]
99
+ if isinstance(components, str):
100
+ return [components]
101
+ return sorted(str(c) for c in components)
102
+
103
+
104
+ def list_cache_files(cache_path):
105
+ """Every ``.tensorbin`` under ``cache_path``, recursively, as sorted relative POSIX paths.
106
+
107
+ Recursive because forked loaders nest per-layer weights in subdirectories (qwen36
108
+ ``layers.{n}/``, gemma4 ``layer_{i}/``); a top-level ``glob`` would call a cache complete when
109
+ only the root-level ``output.weight`` survived an interrupted seed."""
110
+ cache_path = Path(cache_path)
111
+ return sorted(p.relative_to(cache_path).as_posix() for p in cache_path.rglob("*.tensorbin"))
112
+
113
+
114
+ # One-entry cache so the completeness gate's validation load is reused by
115
+ # build_cached_state_dict instead of torch.load-ing the same multi-GB file twice per warm run
116
+ # (gemma-4-31b's embedding alone is ~2.8 GB). Keyed on (path, mtime, size) so a republished
117
+ # sidecar is never served stale; the builder consumes the entry so the tensors are not pinned
118
+ # past the build. (#45400 review, finding R1)
119
+ _SIDECAR_CACHE = {}
120
+
121
+
122
+ def load_host_sidecar(cache_path, *, consume=False):
123
+ """Load the host-weights sidecar, or None if absent/unreadable.
124
+
125
+ ``consume=True`` drops the memoized entry after returning it (the caller takes ownership)."""
126
+ sidecar = Path(cache_path) / HOST_WEIGHTS_SIDECAR
127
+ if not sidecar.is_file():
128
+ return None
129
+ try:
130
+ st = sidecar.stat()
131
+ key = (str(sidecar), st.st_mtime_ns, st.st_size)
132
+ host = _SIDECAR_CACHE.get(key)
133
+ if host is None:
134
+ host = torch.load(sidecar, map_location="cpu", weights_only=True)
135
+ _SIDECAR_CACHE.clear()
136
+ _SIDECAR_CACHE[key] = host
137
+ if consume:
138
+ _SIDECAR_CACHE.pop(key, None)
139
+ return host
140
+ except Exception:
141
+ return None
142
+
143
+
144
+ def weight_cache_is_complete(
145
+ cache_path,
146
+ *,
147
+ model_name,
148
+ n_layers,
149
+ mesh_shape,
150
+ components=None,
151
+ build_variant=None,
152
+ force_env=DEFAULT_FORCE_ENV,
153
+ ):
154
+ """True when the on-disk ttnn weight cache at ``cache_path`` was fully built by a previous run
155
+ for this exact build, and every tensorbin that build produced is still present.
156
+
157
+ ``components`` names the model parts this build will construct (e.g. ``"text"`` vs
158
+ ``"text+vision"``); a marker written by a narrower build does not satisfy a wider one, because
159
+ the wider build needs tensorbins the narrower one never wrote. ``force_env=...=1`` forces a
160
+ cold load."""
161
+ if force_env and os.getenv(force_env) == "1":
162
+ return False
163
+ # A variant we could not compute is a variant we cannot verify: accepting it could hand a
164
+ # placeholder to a build whose cache-filename set we did not check, and as_tensor would
165
+ # persist that placeholder to disk. Fail closed, loudly. (#45400 review, finding R3)
166
+ if _variant_unverifiable(build_variant):
167
+ logger.warning(
168
+ f"Warm-cache check for {cache_path}: build_variant could not be computed "
169
+ f"({build_variant.get('error', 'unknown error')}); forcing a cold load."
170
+ )
171
+ return False
172
+ cache_path = Path(cache_path)
173
+ marker = marker_path(cache_path, build_variant)
174
+ if not marker.is_file():
175
+ return False
176
+ try:
177
+ meta = json.loads(marker.read_text())
178
+ except (ValueError, OSError):
179
+ return False
180
+ if meta.get("format_version") != WEIGHT_CACHE_FORMAT_VERSION:
181
+ return False
182
+ if meta.get("model_name") != model_name or meta.get("n_layers") != n_layers:
183
+ return False
184
+ if meta.get("mesh_shape") != normalize_mesh_shape(mesh_shape):
185
+ return False
186
+ # The recorded build must cover every component this build needs. Superset is fine (a
187
+ # text+vision seed wrote the text tensorbins too, so it satisfies a text-only build); a subset
188
+ # is not (a text-only seed never wrote the vision tower's tensorbins, and accepting it would
189
+ # make as_tensor dump placeholders for them).
190
+ if not set(_normalize_components(components)).issubset(set(meta.get("components") or [])):
191
+ return False
192
+ # Build options that change an as_tensor cache FILENAME (prefetcher, precision) must match
193
+ # exactly. A superset rule is wrong here: a different variant does not need fewer files, it
194
+ # needs DIFFERENT ones, and any it is missing would be regenerated from the placeholder.
195
+ if meta.get("build_variant") != build_variant:
196
+ return False
197
+ if not meta.get("weights"):
198
+ return False
199
+ # Every tensorbin the completed build produced must still be on disk. Any missing file would
200
+ # otherwise be regenerated by as_tensor FROM THE PLACEHOLDER we are about to hand it, writing
201
+ # garbage into the cache permanently. Missing file => cold load, which rebuilds it correctly.
202
+ recorded = meta.get("cache_files")
203
+ if not recorded:
204
+ return False
205
+ present = set(list_cache_files(cache_path))
206
+ if not all(f in present for f in recorded):
207
+ return False
208
+ # If host weights were captured, the sidecar must be present AND loadable. A torn/corrupt
209
+ # sidecar (interrupted or racing seed) must fall back to a cold load -- the way a torn marker
210
+ # already does via the except above -- rather than pass this gate and then crash torch.load on
211
+ # every subsequent run, bricking the cache dir. Checked LAST so the load it performs is
212
+ # memoized only when the gate is about to pass, for build_cached_state_dict to consume.
213
+ # (#45400 review)
214
+ if meta.get("host_weights") and load_host_sidecar(cache_path) is None:
215
+ return False
216
+ return True
217
+
218
+
219
+ def mark_weight_cache_complete(
220
+ cache_path,
221
+ state_dict,
222
+ *,
223
+ model_name,
224
+ n_layers,
225
+ mesh_shape,
226
+ components=None,
227
+ build_variant=None,
228
+ is_moe=False,
229
+ is_host_weight=None,
230
+ ):
231
+ """Record that the ttnn weight cache at ``cache_path`` is fully built.
232
+
233
+ Writes a ``.weights_complete`` marker holding a ``{key: [shape, dtype]}`` manifest of every
234
+ weight plus the recursive list of ``.tensorbin`` files this build produced (verified per-file
235
+ on read). If ``is_host_weight(key)`` is provided, the (real) tensors it matches are also saved
236
+ to a ``.host_weights.pt`` sidecar so a later warm run can serve them for real (hybrid).
237
+
238
+ Call this only AFTER the model has been constructed, so the tensorbins exist to be recorded."""
239
+ if _variant_unverifiable(build_variant):
240
+ # Never certify a cache under an identity we could not compute -- a later run computing
241
+ # the same error string would otherwise warm-match it. (#45400 review, finding R3)
242
+ logger.warning(
243
+ f"Not marking weight cache complete at {cache_path}: build_variant could not be "
244
+ f"computed ({build_variant.get('error', 'unknown error')})."
245
+ )
246
+ return
247
+ cache_path = Path(cache_path)
248
+ marker = marker_path(cache_path, build_variant)
249
+ weights = {}
250
+ host = {}
251
+ for k, v in state_dict.items():
252
+ shape = getattr(v, "shape", None)
253
+ dt = getattr(v, "dtype", None)
254
+ if shape is None or dt is None:
255
+ continue # skip non-tensor entries
256
+ weights[k] = [list(shape), str(dt)]
257
+ if is_host_weight is not None and is_host_weight(k):
258
+ host[k] = v
259
+ try:
260
+ cache_path.mkdir(parents=True, exist_ok=True)
261
+ cache_files = list_cache_files(cache_path)
262
+ if not cache_files:
263
+ logger.warning(f"Not marking weight cache complete: no .tensorbin files under {cache_path}")
264
+ return
265
+ # Write both the sidecar and the marker atomically (temp file + os.replace, atomic on
266
+ # POSIX). Two jobs can seed the same (model, dtype, mesh) dir on one host concurrently, and
267
+ # an interrupted write must never leave a torn file that a later run picks up: a half-written
268
+ # sidecar would otherwise pass the is_file() gate and crash torch.load on every subsequent
269
+ # run. The temp name is pid-unique so two concurrent seeders cannot write the SAME temp
270
+ # inode -- with a fixed name, B could publish the file while A was still writing into it.
271
+ # Sidecar first, then marker, so the completeness gate only appears once its sidecar is
272
+ # fully in place. (#45400 review)
273
+ uniq = os.getpid()
274
+ if host:
275
+ sidecar = cache_path / HOST_WEIGHTS_SIDECAR
276
+ sidecar_tmp = sidecar.with_suffix(sidecar.suffix + f".tmp.{uniq}")
277
+ torch.save(host, sidecar_tmp)
278
+ os.replace(sidecar_tmp, sidecar)
279
+ marker_body = json.dumps(
280
+ {
281
+ "format_version": WEIGHT_CACHE_FORMAT_VERSION,
282
+ "model_name": model_name,
283
+ "n_layers": n_layers,
284
+ "mesh_shape": normalize_mesh_shape(mesh_shape),
285
+ "components": _normalize_components(components),
286
+ "build_variant": build_variant,
287
+ "cache_files": cache_files,
288
+ "is_moe": bool(is_moe),
289
+ "host_weights": sorted(host.keys()),
290
+ "weights": weights,
291
+ }
292
+ )
293
+ marker_tmp = marker.with_suffix(marker.suffix + f".tmp.{uniq}")
294
+ marker_tmp.write_text(marker_body)
295
+ os.replace(marker_tmp, marker)
296
+ logger.info(f"Marked ttnn weight cache complete: {marker} ({len(weights)} weights, {len(host)} host-loaded)")
297
+ except Exception as e:
298
+ # Deliberately broad: this function only RECORDS completion -- failing to record must
299
+ # never kill a build that already succeeded. The concrete case: on a read-only
300
+ # /mnt/MLPerf, torch.save of the host sidecar raises RuntimeError from torch's C++
301
+ # serializer (inline_container.cc "Read-only file system"), not OSError, and the narrow
302
+ # except crashed every read-only cold run of the sidecar models (gemma4/gemma3) right
303
+ # after a successful build. (#45400 review, finding R5; seen on Gemma-4-E2B bh_p150,
304
+ # run 32511945147)
305
+ logger.warning(f"Could not write weight-cache completion marker {marker}: {e}")
306
+
307
+
308
+ class CachedStateDict(collections.abc.MutableMapping):
309
+ """A stand-in ``state_dict`` for warm-cache builds.
310
+
311
+ Serves the real tensor for keys captured in the host-weights sidecar; for every other key it
312
+ returns a fresh dataless ``torch.empty`` of the manifest shape/dtype (which ``ttnn.as_tensor``
313
+ discards on the guaranteed cache hit). Mutable (some loaders ``setdefault`` missing KV-shared
314
+ weights) and truthy (some loaders gate real-weight loading on ``if state_dict:``)."""
315
+
316
+ # Explicit marker that this is a warm-cache stand-in, NOT real weights. Callers that must tell
317
+ # "warm-cache placeholder" apart from "real weights" MUST branch on this attribute, never on
318
+ # truthiness: this mapping is truthy (non-zero __len__) but tt_transformers' _PlaceholderStateDict
319
+ # is falsy (__bool__ -> False), so a truthiness test silently means opposite things for the two.
320
+ # If tt_transformers is ever collapsed onto this class (a listed follow-up), the attribute keeps
321
+ # `if is_placeholder(...)` reload sites (e.g. test_model_prefill) correct. (#45400 review)
322
+ is_placeholder = True
323
+
324
+ def __init__(self, manifest, host):
325
+ self._manifest = manifest # key -> (shape, dtype_str)
326
+ self._host = dict(host or {}) # key -> real torch.Tensor
327
+ self._overrides = {} # keys set by the caller at build time
328
+ self._deleted = set()
329
+
330
+ def __getitem__(self, key):
331
+ if key in self._deleted:
332
+ raise KeyError(key)
333
+ if key in self._overrides:
334
+ return self._overrides[key]
335
+ if key in self._host:
336
+ return self._host[key]
337
+ spec = self._manifest.get(key)
338
+ if spec is None:
339
+ raise KeyError(key)
340
+ shape, dt = spec
341
+ return torch.empty(tuple(shape), dtype=_dtype_from_str(dt))
342
+
343
+ def __setitem__(self, key, value):
344
+ self._deleted.discard(key)
345
+ self._overrides[key] = value
346
+
347
+ def __delitem__(self, key):
348
+ if key not in self:
349
+ raise KeyError(key)
350
+ self._overrides.pop(key, None)
351
+ if key in self._host or key in self._manifest:
352
+ self._deleted.add(key)
353
+
354
+ def __iter__(self):
355
+ seen = set()
356
+ for k in list(self._overrides) + list(self._host) + list(self._manifest):
357
+ if k in self._deleted or k in seen:
358
+ continue
359
+ seen.add(k)
360
+ yield k
361
+
362
+ def __len__(self):
363
+ return sum(1 for _ in self)
364
+
365
+ # Mapping's default __contains__/get/items route through __getitem__, which allocates a
366
+ # full-size torch.empty for EVERY key touched -- including multi-GB ones like lm_head.weight.
367
+ # substate() (models/tt_dit/utils/substate.py) iterates .items() and filters by prefix, so a
368
+ # 62-layer gemma4 build would allocate the entire model once per layer just to discard it.
369
+ # Answer membership from the key sets, and make items() lazy so only matching keys materialize.
370
+ def __contains__(self, key):
371
+ if key in self._deleted:
372
+ return False
373
+ return key in self._overrides or key in self._host or key in self._manifest
374
+
375
+ def keys(self):
376
+ return list(self)
377
+
378
+ def items(self):
379
+ for k in self:
380
+ yield k, self[k]
381
+
382
+ def get(self, key, default=None):
383
+ if key not in self:
384
+ return default
385
+ return self[key]
386
+
387
+
388
+ def build_cached_state_dict(cache_path, host=None, args=None, build_variant=None):
389
+ """Build the warm-cache stand-in ``state_dict`` from the marker manifest + host sidecar.
390
+
391
+ ``host`` may be a sidecar dict already loaded by ``weight_cache_is_complete``'s validation, to
392
+ avoid a second multi-GB ``torch.load`` of the same file on every warm run (gemma-4-31b's
393
+ embedding alone is ~2.8 GB).
394
+
395
+ ``args`` (a ModelArgs-like) has ``is_mixture_of_experts`` restored from the marker. That flag is
396
+ normally set as a side effect of ``load_state_dict`` (by sniffing for ``.experts.`` keys), which
397
+ the warm path skips -- so without this a MoE checkpoint would build a dense decoder and die on a
398
+ missing ``feed_forward.w1.weight``. (#45400 review)"""
399
+ cache_path = Path(cache_path)
400
+ meta = json.loads(marker_path(cache_path, build_variant).read_text())
401
+ manifest = meta["weights"]
402
+ if args is not None and hasattr(args, "__dict__"):
403
+ args.is_mixture_of_experts = bool(meta.get("is_moe", False))
404
+ # fuse_qkv / fuse_mlp are normally sniffed from the checkpoint keys inside load_state_dict,
405
+ # which the warm path skips -- leaving them at their __init__ defaults and silently changing
406
+ # how the decoder is built. The manifest holds the same key set, so derive them identically.
407
+ keys = manifest.keys()
408
+ args.fuse_qkv = any("qkv" in k for k in keys)
409
+ args.fuse_mlp = any("gate_up" in k for k in keys)
410
+ if args.is_mixture_of_experts:
411
+ args.moe = True
412
+ expert_indices = [int(k[-11]) + 1 for k in keys if "block_sparse_moe.experts" in k]
413
+ if expert_indices:
414
+ args.num_experts = max(expert_indices)
415
+ elif hasattr(args, "num_local_experts"):
416
+ args.num_experts = args.num_local_experts
417
+ if host is None and meta.get("host_weights"):
418
+ # consume=True: reuse the load the completeness gate just performed and release the
419
+ # memoized entry, so the sidecar is read from NAS once per warm run, not twice. (R1)
420
+ host = load_host_sidecar(cache_path, consume=True)
421
+ host = host or {}
422
+ logger.info(
423
+ f"Warm ttnn weight cache: built state_dict for {len(manifest)} weights "
424
+ f"({len(host)} real host weights, no full HF load)."
425
+ )
426
+ return CachedStateDict(manifest, host)
code/models/tt_transformers/Host_Mem_Profile.md ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model memory on host for performance and accuracy
2
+
3
+ [Host memory usage results](../sample_data/host_mem_profiling/) at peak memory (in MB) are collected from host_mem_profiler.py by running the target demo under memory_profiler at
4
+ 100 ms sampling intervals. You can generate these results by running the script with --mesh-device, --hf-model, and
5
+ --test arguments in [demo/simple_text_demo.py](demo/simple_text_demo.py); it produces a timestamped PNG plot under profiling_results/ and prints peak and baseline RSS to the
6
+ console.
7
+
8
+ Note that all measurements include child processes spawned by the TT device runtime, giving a realistic view of total
9
+ host-side footprint. Peak memory is defined as the maximum RSS observed across the full test lifetime — including
10
+ model weight loading, KV-cache allocation, and decode iterations. Baseline is the RSS at process start before any
11
+ model work begins.
12
+
13
+ Host memory (RSS) was profiled across three configurations — [Performance](#performance), [Accuracy](#accuracy), and [Multimodal](#multimodal-models) — using up to 200
14
+ decode iterations on T3K (8-chip) and N300 (2-chip) devices.
15
+
16
+ All [numbers](../sample_data/host_mem_profiling/) below were captured with a maximum generation of 200 tokens (200 decode iterations) to match the
17
+ conditions used for centralized targets in [models/model_targets.yaml](../model_targets.yaml).
18
+
19
+ ## Performance
20
+
21
+ This configuration uses bfp4 MLP and bfp8 attention weights for all models except:
22
+ * Qwen-2.5-7B, which uses bfp8 MLP and bfp16 attention weights in all decoder layers
23
+ * Llama-3.1-8B which uses bfp8 MLP in only the 32nd decoder layer and bfp4 MLP elsewhere
24
+
25
+ | Model | Device | batch-1 max mem (MB) | batch-32 max mem (MB) | Speed (t/s/u) | TTFT (ms) |
26
+ |-------------------|-------------|-----------------------|-----------------------|---------------|-----------|
27
+ | Llama-3.2-1B | T3K | 3532 | 2712 | 119.8 | 32 |
28
+ | Llama-3.2-3B | T3K | 7846 | 5388 | 68.5 | 52 |
29
+ | Llama-3.1-8B | T3K | 13683 | 6922 | 64.3 | 53 |
30
+ | Llama-3.2-11B | T3K | 13885 | 7254 | 62.7 | 47 |
31
+ | Llama-3.1-70B | T3K | 35820 | 35187 | 16.6 | 164 |
32
+ | Llama-3.3-70B | T3K | 101640 | 36176 | 16.6 | 164 |
33
+ | Llama-3.2-90B | T3K | 34427 | 34842 | 6 | 5535 |
34
+ | Qwen2.5-7B | N300 | 16597 | 17754 | 24.6 | 92 |
35
+ | Qwen2.5-72B | T3K | 147106 | 145432 | 15.2 | 225 |
36
+ | Qwen2.5-32B | T3K | 53434 | 15594 | 22.4 | 190 |
37
+ | Qwen3-32B | T3K | 56033 | 20422 | 22.9 | 123 |
38
+ | QwQ-32B | T3K | 69900 | 67186 | 20.7 | 105 |
39
+
40
+ ## Accuracy
41
+
42
+ This configuration uses bfp8 MLP and BF16 attention weights (70B+ models use bfp8 attention and bfp4 MLP).
43
+ Llama 3 models test as insensitive to attention precision and so we use bfp8 attention and kv-cache for them even in accuracy mode.
44
+
45
+ | Model | Device | batch-1 max mem (MB) | batch-32 max mem (MB) | Speed (t/s/u) | TTFT (ms) |
46
+ |-------------------|-------------|----------------------|------------------------|---------------|-----------|
47
+ | Llama-3.2-1B | T3K | 4654 | 2707 | 120.5 | 28 |
48
+ | Llama-3.2-3B | T3K | 10374 | 5270 | 67.9 | 69 |
49
+ | Llama-3.1-8B | T3K | 20406 | 7704 | 60.8 | 81 |
50
+ | Llama-3.2-11B | T3K | 20265 | 7051 | 61.4 | 53 |
51
+ | Llama-3.1-70B | T3K | 154122 | 37370 | 16.5 | 168 |
52
+ | Llama-3.3-70B | T3K | 100112 | 35026 | 16.5 | 168 |
53
+ | Llama-3.2-90B | T3K | 155139 | 34997 | 6 | 5600 |
54
+ | Qwen2.5-7B | N300 | 20189 | 17956 | 24.6 | 92 |
55
+ | Qwen2.5-72B | T3K | 146204 | 147032 | 15.1 | 216 |
56
+ | Qwen2.5-32B | T3K | 74162 | 14811 | 19.7 | 183 |
57
+ | Qwen3-32B | T3K | 76288 | 20785 | 19.6 | 119 |
58
+ | QwQ-32B | T3K | 72627 | 67278 | 18.3 | 120 |
59
+
60
+
61
+ ## Multimodal Models
62
+ The results are collected using this script [demo/simple_vision_demo.py](demo/simple_vision_demo.py)); with host memory profiler and evaluated on both language and vision branches.
63
+ The memory is reported in MB.
64
+
65
+ | Model | Device | batch-1 max mem (MB) | batch-32 max mem (MB) | Speed (t/s/u) | TTFT (ms) |
66
+ |-------------------|-------------|-----------------------|-----------------------|---------------|-----------|
67
+ | Llama-3.2-11B | T3K | 14309 | 8610 | 61.4 | 53 |
68
+ | Llama-3.2-90B | T3K | 75451 | - | 6 | 5600 |
69
+ | Qwen2.5-VL-3B | N300 | 9319 | 9068 | 24.6 | 92 |
70
+ | Qwen2.5-VL-7B | N300 | 18745 | 10946 | 24.6 | 92 |
71
+ | Qwen2.5-VL-72B | T3K | 160437 | 147925 | 15.1 | 216 |
72
+ | Qwen2.5-VL-32B | T3K | 71960 | 68464 | 19.7 | 183 |
73
+ | Qwen3-VL-32B | T3K | 76297 | 21514 | 19.6 | 119 |
74
+
75
+
76
+ [Performance](#performance) vs. [Accuracy](#accuracy) modes trade memory for precision: accuracy mode uses BF16 attention weights versus bfp8/bfp4
77
+ in performance mode, and typically raises peak memory. The increase is clearest at batch-1, where most models grow noticeably Llama-3.1-8B rises from 13.7 GB to 20.4 GB and Qwen2.5-32B from 53 GB to 74 GB — while 70B+ models show a smaller relative increase since attention weight precision is already reduced in both modes. At batch-32 the picture is mixed: KV-cache and activations dominate, and several models (Llama-3.2-1B/3B/11B, Llama-3.3-70B, Qwen2.5-32B) report slightly lower peaks in accuracy mode rather than higher.
78
+
79
+ Memory scales roughly with model size, ranging from ~3–4 GB for 1B models up to ~147–155 GB for 72–90B models. The
80
+ largest models (Qwen2.5-72B, Llama-3.2-90B) show little difference between batch-1 and batch-32, suggesting KV-cache
81
+ and weight loading dominate over activation memory. Mid-size models (32B class) exhibit a larger batch-1 to batch-32
82
+ gap, likely due to KV-cache growth with sequence length at batch-1.
83
+
84
+ Throughput and TTFT follow expected trends: smaller models deliver higher tokens/s/user (up to 120 t/s/u for 1B) and
85
+ low TTFT (~30 ms), while 90B models drop to 6 t/s/u with TTFT exceeding 5.5 seconds.
86
+
87
+ [Multimodal](#multimodal-models) models carry a modest overhead versus their text-only counterparts — Llama-3.2-11B adds ~400 MB at batch-1
88
+ though Qwen2.5-VL-72B reaches the highest overall footprint at ~160 GB.
code/models/tt_transformers/PERF.md ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Note: DEPRECATED. PLEASE CHECK THE YAML FILE [models/model_targets.yaml](models/model_targets.yaml)
2
+
3
+ # Model performance and accuracy
4
+
5
+ Performance and token accuracy using teacher forcing is collected from [demo/simple_text_demo.py](demo/simple_text_demo.py) with the `ci-token-matching` test case. You can generate this table by running these tests with the `lt` tool (tell it to run `table` or `pareto`) and pressing `m` whilst in the results section to export to markdown.
6
+
7
+ Note that token accuracy parses the below to determine expected values +- 0.5. In May 2025 we switched the default to measuring the accuracy by prefilling 512 tokens and generating another 511, rather than generating 128 tokens in earlier versions. This caused overall accuracy values to drop slightly.
8
+
9
+ Also note that all the performance metrics below were taken for a maximum generation of 200 tokens, i.e., 200 decode iterations.
10
+
11
+ ## Performance
12
+
13
+ This configuration uses bfp4 MLP and bfp8 attention weights for all models except:
14
+ * Qwen-2.5-7B, which uses bfp8 MLP and bfp16 attention weights in all decoder layers
15
+ * Llama-3.1-8B which uses bfp8 MLP in only the 32nd decoder layer and bfp4 MLP elsewhere
16
+
17
+ | Model | Device | Top-1 (%) | Top-5 (%) | Speed (t/s/u) | TTFT (ms) |
18
+ |-------------------|-------------|-----------|-----------|---------------|-----------|
19
+ | Llama-3.2-1B | N150 | 79 | 97 | 87.8 | 26 |
20
+ | Llama-3.2-1B | N300 | 79 | 97 | 105.9 | 22 |
21
+ | Llama-3.2-1B | T3K | 80 | 97 | 119.8 | 32 |
22
+ | Llama-3.2-1B | TG | 77 | 96 | 51.0 | |
23
+ | Llama-3.2-3B | N150 | 89 | 98 | 54.0 | 55 |
24
+ | Llama-3.2-3B | N300 | 89 | 98 | 68.0 | 39 |
25
+ | Llama-3.2-3B | T3K | 91 | 99 | 68.5 | 52 |
26
+ | Llama-3.2-3B | TG | 87 | 97 | 33.5 | |
27
+ | Llama-3.1-8B | N150 | 90 | 97 | 28.3 | 104 |
28
+ | Llama-3.1-8B | N300 | 90 | 97 | 44.2 | 67 |
29
+ | Llama-3.1-8B | P100 | 90 | 98 | 29.5 | 84 |
30
+ | Llama-3.1-8B | P150 | 90 | 98 | 33.6 | 76 |
31
+ | Llama-3.1-8B | T3K | 90 | 98 | 64.3 | 53 |
32
+ | Llama-3.1-8B | T3K (DP=4) | | | 39.6 | 58 |
33
+ | Llama-3.1-8B | T3K (DP=8) | | | 24.9 | 86 |
34
+ | Llama-3.1-8B | TG | 88 | 97 | 29.5 | |
35
+ | Llama-3.2-11B | N150 | 90 | 98 | 55.5 | 58 |
36
+ | Llama-3.2-11B | N300 | 90 | 98 | 44.1 | 67 |
37
+ | Llama-3.2-11B | T3K | 90 | 98 | 62.7 | 47 |
38
+ | Llama-3.2-11B | TG | 87 | 97 | 29.5 | |
39
+ | Llama-3.1-70B | T3K | 96 | 100 | 16.6 | 164 |
40
+ | Llama-3.3-70B | T3K | 96 | 100 | 16.6 | 164 |
41
+ | Llama-3.1-70B | TG | 95 | 100 | 12.7 | |
42
+ | Llama-3.3-70B | TG | 95 | 100 | 12.7 | |
43
+ | Llama-3.1-70B | TG (DP=4) | | | 14.8 | 189 |
44
+ | Llama-3.2-90B | T3K | 96 | 100 | 6 | 5535 |
45
+ | Qwen2.5-7B | N300 | 84 | 96 | 24.6 | 92 |
46
+ | Qwen2.5-72B | T3K | 99 | 100 | 15.2 | 225 |
47
+ | Qwen2.5-32B | T3K | 98 | 99 | 22.4 | 190 |
48
+ | Qwen2.5-72B | P150x8 | 99 | 100 | | |
49
+ | Qwen2.5-32B | P150x8 | 97 | 99 | | |
50
+ | Qwen2.5-Coder-32B | T3K | 96 | 99 | 22.4 | 190 |
51
+ | Qwen3-32B | T3K | 89 | 97 | 22.9 | 123 |
52
+ | QwQ-32B | T3K | 96 | 100 | 20.7 | 105 |
53
+ | Phi3.5-mini | N150 | | | 43.2 | 98 |
54
+ | Phi3.5-mini | N300 | | | 57.8 | 62 |
55
+ | Phi3.5-mini | T3K | | | 48.8 | 51 |
56
+ | Mistral-7B | N150 | 95 | 99 | 29.75 | 100.24 |
57
+ | Mistral-7B | N300 | 95 | 100 | 47.01 | 65.95 |
58
+ | Mistral-7B | T3K | 95 | 100 | 67.82 | 53.93 |
59
+ | Mistral-Small-3.1-24B | T3K | 95 | 99 | | |
60
+ | Phi-3-mini-128k-instruct | N150 | 89 | 99 | 45.0 | 73.32 |
61
+ | Phi-3-mini-128k-instruct | N300 | 89 | 99 | 60.87 | 114.94 |
62
+ | Phi-4 | N300 | 97 | 100 | 37.34 | 123.33 |
63
+ | Mixtral-8x7B-v0.1 | T3K | 98 | 100 | 67.82 | 53.93 |
64
+ | Ministral-8B | N300 | 93 | 98 | 22.15 | 79.3 |
65
+
66
+ ## Accuracy
67
+
68
+ This configuration uses bfp8 MLP and BF16 attention weights (70B+ models use bfp8 attention and bfp4 MLP).
69
+ Llama 3 models test as insensitive to attention precision and so we use bfp8 attention and kv-cache for them even in accuracy mode.
70
+
71
+ | Model | Device | Top-1 (%) | Top-5 (%) | Speed (t/s/u) | TTFT (ms) |
72
+ |-------------------|-------------|-----------|-----------|---------------|-----------|
73
+ | Llama-3.2-1B | N150 | 87 | 99 | 84.7 | 29 |
74
+ | Llama-3.2-1B | N300 | 87 | 98 | 102.8 | 21 |
75
+ | Llama-3.2-1B | T3K | 88 | 99 | 120.5 | 28 |
76
+ | Llama-3.2-1B | TG | 85 | 98 | 48.4 | |
77
+ | Llama-3.2-3B | N150 | 96 | 100 | 47.6 | 63 |
78
+ | Llama-3.2-3B | N300 | 96 | 100 | 63.5 | 41 |
79
+ | Llama-3.2-3B | T3K | 96 | 100 | 67.9 | 69 |
80
+ | Llama-3.2-3B | TG | 92 | 99 | 33.6 | |
81
+ | Llama-3.1-8B | N150 | 96 | 100 | 25.2 | 138 |
82
+ | Llama-3.1-8B | N300 | 96 | 100 | 38.8 | 79 |
83
+ | Llama-3.1-8B | T3K | 97 | 100 | 60.8 | 81 |
84
+ | Llama-3.1-8B | TG | 95 | 100 | 29.5 | |
85
+ | Llama-3.2-11B | N150 | 95 | 100 | 56.7 | 62 |
86
+ | Llama-3.2-11B | N300 | 95 | 100 | 38.3 | 78 |
87
+ | Llama-3.2-11B | T3K | 96 | 100 | 61.4 | 53 |
88
+ | Llama-3.2-11B | TG | 94 | 100 | 29.5 | |
89
+ | Llama-3.1-70B | T3K | 96 | 100 | 16.5 | 168 |
90
+ | Llama-3.1-70B | TG | 95 | 100 | 12.7 | |
91
+ | Llama-3.2-90B | T3K | 96 | 100 | 6 | 5600 |
92
+ | Qwen2.5-7B | N300 | 84 | 96 | 24.6 | 92 |
93
+ | Qwen2.5-72B | T3K | 99 | 100 | 15.1 | 216 |
94
+ | Qwen2.5-32B | T3K | 95 | 99 | 19.7 | 183 |
95
+ | Qwen2.5-72B | P150x8 | 99 | 100 | | |
96
+ | Qwen2.5-32B | P150x8 | 97 | 99 | | |
97
+ | Qwen2.5-Coder-32B | T3K | 95 | 99 | 19.7 | 183 |
98
+ | Qwen3-32B | T3K | 95 | 100 | 19.6 | 119 |
99
+ | QwQ-32B | T3K | 99 | 100 | 18.3 | 120 |
100
+ | Phi3.5-mini | N150 | | | 38.8 | 92 |
101
+ | Phi3.5-mini | N300 | | | 53.9 | 63 |
102
+ | Phi3.5-mini | T3K | | | 48.6 | 53 |
103
+ | Mistral-7B | N150 | 96 | 100 | 29.75 | 100.24 |
104
+ | Mistral-7B | N300 | 97 | 100 | 47.01 | 65.95 |
105
+ | Mistral-7B | T3K | 98 | 100 | 67.82 | 53.93 |
106
+ | Mistral-Small-3.1-24B | T3K | 95 | 99 | | |
107
+ | Phi-3-mini-128k-instruct | N150 | 94 | 99 | 40.41 | 82.58 |
108
+ | Phi-3-mini-128k-instruct | N300 | 94 | 99 | 57.0 | 115.36 |
109
+ | Phi-4 | N300 | 99 | 100 | 20.48 | 146.32 |
110
+ | Mixtral-8x7B-v0.1 | T3K | 95 | 100 | 67.82 | 53.93 |
111
+ | Ministral-8B | N300 | 97 | 100 | 19.95 | 93.22 |
112
+
113
+ ## Long-context (64K Tokens)
114
+
115
+ This configuration uses bfp4 MLP FF1+FF3 for all models. **Batch_size=1 and prefill_length is 64k tokens.**
116
+
117
+ | Model | Device | Speed (t/s/u) | TTFT (ms) |
118
+ |----------------|--------|---------------|-----------|
119
+ | Llama-3.2-1B | N150 | 53.0 | 20066 |
120
+ | Llama-3.2-1B | N300 | 65.2 | 10949 |
121
+ | Llama-3.2-1B | T3K | 73.7 | 5271 |
122
+ | Llama-3.2-1B | TG | | |
123
+ | Llama-3.2-3B | N150 | 25.3 | 46743 |
124
+ | Llama-3.2-3B | N300 | 34.8 | 22921 |
125
+ | Llama-3.2-3B | T3K | 41.0 | 10677 |
126
+ | Llama-3.2-3B | TG | | |
127
+ | Llama-3.1-8B | N150 | 16.9 | 64385 |
128
+ | Llama-3.1-8B | N300 | 26.1 | 36229 |
129
+ | Llama-3.1-8B | T3K | 38.1 | 16165 |
130
+ | Llama-3.1-8B | TG | | |
131
+ | Llama-3.2-11B | N300 | 26.1 | 36247 |
132
+ | Llama-3.2-11B | T3K | 38.4 | 16167 |
133
+ | Llama-3.2-11B | TG | | |
134
+ | Llama-3.1-70B | T3K | 11.9 | 74363 |
135
+ | Llama-3.1-70B | TG | | |
136
+ | Qwen2.5-7B | N300 | | |
137
+ | Qwen2.5-72B | T3K | | |
138
+
139
+ ## Long-context (32K Tokens)
140
+
141
+ This configuration uses bfp4 MLP FF1+FF3 for all models. **Batch_size=1 and prefill_length is 32k tokens.**
142
+
143
+ | Model | Device | Speed (t/s/u) | TTFT (ms) |
144
+ |---------------------------|--------|---------------|-----------|
145
+ | Phi-3-mini-128k-instruct | N300 | 26.1 | 10072 |
146
+
147
+ ## Short-Context, Batch-32
148
+
149
+ This configuration uses bfp4 MLP FF1+FF3 for all models. **Batch_size=32 and prefill_length is 128 tokens.**
150
+
151
+ | Model | Device | Speed (t/s/u) | avg TTFT (ms) |
152
+ |----------------|--------|---------------|---------------|
153
+ | Llama-3.2-1B | N150 | 54.7 | 38 |
154
+ | Llama-3.2-1B | N300 | 64.2 | 34 |
155
+ | Llama-3.2-1B | T3K | 69.9 | 42 |
156
+ | Llama-3.2-1B | TG | | |
157
+ | Llama-3.2-3B | N150 | 36.5 | 69 |
158
+ | Llama-3.2-3B | N300 | 45.8 | 51 |
159
+ | Llama-3.2-3B | T3K | 47.8 | 63 |
160
+ | Llama-3.2-3B | TG | | |
161
+ | Llama-3.1-8B | N150 | 22.3 | 119 |
162
+ | Llama-3.1-8B | N300 | 33.5 | 80 |
163
+ | Llama-3.1-8B | T3K | 45.6 | 64 |
164
+ | Llama-3.1-8B | TG | | |
165
+ | Llama-3.2-11B | N300 | 33.4 | 79 |
166
+ | Llama-3.2-11B | T3K | 45.1 | 64 |
167
+ | Llama-3.2-11B | TG | | |
168
+ | Llama-3.1-70B | T3K | 14.8 | 192 |
169
+ | Llama-3.1-70B | TG | | |
170
+ | Qwen2.5-7B | N300 | | |
171
+ | Qwen2.5-72B | T3K | | |
172
+ | Phi-3-mini-128k-instruct | 150 | 25.66 | 68.58 |
173
+ | Phi-3-mini-128k-instruct | N300 | 39.4 | 85.99 |
174
+
175
+ # Llama 3 model precision and math fidelity
176
+
177
+ ## precision_cfg = {ff1_3: bfp4, ff2: bfp4, wqkv: bfp8, wo: bfp8, kv_cache: bfp8, activation: mixed}, fidelity_cfg = {li_ff1_3: lofi, li_ff2: lofi, li_qkv_decode: hifi2, li_o_decode: hifi2, sdpa_decode: hifi2na, li_qkv_prefill: hifi2, li_o_prefill: hifi2fp16, sdpa_prefill: hifi4}
178
+
179
+ | Model | Device | Top-1 (%) | Top-5 (%) | Speed (t/s/u) | TTFT (ms) |
180
+ |----------------|--------|-----------|-----------|---------------|-----------|
181
+ | Llama-3.2-1B | N300 | 85 | 98 | 100.3 | 69 |
182
+
183
+ ## precision_cfg = {ff1_3: bfp4, ff2: bfp8, wqkv: bfp8, wo: bfp8, kv_cache: bfp8, activation: mixed}, fidelity_cfg = {li_ff1_3: lofi, li_ff2: hifi2, li_qkv_decode: hifi2, li_o_decode: hifi2, sdpa_decode: hifi2na, li_qkv_prefill: hifi2, li_o_prefill: hifi2fp16, sdpa_prefill: hifi4}
184
+
185
+ | Model | Device | Top-1 (%) | Top-5 (%) | Speed (t/s/u) | TTFT (ms) |
186
+ |----------------|--------|-----------|-----------|---------------|-----------|
187
+ | Llama-3.2-1B | N300 | 88 | 98 | 100.3 | 55 |
188
+
189
+ ## precision_cfg = {ff1_3: bfp4, ff2: bf16, wqkv: bfp8, wo: bfp8, kv_cache: bfp8, activation: mixed}, fidelity_cfg = {li_ff1_3: lofi, li_ff2: hifi4, li_qkv_decode: hifi2, li_o_decode: hifi2, sdpa_decode: hifi2na, li_qkv_prefill: hifi2, li_o_prefill: hifi2fp16, sdpa_prefill: hifi4}
190
+
191
+ | Model | Device | Top-1 (%) | Top-5 (%) | Speed (t/s/u) | TTFT (ms) |
192
+ |----------------|--------|-----------|-----------|---------------|-----------|
193
+ | Llama-3.2-1B | N300 | 87 | 98 | 96.8 | 51 |
194
+
195
+ ## precision_cfg = {ff1_3: bfp8, ff2: bfp4, wqkv: bfp8, wo: bfp8, kv_cache: bfp8, activation: mixed}, fidelity_cfg = {li_ff1_3: hifi2, li_ff2: lofi, li_qkv_decode: hifi2, li_o_decode: hifi2, sdpa_decode: hifi2na, li_qkv_prefill: hifi2, li_o_prefill: hifi2fp16, sdpa_prefill: hifi4}
196
+
197
+ | Model | Device | Top-1 (%) | Top-5 (%) | Speed (t/s/u) | TTFT (ms) |
198
+ |----------------|--------|-----------|-----------|---------------|-----------|
199
+ | Llama-3.2-1B | N300 | 87 | 98 | 98.5 | 50 |
200
+
201
+ ## precision_cfg = {ff1_3: bfp8, ff2: bfp8, wqkv: bfp8, wo: bfp8, kv_cache: bfp8, activation: mixed}, fidelity_cfg = {li_ff1_3: hifi2, li_ff2: hifi2, li_qkv_decode: hifi2, li_o_decode: hifi2, sdpa_decode: hifi2na, li_qkv_prefill: hifi2, li_o_prefill: hifi2fp16, sdpa_prefill: hifi4}
202
+
203
+ | Model | Device | Top-1 (%) | Top-5 (%) | Speed (t/s/u) | TTFT (ms) |
204
+ |----------------|--------|-----------|-----------|---------------|-----------|
205
+ | Llama-3.2-1B | N300 | 91 | 98 | 99.0 | 60 |
206
+
207
+ ## precision_cfg = {ff1_3: bfp8, ff2: bf16, wqkv: bfp8, wo: bfp8, kv_cache: bfp8, activation: mixed}, fidelity_cfg = {li_ff1_3: hifi2, li_ff2: hifi4, li_qkv_decode: hifi2, li_o_decode: hifi2, sdpa_decode: hifi2na, li_qkv_prefill: hifi2, li_o_prefill: hifi2fp16, sdpa_prefill: hifi4}
208
+
209
+ | Model | Device | Top-1 (%) | Top-5 (%) | Speed (t/s/u) | TTFT (ms) |
210
+ |----------------|--------|-----------|-----------|---------------|-----------|
211
+ | Llama-3.2-1B | N300 | 89 | 99 | 95.2 | 49 |
212
+
213
+ ## precision_cfg = {ff1_3: bf16, ff2: bfp4, wqkv: bfp8, wo: bfp8, kv_cache: bfp8, activation: mixed}, fidelity_cfg = {li_ff1_3: hifi4, li_ff2: lofi, li_qkv_decode: hifi2, li_o_decode: hifi2, sdpa_decode: hifi2na, li_qkv_prefill: hifi2, li_o_prefill: hifi2fp16, sdpa_prefill: hifi4}
214
+
215
+ | Model | Device | Top-1 (%) | Top-5 (%) | Speed (t/s/u) | TTFT (ms) |
216
+ |----------------|--------|-----------|-----------|---------------|-----------|
217
+ | Llama-3.2-1B | N300 | 89 | 98 | 95.2 | 53 |
218
+
219
+ ## precision_cfg = {ff1_3: bf16, ff2: bfp8, wqkv: bfp8, wo: bfp8, kv_cache: bfp8, activation: mixed}, fidelity_cfg = {li_ff1_3: hifi4, li_ff2: hifi2, li_qkv_decode: hifi2, li_o_decode: hifi2, sdpa_decode: hifi2na, li_qkv_prefill: hifi2, li_o_prefill: hifi2fp16, sdpa_prefill: hifi4}
220
+
221
+ | Model | Device | Top-1 (%) | Top-5 (%) | Speed (t/s/u) | TTFT (ms) |
222
+ |----------------|--------|-----------|-----------|---------------|-----------|
223
+ | Llama-3.2-1B | N300 | 91 | 98 | 94.4 | 57 |
224
+
225
+ ## precision_cfg = {ff1_3: bf16, ff2: bf16, wqkv: bfp8, wo: bfp8, kv_cache: bfp8, activation: mixed}, fidelity_cfg = {li_ff1_3: hifi4, li_ff2: hifi4, li_qkv_decode: hifi2, li_o_decode: hifi2, sdpa_decode: hifi2na, li_qkv_prefill: hifi2, li_o_prefill: hifi2fp16, sdpa_prefill: hifi4}
226
+
227
+ | Model | Device | Top-1 (%) | Top-5 (%) | Speed (t/s/u) | TTFT (ms) |
228
+ |----------------|--------|-----------|-----------|---------------|-----------|
229
+ | Llama-3.2-1B | N300 | 90 | 98 | 91.2 | 60 |
230
+
231
+ ## precision_cfg = {ff1_3: bfp8, ff2: bfp8, wqkv: bfp8, wo: bfp4, kv_cache: bfp8, activation: bf16}, fidelity_cfg = {li_ff1_3: hifi2, li_ff2: hifi2, li_qkv_decode: hifi2, li_o_decode: lofi, sdpa_decode: hifi2na, li_qkv_prefill: hifi2, li_o_prefill: lofi, sdpa_prefill: hifi4}
232
+
233
+ | Model | Device | Top-1 (%) | Top-5 (%) | Speed (t/s/u) | TTFT (ms) |
234
+ |----------------|--------|-----------|-----------|---------------|-----------|
235
+ | Llama-3.2-1B | N300 | 88 | 98 | 98.2 | 45 |
236
+
237
+ ## precision_cfg = {ff1_3: bfp8, ff2: bfp8, wqkv: bfp8, wo: bfp4, kv_cache: bfp8, activation: bfp8}, fidelity_cfg = {li_ff1_3: hifi2, li_ff2: hifi2, li_qkv_decode: hifi2, li_o_decode: lofi, sdpa_decode: hifi2na, li_qkv_prefill: hifi2, li_o_prefill: lofi, sdpa_prefill: hifi4}
238
+
239
+ | Model | Device | Top-1 (%) | Top-5 (%) | Speed (t/s/u) | TTFT (ms) |
240
+ |----------------|--------|-----------|-----------|---------------|-----------|
241
+ | Llama-3.2-1B | N300 | 90 | 98 | 101.0 | 57 |
242
+
243
+ ## precision_cfg = {ff1_3: bfp8, ff2: bfp8, wqkv: bfp8, wo: bfp4, kv_cache: bfp8, activation: mixed}, fidelity_cfg = {li_ff1_3: hifi2, li_ff2: hifi2, li_qkv_decode: hifi2, li_o_decode: lofi, sdpa_decode: hifi2na, li_qkv_prefill: hifi2, li_o_prefill: lofi, sdpa_prefill: hifi4}
244
+
245
+ | Model | Device | Top-1 (%) | Top-5 (%) | Speed (t/s/u) | TTFT (ms) |
246
+ |----------------|--------|-----------|-----------|---------------|-----------|
247
+ | Llama-3.2-1B | N300 | 90 | 98 | 99.4 | 73 |
code/models/tt_transformers/README.md ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TT-Transformers
2
+
3
+ This code can run large language models such as the Llama3 family, Qwen2.5, Mistral, DeepSeek-R1-Distill variants and similar. Tensor-parallelism automatically distributes workloads across all available chips.
4
+
5
+ The current version is verified to work with the following models:
6
+ | Model | Hardware | <org/model> |
7
+ |--------------------------------------------------------------------------------------------------|-----------------------------|-------------------------------------------------|
8
+ | [DeepSeek R1 Distill Llama 70B](https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Llama-70B)| LoudBox / QuietBox / Galaxy | ```deepseek-ai/DeepSeek-R1-Distill-Llama-70B``` |
9
+ | [Llama 3.1 8B](https://huggingface.co/meta-llama/Llama-3.1-8B) | n150 / p100 / p150 | ```meta-llama/Llama-3.1-8B``` |
10
+ | [Llama 3.1 70B](https://huggingface.co/meta-llama/Llama-3.1-70B) | LoudBox / QuietBox / Galaxy | ```meta-llama/Llama-3.1-70B``` |
11
+ | [Llama 3.2 1B](https://huggingface.co/meta-llama/Llama-3.2-1B) | n150 | ```meta-llama/Llama-3.2-1B``` |
12
+ | [Llama 3.2 3B](https://huggingface.co/meta-llama/Llama-3.2-3B) | n150 | ```meta-llama/Llama-3.2-3B``` |
13
+ | [Llama 3.2 11B Vision](https://huggingface.co/meta-llama/Llama-3.2-11B-Vision) | n300 | ```meta-llama/Llama-3.2-11B-Vision``` |
14
+ | [Llama 3.2 90B Vision](https://huggingface.co/meta-llama/Llama-3.2-90B-Vision) | LoudBox / QuietBox | ```meta-llama/Llama-3.2-90B-Vision``` |
15
+ | [Mistral 7B Instruct v0.3](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.3) | n150 | ```mistralai/Mistral-7B-Instruct-v0.3``` |
16
+ | [Mistral Small 3.1 24B Instruct](https://huggingface.co/mistralai/Mistral-Small-3.1-24B-Instruct-2503) | T3K | ```mistralai/Mistral-Small-3.1-24B-Instruct-2503``` |
17
+ | [Mixtral 8x7B Instruct v0.1](https://huggingface.co/mistralai/Mixtral-8x7B-Instruct-v0.1) | LoudBox / QuietBox | ```mistralai/Mixtral-8x7B-Instruct-v0.1``` |
18
+ | [Qwen 2.5 7B](https://huggingface.co/Qwen/Qwen2.5-7B) | n300 | ```Qwen/Qwen2.5-7B``` |
19
+ | [Qwen 2.5 Coder 32B](https://huggingface.co/Qwen/Qwen2.5-Coder-32B) | LoudBox / QuietBox | ```Qwen/Qwen2.5-Coder-32B``` |
20
+ | [Qwen 2.5 72B](https://huggingface.co/Qwen/Qwen2.5-72B) | LoudBox / QuietBox | ```Qwen/Qwen2.5-72B``` |
21
+ | [Qwen 3 32B](https://huggingface.co/Qwen/Qwen3-32B) | LoudBox / QuietBox | ```Qwen/Qwen3-32B``` |
22
+ | [Phi-3-mini-128k-instruct](https://huggingface.co/microsoft/Phi-3-mini-128k-instruct) | n150 / n300 | ```microsoft/Phi-3-mini-128k-instruct``` |
23
+
24
+ <details>
25
+ <summary> Explore an extended list of compatible models (experimental) </summary>
26
+
27
+ | Model | Hardware | <org/model>. |
28
+ |---------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------------------------------------|
29
+ | [Bellatrix-Tiny-0.5B](https://huggingface.co/prithivMLmods/Bellatrix-Tiny-0.5B) | N150 | `prithivMLmods/Bellatrix-Tiny-0.5B` |
30
+ | [Bellatrix-Tiny-1B](https://huggingface.co/prithivMLmods/Bellatrix-Tiny-1B) | N150 | `prithivMLmods/Bellatrix-Tiny-1B` |
31
+ | [Bellatrix-Tiny-1B-R1](https://huggingface.co/prithivMLmods/Bellatrix-Tiny-1B-R1) | N150 | `prithivMLmods/Bellatrix-Tiny-1B-R1` |
32
+ | [deepseek-coder-6.7b-instruct](https://huggingface.co/deepseek-ai/deepseek-coder-6.7b-instruct) | N300 | `deepseek-ai/deepseek-coder-6.7b-instruct` |
33
+ | [deepseek-llm-7b-base](https://huggingface.co/deepseek-ai/deepseek-llm-7b-base) | N300 | `deepseek-ai/deepseek-llm-7b-base` |
34
+ | [deepseek-llm-7b-chat](https://huggingface.co/deepseek-ai/deepseek-llm-7b-chat) | N300 | `deepseek-ai/deepseek-llm-7b-chat` |
35
+ | [DeepSeek-R1-Distill-Llama-8B](https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Llama-8B) | N300 | `deepseek-ai/DeepSeek-R1-Distill-Llama-8B` |
36
+ | [DeepSeek-R1-Distill-Llama-8B](https://huggingface.co/unsloth/DeepSeek-R1-Distill-Llama-8B) | N300 | `unsloth/DeepSeek-R1-Distill-Llama-8B` |
37
+ | [DeepSeek-R1-Distill-Llama-8B-abliterated](https://huggingface.co/huihui-ai/DeepSeek-R1-Distill-Llama-8B-abliterated) | N300 | `huihui-ai/DeepSeek-R1-Distill-Llama-8B-abliterated` |
38
+ | [DeepSeek-R1-Distill-Qwen-1.5B](https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B) | N150 | `deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B` |
39
+ | [DeepSeek-R1-Distill-Qwen-32B](https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-32B) | T3K | `deepseek-ai/DeepSeek-R1-Distill-Qwen-32B` |
40
+ | [DeepSeek-R1-Distill-Qwen-32B-Japanese](https://huggingface.co/cyberagent/DeepSeek-R1-Distill-Qwen-32B-Japanese) | T3K | `cyberagent/DeepSeek-R1-Distill-Qwen-32B-Japanese` |
41
+ | [DeepSeek-R1-Distill-Qwen-32B-abliterated](https://huggingface.co/huihui-ai/DeepSeek-R1-Distill-Qwen-32B-abliterated) | T3K | `huihui-ai/DeepSeek-R1-Distill-Qwen-32B-abliterated` |
42
+ | [DeepSeek-R1-Distill-Qwen-7B](https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B) | N300 | `deepseek-ai/DeepSeek-R1-Distill-Qwen-7B` |
43
+ | [DeepSeek-R1-Distill-Qwen-7B-abliterated-v2](https://huggingface.co/huihui-ai/DeepSeek-R1-Distill-Qwen-7B-abliterated-v2) | N300 | `huihui-ai/DeepSeek-R1-Distill-Qwen-7B-abliterated-v2` |
44
+ | [DeepSeek-R1-Distill-Qwen-7B-Japanese](https://huggingface.co/lightblue/DeepSeek-R1-Distill-Qwen-7B-Japanese) | N300 | `lightblue/DeepSeek-R1-Distill-Qwen-7B-Japanese` |
45
+ | [Dobby-Mini-Leashed-Llama-3.1-8B](https://huggingface.co/SentientAGI/Dobby-Mini-Leashed-Llama-3.1-8B) | N300 | `SentientAGI/Dobby-Mini-Leashed-Llama-3.1-8B` |
46
+ | [Dobby-Mini-Unhinged-Llama-3.1-8B](https://huggingface.co/SentientAGI/Dobby-Mini-Unhinged-Llama-3.1-8B) | N300 | `SentientAGI/Dobby-Mini-Unhinged-Llama-3.1-8B` |
47
+ | [Dolphin3.0-Mistral-24B](https://huggingface.co/cognitivecomputations/Dolphin3.0-Mistral-24B) | T3K | `cognitivecomputations/Dolphin3.0-Mistral-24B` |
48
+ | [Dolphin3.0-R1-Mistral-24B](https://huggingface.co/cognitivecomputations/Dolphin3.0-R1-Mistral-24B) | T3K | `cognitivecomputations/Dolphin3.0-R1-Mistral-24B` |
49
+ | [EuroLLM-9B-Instruct](https://huggingface.co/utter-project/EuroLLM-9B-Instruct) | N300 | `utter-project/EuroLLM-9B-Instruct` |
50
+ | [GPT4chan-24B](https://huggingface.co/v2ray/GPT4chan-24B) | T3K | `v2ray/GPT4chan-24B` |
51
+ | [Llama-2-7b-chat-hf](https://huggingface.co/meta-llama/Llama-2-7b-hf) | N300 | `meta-llama/Llama-2-7b-hf` |
52
+ | [Llama-2-7b-hf](https://huggingface.co/meta-llama/Llama-2-7b-hf) | N300 | `meta-llama/Llama-2-7b-hf` |
53
+ | [Llama-3.1-8B-Instruct](https://huggingface.co/meta-llama/Llama-3.1-8B-Instruct) | N300 | `meta-llama/Llama-3.1-8B-Instruct` |
54
+ | [Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct) | N150 | `meta-llama/Llama-3.2-1B-Instruct` |
55
+ | [Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct) | N150 | `meta-llama/Llama-3.2-3B-Instruct` |
56
+ | [Llama-3.2-6B-AlgoCode](https://huggingface.co/prithivMLmods/Llama-3.2-6B-AlgoCode) | N150 | `prithivMLmods/Llama-3.2-6B-AlgoCode` |
57
+ | [Llama-Krikri-8B-Base](https://huggingface.co/ilsp/Llama-Krikri-8B-Base) | N150 | `ilsp/Llama-Krikri-8B-Base` |
58
+ | [Llama-Krikri-8B-Instruct](https://huggingface.co/ilsp/Llama-Krikri-8B-Instruct) | N300 | `ilsp/Llama-Krikri-8B-Instruct` |
59
+ | [LwQ-10B-Instruct](https://huggingface.co/prithivMLmods/LwQ-10B-Instruct) | N300 | `prithivMLmods/LwQ-10B-Instruct` |
60
+ | [Magnum-v4-Cydonia-vXXX-22B](https://huggingface.co/Kaoeiri/Magnum-v4-Cydonia-vXXX-22B) | T3K | `Kaoeiri/Magnum-v4-Cydonia-vXXX-22B` |
61
+ | [Megatron-Opus-7B-Exp](https://huggingface.co/prithivMLmods/Megatron-Opus-7B-Exp) | N300 | `prithivMLmods/Megatron-Opus-7B-Exp` |
62
+ | [Meta-Llama-3-8B](https://huggingface.co/meta-llama/Meta-Llama-3-8B) | N300 | `meta-llama/Meta-Llama-3-8B` |
63
+ | [Meta-Llama-3-8B-Instruct](https://huggingface.co/meta-llama/Meta-Llama-3-8B-Instruct) | N300 | `meta-llama/Meta-Llama-3-8B-Instruct` |
64
+ | [Mistral-7B-Instruct-v0.1](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.1) | N150 | `mistralai/Mistral-7B-Instruct-v0.1` |
65
+ | [Mistral-7B-Instruct-v0.2](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.2) | N150 | `mistralai/Mistral-7B-Instruct-v0.2` |
66
+ | [Mistral-7B-v0.1](https://huggingface.co/mistralai/Mistral-7B-v0.1) | N150 | `mistralai/Mistral-7B-v0.1` |
67
+ | [Mistral-Small-24B-Base-2501](https://huggingface.co/mistralai/Mistral-Small-24B-Base-2501) | T3K | `mistralai/Mistral-Small-24B-Base-2501` |
68
+ | [Mistral-Small-24B-Instruct-2501](https://huggingface.co/mistralai/Mistral-Small-24B-Instruct-2501) | T3K | `mistralai/Mistral-Small-24B-Instruct-2501` |
69
+ | [Mistral-Small-3.1-24B-Instruct-2503](https://huggingface.co/mistralai/Mistral-Small-3.1-24B-Instruct-2503) | T3K | `mistralai/Mistral-Small-3.1-24B-Instruct-2503` |
70
+ | [NuminaMath-7B-TIR](https://huggingface.co/AI-MO/NuminaMath-7B-TIR) | N300 | `AI-MO/NuminaMath-7B-TIR` |
71
+ | [OpenThinker-7B](https://huggingface.co/open-thoughts/OpenThinker-7B) | N300 | `open-thoughts/OpenThinker-7B` |
72
+ | [Primal-Mini-3B-Exp](https://huggingface.co/prithivMLmods/Primal-Mini-3B-Exp) | N150 | `prithivMLmods/Primal-Mini-3B-Exp` |
73
+ | [Qwen2.5-0.5B](https://huggingface.co/Qwen/Qwen2.5-0.5B) | N150 | `Qwen/Qwen2.5-0.5B` |
74
+ | [Qwen2.5-0.5B-Instruct](https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct) | N150 | `Qwen/Qwen2.5-0.5B-Instruct` |
75
+ | [Qwen2.5-1.5B](https://huggingface.co/Qwen/Qwen2.5-1.5B) | N150 | `Qwen/Qwen2.5-1.5B` |
76
+ | [Qwen2.5-1.5B-Instruct](https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct) | N150 | `Qwen/Qwen2.5-1.5B-Instruct` |
77
+ | [Qwen2.5-3B-Instruct](https://huggingface.co/Qwen/Qwen2.5-3B-Instruct) | N150 | `Qwen/Qwen2.5-3B-Instruct` |
78
+ | [Qwen2.5-32B](https://huggingface.co/Qwen/Qwen2.5-32B) | T3K | `Qwen/Qwen2.5-32B` |
79
+ | [Qwen2.5-Coder-7B-Instruct](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct) | N300 | `Qwen/Qwen2.5-Coder-7B-Instruct` |
80
+ | [Qwen2.5-Math-1.5B](https://huggingface.co/Qwen/Qwen2.5-Math-1.5B) | N150 | `Qwen/Qwen2.5-Math-1.5B` |
81
+ | [Qwen2.5-Math-7B](https://huggingface.co/Qwen/Qwen2.5-Math-7B) | N300 | `Qwen/Qwen2.5-Math-7B` |
82
+ | [Reasoning-Distilled-ta-7B](https://huggingface.co/prithivMLmods/Reasoning-Distilled-ta-7B) | N300 | `prithivMLmods/Reasoning-Distilled-ta-7B` |
83
+ | [s1-32B](https://huggingface.co/simplescaling/s1-32B) | T3K | `simplescaling/s1-32B` |
84
+ | [Selene-1-Mini-Llama-3.1-8B](https://huggingface.co/AtlaAI/Selene-1-Mini-Llama-3.1-8B) | N300 | `AtlaAI/Selene-1-Mini-Llama-3.1-8B` |
85
+ | [Sky-T1-32B-Preview](https://huggingface.co/NovaSky-AI/Sky-T1-32B-Preview) | T3K | `NovaSky-AI/Sky-T1-32B-Preview` |
86
+ | [SmallThinker-3B-Preview](https://huggingface.co/PowerInfer/SmallThinker-3B-Preview) | N150 | `PowerInfer/SmallThinker-3B-Preview` |
87
+ | [SmolLM2-1.7B-Instruct](https://huggingface.co/HuggingFaceTB/SmolLM2-1.7B-Instruct) | N150 | `HuggingFaceTB/SmolLM2-1.7B-Instruct` |
88
+ | [Sqweeks-7B-Instruct](https://huggingface.co/prithivMLmods/Sqweeks-7B-Instruct) | N300 | `prithivMLmods/Sqweeks-7B-Instruct` |
89
+ | [TinyLlama-1.1B-Chat-v1.0](https://huggingface.co/TinyLlama/TinyLlama-1.1B-Chat-v1.0) | N150 | `TinyLlama/TinyLlama-1.1B-Chat-v1.0` |
90
+ | [TinySwallow-1.5B](https://huggingface.co/SakanaAI/TinySwallow-1.5B) | N150 | `SakanaAI/TinySwallow-1.5B` |
91
+ | [TinySwallow-1.5B-Instruct](https://huggingface.co/SakanaAI/TinySwallow-1.5B-Instruct) | N150 | `SakanaAI/TinySwallow-1.5B-Instruct` |
92
+ | [Triangulum-v2-10B](https://huggingface.co/prithivMLmods/Triangulum-v2-10B) | N300 | `prithivMLmods/Triangulum-v2-10B` |
93
+ | [WebMind-7B-v0.1](https://huggingface.co/prithivMLmods/WebMind-7B-v0.1) | N300 | `prithivMLmods/WebMind-7B-v0.1` |
94
+ | [WizardLM-7B-Uncensored](https://huggingface.co/cognitivecomputations/WizardLM-7B-Uncensored) | N300 | `cognitivecomputations/WizardLM-7B-Uncensored` |
95
+ | [Xwen-7B-Chat](https://huggingface.co/xwen-team/Xwen-7B-Chat) | N300 | `xwen-team/Xwen-7B-Chat` |
96
+ | [YuE-s1-7B-anneal-en-cot](https://huggingface.co/m-a-p/YuE-s1-7B-anneal-en-cot) | N300 | `m-a-p/YuE-s1-7B-anneal-en-cot` |
97
+ | [YuE-s2-1B-general](https://huggingface.co/m-a-p/YuE-s2-1B-general) | N150 | `m-a-p/YuE-s2-1B-general` |
98
+
99
+ </details>
100
+
101
+ ## Prerequisites
102
+
103
+ Install [TT-Metalium and TTNN](../../INSTALLING.md).
104
+
105
+
106
+ ## How to Run
107
+
108
+ To configure the weights and run a demo, choose [Automatic Download](#automatic-download) for a quick setup or [Manual Download](#manual-download) for more control over the files.
109
+
110
+ ---
111
+
112
+ ### Automatic Setup
113
+
114
+ #### Login to HuggingFace using your token
115
+
116
+ Use `huggingface-cli login` or set the token with the command `export HF_TOKEN=<token>`
117
+ - To obtain a HuggingFace token visit: https://huggingface.co/docs/hub/security-tokens
118
+
119
+ #### Choose your Model
120
+
121
+ Set the `HF_MODEL` environment variable to the HuggingFace org/name of the model to be run.
122
+
123
+ This will automatically download the weights into your HuggingFace cache directory and run the model directly.
124
+
125
+ - Check the models chart on the top of the page and substitute the `<org/model>` on the following command:
126
+ ```
127
+ export HF_MODEL=<org/name>
128
+ ```
129
+
130
+ #### You are all set!
131
+
132
+ - Skip to section: [Run the Demo](#run-the-demo)
133
+
134
+ ---
135
+
136
+ ### Manual Setup
137
+
138
+ If you wish, you can manually download the weights [from HuggingFace](#download-weights-from-huggingface) as described by the following section:
139
+
140
+ #### Download Weights from HuggingFace
141
+
142
+ Navigate to [HuggingFace](https://huggingface.co) and download the weights of the model you wish to run (check [models chart](#tt-transformers) for compatibility and direct links to HuggingFace models).
143
+
144
+ - Ensure your model directory has the following structure:
145
+ ```
146
+ /path_to/org/model/
147
+ config.json
148
+ generation_config.json
149
+ model-00001-of-00062.safetensors
150
+ ...
151
+ ```
152
+
153
+ - Set `HF_MODEL` to the directory of the downloaded weights:
154
+ ```
155
+ export HF_MODEL=<path_to_downloaded_directory>
156
+ ```
157
+
158
+ #### You are all set!
159
+
160
+ - Skip to section: [Run the Demo](#run-the-demo)
161
+
162
+ ---
163
+
164
+ ### Run the Demo
165
+
166
+ - Make sure `HF_MODEL` has been set:
167
+
168
+ `export HF_MODEL=<hf_model_name or hf_downloaded_directory>`
169
+
170
+ - Run the Demo
171
+
172
+ These are some example commands on how to run the demo. For more information about the parameters, check [parameters description.](#parameters-description)
173
+
174
+ ```
175
+ # Batch-1
176
+ pytest models/tt_transformers/demo/simple_text_demo.py -k "performance and batch-1"
177
+
178
+ # Batch-32
179
+ pytest models/tt_transformers/demo/simple_text_demo.py -k "performance and batch-32"
180
+
181
+ # Long context with custom parameters
182
+ pytest models/tt_transformers/demo/simple_text_demo.py -k "long-context" --max_seq_len=16384
183
+
184
+ # Long-context
185
+ pytest models/tt_transformers/demo/simple_text_demo.py -k "performance and long"
186
+ ```
187
+
188
+ The above examples are run in `ModelOptimizations.performance` mode. You can override this by setting the `optimizations` or the `decoder_config_file` argument in the demo. To use instead the accuracy mode you can call the above tests with `-k "accuracy and ..."` instead of performance.
189
+
190
+ NOTE: trace region sizes are declared in [`models/model_trace_region_sizes.yaml`](../model_trace_region_sizes.yaml) and resolved at device-open time via [`get_supported_trace_region_size`](demo/trace_region_config.py) (which delegates to [`resolve_trace_region_size`](../demos/utils/trace_region_sizes.py)). A `(model, SKU)` pair without a YAML entry is not an error: resolution logs an info message and falls back to `TRACE_REGION_SIZE_DYNAMIC` (`0`, dynamic allocation). Add an explicit entry when a model needs a fixed reserved trace region.
191
+
192
+ ## Details
193
+
194
+ ### Extra compatibility settings for non-Llama models
195
+
196
+ If you are bringing up a new model that is similar to these but is not listed above, you may also need to set additional environment variables:
197
+ - `MAX_PREFILL_CHUNK_SIZE` - this determines how many thousands of tokens are prefilled in one go. For optimal performance pick 128. Depending on the model dimensions and hardware you're running on, there may not be enough L1 to prefill 128K tokens at once, in which case you can reduce this in powers of 2 down to 4.
198
+ - `PAD_MLP_CORES` - models with a hidden_dim that is not a nice power of 2 may not have a valid layout or may run with lower performance. You can set this to a multiple of 8 between 8 and 64; `16` and `32` commonly work well if this is required.
199
+
200
+ You should also watch out for:
201
+ - RoPE encoding style. `llama3`, `yarn` and of course `none` are supported. HuggingFace models encode the complex numbers in RoPE as r1, r2, ..., i1, i2, ... whereas Meta models encode the complex numbers as r1, i1, r2, i2, ... - TTT uses a Meta-style implementation of the RoPE op and when loading a HuggingFace it will reshuffle the weights of the pre-rope attention weights to interleave their outputs in this style (see `reverse_permute` in [load_checkpoints.py](tt/load_checkpoints.py)). When _using_ TTT this happens invisibly and should not affect you, but when bringing up a new model or modifying TTT being aware of this will make some otherwise confusing things clearer.
202
+ - Our [accuracy test](demo/simple_text_demo.py -k ci-token-matching) will require you to [generate some reference logits](tests/generate_reference_hf.py) and perhaps update the test to use them.
203
+ - We parallelise attention over the number of heads. If this number is e.g. 14 then you will not be able to run it on more than 2 chips (because 14/2=7, a prime number). We do not support head-padding or similar mitigations at this time but a PR would be cool.
204
+
205
+ Huggingface models specify their architecture in the `config.json` file. The following architectures are known to work:
206
+
207
+ - LlamaForCausalLM
208
+ - Qwen2ForCausalLM
209
+ - Qwen3ForCausalLM
210
+ - MistralForCausalLM
211
+ - Mistral3ForConditionalGeneration
212
+ - Phi3ForCausalLM
213
+
214
+ At the time of writing this covers the majority of popular HuggingFace text-generation models. If you find another architecture that works or extend TT-Transformers to support one we would love to accept a PR!
215
+
216
+ ---
217
+
218
+ ### Environment Variables Description
219
+
220
+ - `HF_MODEL` is the HuggingFace org/name of the model you want to run or the path to the downloaded Huggingface weights.
221
+ - `TT_CACHE_PATH` is optional. It sets the path for ttnn's weight cache files. See below for more details.
222
+ - `MESH_DEVICE` is optional. It allows you to use fewer devices than are available. See below for more details.
223
+
224
+ On the first execution of each model, TTNN will create weight cache files for that model, to speed up future runs. These cache files only need to be created once for each model and device. These files are stored in one of three places:
225
+
226
+ 1. `TT_CACHE_PATH` if you have set it.
227
+ 2. `HF_MODEL/device_name` if a path to downloaded weights was specified using `HF_MODEL`.
228
+ 3. `model_cache/HF_MODEL/device_name` if a HuggingFace model name was specified using `HF_MODEL`.
229
+
230
+ The device name used is:
231
+
232
+ - `N150` for N150
233
+ - `N300` for N300
234
+ - `T3K` for LoudBox / QuietBox
235
+ - `TG` for Galaxy
236
+
237
+ By default tensor parallelism is used to run the model over all available chips. You can instead run on a smaller mesh either for testing or for performance reasons (for very small models the communication overhead of tensor parallelism may be larger than the performance gained). To use a smaller mesh, set `MESH_DEVICE` to one of the supported devices: `N150`, `N300`, `T3K` or `TG`.
238
+
239
+ Example: `export MESH_DEVICE=N150`, will enable running one a single chip of a multi-chip system.
240
+
241
+ ---
242
+
243
+ ### Parameters Description
244
+
245
+ The `simple_text_demo.py` script includes the following main modes of operation and is parametrized to support other configurations.
246
+
247
+ - `batch-1`: Runs a small prompt (128 tokens) for a single user
248
+ - `batch-32`: Runs a small prompt (128 tokens) for a a batch of 32 users
249
+ - `long-context`: Runs a large prompt (64k tokens) for a single user
250
+ - `reasoning-1`: Runs a reasoning prompt for a single user (generates up to 15k tokens)
251
+
252
+ If you want to provide your own demo configuration, please take a look at the pytest parametrize calls in `models/tt_transformers/demo/simple_text_demo.py`. For convenience we list all the supported params below:
253
+
254
+ - `input_prompts (string)`: input json file with prompts to process. See `models/tt_transformers/demo/*.json` for a list of input files
255
+ - `instruct (bool)`: Whether to use Llama instruct weights or general weights
256
+ - `repeat_batches (int)`: Number of consecutive batches of users to run (default: 1)
257
+ - `max_seq_len (int)`: Maximum context length supported by the model (refer to the table above)
258
+ - `batch_size (int)`: Number of users in a batch (Supports 1/2/4/8/16/32 batches)
259
+ - `max_generated_tokens (int)`: Maximum number of tokens to generate for each user (Note that the users will stop generation before this limit if they reach a eos token)
260
+ - `paged_attention (bool)`: Whether to use paged attention or default attention (vLLM support (WIP) requires paged attention)
261
+ - `page_params (dict)`: Page parameters for paged attention - [`block_size`, `max_num_blocks`]. For smaller context lengths use `block_size=32` and `max_num_blocks=1024`, for larger context use block_size=64 and max_num_blocks=2048
262
+ - `sampling_params (dict)`: Sampling parameters for decoding -[`temperature`, `top_p`]. If temperature is set to 0, argmax (greedy decode) is used.
263
+ - `stop_at_eos (bool)`: Flag to stop decoding when the model generates an EoS token
264
+ - `optimizations (ModelOptimizations)`: Optimization level to use for the model [`accuracy`, `performance`]. Applied uniformly across all decoders unless an override config exists in `models/tt_transformers/model_params/<model-name>`
265
+ - `decoder_config_file (DecodersPrecision)`: Fine-grained optimization control that allows specifying a configuration file to set different settings for each decoder.
266
+
267
+ Please note that using `argmax` with `batch_size > 1` or using `top-p` sampling with any batch size, these ops will be run on host. This is because those ops are not yet fully supported on device. A decrease in performance is expected when these configurations are enabled.
268
+
269
+ ---
270
+
271
+ ### Optimization overrides
272
+ Some models require a unique set of optimizations defined in `models/tt_transformers/model_params/<model-name>`. To override the default optimizations, you can define files named `models/tt_transformers/tt/model_config/PERFORMANCE_DECODER_CONFIG_FILENAME` and `models/tt_transformers/tt/model_config/ACCURACY_DECODER_CONFIG_FILENAME` in the appropriate `models/tt_transformers/model_params/<model-name>` directory to override the `ModelOptimizations.performance` and `ModelOptimizations.accuracy` optimizations respectively. For example, to override the default "performance" optimizations for Llama3.1-8B-Instruct, a file named `performance_decoder_config.json` has been created in the `models/tt_transformers/model_params/Llama3.1-8B-Instruct` directory. The content to write in override files is described in [the custom optimizations section](#custom-optimizations). Optimizations are applied with the following prioritization:
273
+ 1. from override config (if it exists)
274
+ 2. from the `optimizations` argument
275
+
276
+ ---
277
+
278
+ ### Custom input arguments
279
+ To facilitate testing different configurations, `simple_text_demo.py` supports argument overrides. The full list of overrides is included in `models/tt_transformers/demo/conftest.py`.
280
+
281
+ An example usage where the `batch-1` test is modified to run with 16 users and keep generating tokens until 1024 are generated:
282
+
283
+ ```
284
+ pytest models/tt_transformers/demo/simple_text_demo.py -k "performance and batch-1" --batch_size 16 --max_generated_tokens 1024 --stop_at_eos 0
285
+ ```
286
+
287
+ ---
288
+
289
+ ### Custom optimizations
290
+ To apply the same settings across all decoders, the `optimizations` argument can be used. `optimizations` offers a wide range of configurations for precision and math fidelity. The user can override the configurations of the data types of the weight tensors and activation tensors and the math fidelity of the kernels that works on those tensors, using the `--optimizations` argument on the command line. For example:
291
+
292
+ ```
293
+ pytest models/tt_transformers/demo/simple_text_demo.py -k "accuracy and batch-1" --optimizations 'precision_cfg = {ff1_3: bfp4, ff2: bfp4, wqkv: bfp8, wo: bfp8, kv_cache: bfp8, activation: mixed}, fidelity_cfg = {li_ff1_3: hifi2, li_ff2: lofi, li_qkv_decode: hifi2, li_o_decode: hifi2, sdpa_decode: hifi2na, li_qkv_prefill: hifi2, li_o_prefill: hifi2fp16, sdpa_prefill: hifi4}'
294
+ ```
295
+
296
+ Please refer to [model_config.py](models/tt_transformers/tt/model_config.py) for the full list of supported key-value pairs in the `--optimizations` argument. Centralized performance and accuracy targets are defined in [models/model_targets.yaml](../model_targets.yaml). The `lt` utility can still export markdown snapshots for local Pareto analysis.
297
+
298
+ To apply non-uniform settings across the decoders, the user can provide a JSON file using the `decoder_config_file` argument to specify the configuration for each decoder. For example
299
+
300
+ ```
301
+ pytest models/tt_transformers/demo/simple_text_demo.py -k "performance and batch-1" --decoder_config_file 'models/tt_transformers/demo/config_16_decoders.json'
302
+ ```
303
+
304
+ When a component is not specified (e.g., FF2 is missing for decoder 2 in `models/tt_transformers/demo/config_16_decoders.json`), the baseline configuration is used for that component.
305
+
306
+ ---
307
+
308
+ ### Expected performance and accuracy
309
+
310
+ See [models/model_targets.yaml](../model_targets.yaml) for expected performance and accuracy targets across supported configurations.
311
+ Accuracy of the network architectures is measured by exact token matching using teacher forcing method. During inference the previous token is replaced by the ground truth token while the network generates the next token. This allows to avoid accumulating errors when comparisons on a finer level (tokens) assessed in comparison to other known metrics that compare quality and context of the answer. Token accuracy can be reported by passing the argument shown below:
312
+
313
+ ```
314
+ pytest models/tt_transformers/demo/simple_text_demo.py -k "performance and batch-1" --token_accuracy True
315
+ ```
316
+
317
+ ---
318
+
319
+ ### Implementation notes
320
+
321
+ **Chunked prefill (text-only)**: All of the compatible model/device combinations support a max prefill context-length of 128k, with the exception of Llama3.1-8B and Llama3.2-11B on N150 which have a max of 32k (due to a lack of memory). To support these large max context-lengths, chunked prefill is performed with different max chunk sizes as shown in the table below.
322
+
323
+ Max Prefill Chunk Sizes (text-only):
324
+ | | N150 | N300 | T3K | TG |
325
+ |--------------|---------------|---------------|----------------|-------------|
326
+ | Llama3.2-1B | 128k tokens | 128k tokens | 128k tokens | 128k tokens |
327
+ | Llama3.2-3B | 8k tokens | 128k tokens | 128k tokens | 128k tokens |
328
+ | Qwen2.5-7B | 4k tokens | 32k tokens | 128k tokens | 128k tokens |
329
+ | Llama3.1-8B | 4k tokens | 64k tokens | 128k tokens | 128k tokens |
330
+ | Llama3.2-11B | 4k tokens | 64k tokens | 128k tokens | 128k tokens |
331
+ | Mistral-Small-3.1-24B | 8k tokens | 128k tokens | 128k tokens | 128k tokens |
332
+ | Llama3.1-70B | Not supported | Not supported | 32k tokens | 128k tokens |
333
+ | Llama3.2-90B | Not supported | Not supported | 32k tokens | Not supported |
334
+ | DeepSeek-R1-Distill-Llama3.3-70B | Not supported | Not supported | 32k tokens | 128k tokens |
335
+
336
+
337
+ - These max chunk sizes are specific to max context length 128k and are configured via `MAX_PREFILL_CHUNK_SIZES_DIV1024` in [model_config.py](https://github.com/tenstorrent/tt-metal/blob/main/models/demos/llama3/tt/model_config.py). If the max context length is set to a smaller value using the `max_seq_len` flag (see [Run the demo](#run-the-demo)), these chunk sizes can possibly be increased due to using a smaller KV cache.
338
+
339
+ **Chunked prefill (Llama3.2-11B multimodal)**: Llama3.2-11B multimodal is currently only supported on N300 and T3000. On N300, a max prefill context length of 8k is supported, while T3000 supports a max context length of 128k.
340
+
341
+ **Chunked prefill (Mistral-Small-3.1-24B multimodal)**: Mistral-Small-3.1-24B-Instruct-2503 (Pixtral vision) is currently supported on T3000. On T3000, a max prefill context length of 128k is supported.
342
+
343
+ ---
344
+
345
+ ### Memory Optimization
346
+
347
+ #### HuggingFace Model Caching Control
348
+
349
+ To help manage memory usage, you can control whether the HuggingFace model is cached in memory using the `cache_hf` parameter via command line or code:
350
+
351
+ ```python
352
+ # Default: disables caching to conserve memory usage
353
+ model_args = ModelArgs(
354
+ mesh_device,
355
+ cache_hf=False, # Default: Reduces memory usage by not keeping HF model in memory
356
+ max_batch_size=1,
357
+ max_seq_len=2048
358
+ )
359
+
360
+ # Optional: enables caching for faster repeated access
361
+ model_args = ModelArgs(
362
+ mesh_device,
363
+ cache_hf=True, # Cache HF model for better performance running reference tests
364
+ max_batch_size=4,
365
+ max_seq_len=4096
366
+ )
367
+ ```
368
+
369
+ **When to disable caching (`cache_hf=False`):**
370
+ - Running on systems with limited memory (< 256GB)
371
+ - Loading large models (70B+ parameters)
372
+ - Using the model for single inference runs
373
+ - When you don't need reference model comparisons
374
+
375
+ **When to keep caching enabled (`cache_hf=True`, default):**
376
+ - Sufficient memory available
377
+ - Comparisons with torch model is needed
378
+ - Minimizing test duration is prioritized over memory usage
379
+ - Running reference model tests
380
+
381
+ The `cache_hf` parameter affects:
382
+ - `load_state_dict()` method: Controls whether HF model is cached after loading
383
+ - `reference_transformer()` method: Controls whether to reuse cached model or load fresh
384
+
385
+ **Memory Impact:**
386
+ - Disabling caching saves approximately the full model size in memory
387
+ - For a 70B model, this can save ~140GB+ of memory usage
388
+ - Increased test duration as model needs to be reloaded for reference operations
code/models/tt_transformers/conftest.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ import os
5
+
6
+ import pytest
7
+
8
+ import ttnn
9
+
10
+
11
+ @pytest.fixture
12
+ def device_params(request, galaxy_type):
13
+ # Get param dict passed in from test parametrize (or default to empty dict).
14
+ # Any TRACE_MODEL_KEY_PARAM is left in place; the mesh_device fixture resolves it
15
+ # to trace_region_size using the logical submesh SKU.
16
+ params = getattr(request, "param", {}).copy()
17
+
18
+ mesh_device = {"N150": (1, 1), "N300": (1, 2), "N150x4": (1, 4), "T3K": (1, 8), "TG": (8, 4), "P150x8": (1, 8)}.get(
19
+ os.environ.get("MESH_DEVICE"), len(ttnn.get_device_ids())
20
+ )
21
+ is_single_device = (mesh_device == (1, 1)) if isinstance(mesh_device, tuple) else (mesh_device == 1)
22
+
23
+ if "fabric_config" in params:
24
+ if is_single_device:
25
+ params["fabric_config"] = None
26
+ elif params["fabric_config"] == True:
27
+ cluster_type = ttnn.cluster.get_cluster_type()
28
+ if cluster_type == ttnn.cluster.ClusterType.BLACKHOLE_GALAXY:
29
+ # The 8x4 decode path uses Ring collectives along both mesh axes.
30
+ params["fabric_config"] = ttnn.FabricConfig.FABRIC_2D_TORUS_XY
31
+ else:
32
+ params["fabric_config"] = (
33
+ ttnn.FabricConfig.FABRIC_1D_RING if galaxy_type == "6U" else ttnn.FabricConfig.FABRIC_1D
34
+ )
35
+
36
+ return params
code/models/tt_transformers/host_mem_profiler.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """
6
+ host_mem_profiler.py — Host-side memory profiler for model tests
7
+ ===============================================================
8
+
9
+ What this script does
10
+ ---------------------
11
+ Wraps a pytest run with `memory_profiler` to record the host process
12
+ memory (RSS) at 100 ms intervals while the model test executes.
13
+ After the run it produces a PNG plot showing memory over time with a
14
+ peak-memory reference line. The script exits with pytest's own return
15
+ code, so CI pipelines see failures correctly and no plot is saved for a
16
+ failed run.
17
+
18
+ Measured memory includes all child processes spawned by the test
19
+ (e.g. the TT device runtime), giving a realistic view of total host
20
+ footprint during inference.
21
+
22
+ Dependencies
23
+ ------------
24
+ `memory-profiler` and `matplotlib` are auto-installed at startup if
25
+ absent. Everything else must already be present in the active
26
+ environment (loguru, pytest, and the tt-metal Python stack).
27
+
28
+ pip install memory-profiler matplotlib # manual one-time install
29
+
30
+ Arguments
31
+ ---------
32
+ --mesh-device Value forwarded as the MESH_DEVICE environment variable
33
+ to the test process (e.g. N150, N300, T3K, TG).
34
+ Default: N150
35
+ --hf-model Hugging Face model ID forwarded as HF_MODEL.
36
+ Default: meta-llama/Llama-3.2-1B-Instruct
37
+ --test Pytest target — a file path, directory, or node id.
38
+ Default: models/tt_transformers/demo/simple_text_demo.py
39
+ -k Optional pytest -k filter expression to select a subset
40
+ of tests within the target.
41
+
42
+ Output
43
+ ------
44
+ A timestamped directory is created under profiling_results/:
45
+
46
+ profiling_results/<name>_<YYYYMMDD_HHMMSS>/
47
+ memory_profile.png ← time-series plot (peak annotated in red)
48
+
49
+ The script exits with pytest's return code; no plot is saved on failure.
50
+
51
+ Usage examples
52
+ --------------
53
+ # Minimal — profile the default Llama-3.2-1B demo on a single N150 card:
54
+ python host_mem_profiler.py
55
+
56
+ # Llama-3.1-8B on a T3K (8-chip) host, full test file:
57
+ python host_mem_profiler.py \\
58
+ --mesh-device T3K \\
59
+ --hf-model meta-llama/Llama-3.1-8B-Instruct \\
60
+ --test models/tt_transformers/demo/simple_text_demo.py
61
+
62
+ # Same model but only the decode token accuracy test case:
63
+ python host_mem_profiler.py \\
64
+ --mesh-device T3K \\
65
+ --hf-model meta-llama/Llama-3.1-8B-Instruct \\
66
+ --test models/tt_transformers/demo/simple_text_demo.py \\
67
+ -k performance-ci-token-mathcing
68
+ """
69
+
70
+ import argparse
71
+ import importlib
72
+ import os
73
+ import subprocess
74
+ import sys
75
+ from datetime import datetime
76
+ from pathlib import Path
77
+
78
+ from loguru import logger
79
+
80
+ _ALLOWED_PACKAGES = {"memory-profiler", "matplotlib"}
81
+
82
+
83
+ def _require(package, import_name=None):
84
+ if package not in _ALLOWED_PACKAGES:
85
+ raise ValueError(f"Package '{package}' is not in the allowed list: {_ALLOWED_PACKAGES}")
86
+ import_name = import_name or package
87
+ try:
88
+ importlib.import_module(import_name)
89
+ except ImportError:
90
+ print(f"Installing missing dependency: {package}")
91
+ subprocess.check_call([sys.executable, "-m", "pip", "install", package])
92
+ importlib.invalidate_caches() # flush finder cache so the new package is visible
93
+
94
+
95
+ _require("memory-profiler", "memory_profiler")
96
+ _require("matplotlib")
97
+
98
+ import matplotlib.pyplot as plt
99
+ from memory_profiler import memory_usage
100
+
101
+ PYTEST_EXTRA_ARGS = ["-v", "-s"]
102
+ OUTPUT_ROOT = Path("profiling_results")
103
+
104
+
105
+ # ── Runner ────────────────────────────────────────────────────────────────────
106
+ def make_runner(model_cfg, k_filter=None):
107
+ """Return a zero-arg callable that runs a model's test suite."""
108
+
109
+ def run():
110
+ env = os.environ.copy()
111
+ env.update(model_cfg["env"])
112
+ cmd = [sys.executable, "-m", "pytest", model_cfg["test"], *PYTEST_EXTRA_ARGS]
113
+ if k_filter:
114
+ cmd += ["-k", k_filter]
115
+ result = subprocess.run(cmd, capture_output=False, env=env)
116
+ return result.returncode
117
+
118
+ return run
119
+
120
+
121
+ # ── Plot ──────────────────────────────────────────────────────────────────────
122
+ def plot_results(name, elapsed, mib, out_dir):
123
+ fig, ax = plt.subplots(figsize=(14, 6))
124
+ ax.plot(elapsed, mib, "+-k", linewidth=1.0, markersize=4, label=name)
125
+ ax.axhline(max(mib), color="red", linestyle="--", linewidth=0.8, label=f"Peak: {max(mib):.1f} MiB")
126
+
127
+ ax.set_xlabel("Time (s)")
128
+ ax.set_ylabel("Memory (MiB)")
129
+ ax.set_title("Memory Usage Over Time")
130
+ ax.legend()
131
+ fig.tight_layout()
132
+ fig.savefig(os.path.join(out_dir, "memory_profile.png"), dpi=150)
133
+ plt.close(fig)
134
+ logger.info(f"Saved at {os.path.join(out_dir, 'memory_profile.png')}")
135
+
136
+
137
+ # ── Main ──────────────────────────────────────────────────────────────────────
138
+ if __name__ == "__main__":
139
+ parser = argparse.ArgumentParser(description="Profile host-side memory usage of a model test")
140
+ parser.add_argument("--mesh-device", default="N150", help="MESH_DEVICE value (e.g. N150, T3K)")
141
+ parser.add_argument("--hf-model", default="meta-llama/Llama-3.2-1B-Instruct", help="Hugging Face model ID")
142
+ parser.add_argument("--test", default="models/tt_transformers/demo/simple_text_demo.py", help="Pytest target path")
143
+ parser.add_argument("-k", default=None, help="pytest -k filter expression")
144
+ args = parser.parse_args()
145
+
146
+ name = args.hf_model.split("/")[-1] if "/" in args.hf_model else args.hf_model
147
+ if args.k:
148
+ name = f"{name}_{args.k}"
149
+
150
+ model = {
151
+ "name": name,
152
+ "env": {
153
+ "MESH_DEVICE": args.mesh_device,
154
+ "HF_MODEL": args.hf_model,
155
+ },
156
+ "test": args.test,
157
+ }
158
+
159
+ logger.info(f"\n{'='*60}\nProfiling: {model['name']}\n{'='*60}")
160
+
161
+ mem_ts, returncode = memory_usage(
162
+ (make_runner(model, args.k), [], {}),
163
+ interval=0.1,
164
+ retval=True,
165
+ timestamps=True,
166
+ include_children=True,
167
+ )
168
+ if returncode:
169
+ logger.error(f"pytest exited with code {returncode} — skipping plot")
170
+ sys.exit(returncode)
171
+
172
+ mib = [m for m, _ in mem_ts]
173
+ ts = [t for _, t in mem_ts]
174
+ t0 = ts[0]
175
+ elapsed = [t - t0 for t in ts]
176
+
177
+ logger.info(f" Peak: {max(mib):.1f} MiB")
178
+ logger.info(f" Baseline: {min(mib):.1f} MiB")
179
+
180
+ safe_name = name.replace("/", "_").replace(" ", "_")
181
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
182
+ output_root = OUTPUT_ROOT.resolve()
183
+ out_dir = (output_root / f"{safe_name}_{timestamp}").resolve()
184
+ if not out_dir.is_relative_to(output_root):
185
+ raise ValueError(f"Refusing to write outside output root: {out_dir}")
186
+ out_dir.mkdir(parents=True, exist_ok=True)
187
+
188
+ plot_results(model["name"], elapsed, mib, out_dir)
code/models/tt_transformers/scripts/op_perf_results.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2023 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ import csv
5
+ from argparse import ArgumentParser
6
+ from collections import defaultdict
7
+
8
+
9
+ def main():
10
+ parser = ArgumentParser(
11
+ "Parse an op perf results CSV and show performance data using the min allgather time and max other time over devices, optionally only for a specific signpost region."
12
+ )
13
+ parser.add_argument("csv", help="Input CSV file")
14
+ parser.add_argument("--all", help="Show all times for each device", action="store_true")
15
+ parser.add_argument("--signpost", help="Only include data after this signpost and before any others")
16
+ parser.add_argument("--skip-last", help="Do not include timings from the last N ops", type=int, default=0)
17
+ parser.add_argument("--skip-first", help="Do not include timings from the first N ops", type=int, default=0)
18
+ parser.add_argument("--prefill", help="Prefill mode: will compute tok/s", action="store_true")
19
+ parser.add_argument("--seqlen", help="Sequence length used for prefill statistics.", type=int, default=0)
20
+ parser.add_argument(
21
+ "--estimate-full-model",
22
+ help="Estimate the full model performance by multiplying by N and adding back in the skipped ops",
23
+ type=int,
24
+ default=0,
25
+ )
26
+ parser.add_argument("--write-ops-to-csv", help="Write the summarized ops to a CSV file", type=str, default=None)
27
+ args = parser.parse_args()
28
+
29
+ header, rows = read_rows(args.csv)
30
+ blocks, signposts_seen = make_blocks(header, rows, args.signpost)
31
+
32
+ if args.signpost and not args.signpost in signposts_seen:
33
+ print(f'Error: signpost "{args.signpost}" was not found in this file')
34
+ print(f"Valid signposts are: {signposts_seen}")
35
+ return
36
+
37
+ print(f'{"Op":20} {"Time (us)"}')
38
+
39
+ if args.skip_first:
40
+ print(f"The following ops from the start of the run are not included in summary statistics:")
41
+ for block in blocks[: args.skip_first] if args.skip_first else blocks:
42
+ print(block.long_str() if args.all else block.short_str())
43
+ print(f"Ops included in the summary statistics:")
44
+ skipped_ops = blocks[: args.skip_first]
45
+ blocks = blocks[args.skip_first :]
46
+ else:
47
+ skipped_ops = []
48
+
49
+ for block in blocks[: -args.skip_last] if args.skip_last else blocks:
50
+ print(block.long_str() if args.all else block.short_str())
51
+
52
+ if args.skip_last:
53
+ print(f"The following ops from the end of the run are not included in summary statistics below:")
54
+ for block in blocks[-args.skip_last :]:
55
+ print(block.long_str() if args.all else block.short_str())
56
+ skipped_ops += blocks[-args.skip_last :]
57
+ blocks = blocks[: -args.skip_last]
58
+
59
+ total_time_ns = sum(block.time() for block in blocks)
60
+ total_time_s = total_time_ns / 1e9
61
+ tokens_per_s = 1 / total_time_s
62
+ if args.prefill:
63
+ sequences_per_s = tokens_per_s
64
+ tokens_per_s *= args.seqlen
65
+ print(f"Tokens/s: {tokens_per_s:.2f} ({total_time_s*1000*1000:.1f} us latency, {sequences_per_s:.2f} seq/s)")
66
+ else:
67
+ print(f"Tokens/s/user: {tokens_per_s:.2f} ({total_time_s*1000*1000:.1f} us latency)")
68
+
69
+ if args.estimate_full_model:
70
+ total_time_ns *= args.estimate_full_model
71
+ total_time_ns += sum(block.time() for block in skipped_ops)
72
+ total_time_s = total_time_ns / 1e9
73
+ tokens_per_s = 1 / total_time_s
74
+ if args.prefill:
75
+ sequences_per_s = tokens_per_s
76
+ tokens_per_s *= args.seqlen
77
+ print(
78
+ f"Estimated full model ({args.estimate_full_model} * above + skipped ops) tokens/s: {tokens_per_s:.2f} ({total_time_s*1000*1000:.1f} us latency, {sequences_per_s:.2f} seq/s)"
79
+ )
80
+ else:
81
+ print(
82
+ f"Estimated full model ({args.estimate_full_model} * above + skipped ops) tokens/s/user: {tokens_per_s:.2f} ({total_time_s*1000*1000:.1f} us latency)"
83
+ )
84
+
85
+ if signposts_seen and not args.signpost:
86
+ print(f"Warning - this file contains the following signposts that were not used for this analysis:")
87
+ for s in signposts_seen:
88
+ print(f' "{s}"')
89
+ print("Rerun with --signpost to show only the performance for a specific signpost region")
90
+
91
+ if args.write_ops_to_csv:
92
+ write_blocks_to_csv(blocks, args.write_ops_to_csv)
93
+
94
+ return tokens_per_s
95
+
96
+
97
+ def read_rows(csv_file):
98
+ with open(csv_file, "r") as f:
99
+ reader = csv.reader(f)
100
+ header = next(reader)
101
+ rows = list(reader)
102
+ return header, rows
103
+
104
+
105
+ class Block:
106
+ def __init__(self, op_name, times):
107
+ self.op_name = op_name
108
+ self.times = times
109
+
110
+ def time(self):
111
+ return min(self.times) if "AllGather" in self.op_name or "ReduceScatter" in self.op_name else max(self.times)
112
+
113
+ def short_str(self):
114
+ short_name = self.op_name.split("::")[-1].split(")")[0]
115
+ time_range = max(self.times) - min(self.times)
116
+ return f"{short_name:20} {self.time()/1000:-6.0f} ± {time_range/1000:-5.0f}"
117
+
118
+ def long_str(self):
119
+ short_name = self.op_name.split("::")[-1].split(")")[0]
120
+ return f"{short_name:20} {self.time()/1000:-6.0f} <-" + " | ".join(f"{t/1000:-5.0f}" for t in self.times)
121
+
122
+ def __repr__(self):
123
+ return f"Block({self.op_name}, {self.times})"
124
+
125
+
126
+ def make_blocks(header, rows, signpost):
127
+ """Perf dumps have one row per device in order, repeated for each op
128
+ This returns a list of blocks, where each block has an op name
129
+ and a list of times for each device.
130
+ """
131
+
132
+ # group rows by device then merge them together
133
+ block_by_device = defaultdict(list)
134
+ stop_on_signpost = False
135
+ signposts_seen = []
136
+
137
+ OP_CODE = header.index("OP CODE")
138
+ OP_TYPE = header.index("OP TYPE")
139
+ DEVICE_ID = header.index("DEVICE ID")
140
+ FW_DURATION = header.index("DEVICE FW DURATION [ns]")
141
+
142
+ block_op_name = None
143
+ for row in rows:
144
+ op_name = row[OP_CODE]
145
+ op_type = row[OP_TYPE]
146
+
147
+ if op_type == "signpost":
148
+ signposts_seen.append(op_name)
149
+ if stop_on_signpost:
150
+ break
151
+ elif op_name == signpost:
152
+ # clear any previous data and stop on the next signpost
153
+ stop_on_signpost = True
154
+ block_by_device = defaultdict(list)
155
+ elif op_type == "tt_dnn_device":
156
+ device_id = int(row[DEVICE_ID])
157
+ time = int(row[FW_DURATION])
158
+ block_by_device[device_id].append(Block(op_name, [time]))
159
+
160
+ # merge each device block into a single block with all the device times,
161
+ # checking that the op name matches
162
+ # blocks_by_device is a dict of device_id -> Block
163
+ # we want to get a list of Block (with all device times)
164
+
165
+ device_ids = list(sorted(block_by_device.keys()))
166
+ merged_blocks = block_by_device[device_ids[0]]
167
+
168
+ for device_id in device_ids[1:]:
169
+ assert len(block_by_device[device_id]) == len(
170
+ merged_blocks
171
+ ), f"Device {device_id} has {len(block_by_device[device_id])} ops, expected {len(merged_blocks)} from previous devices"
172
+ for row, b in enumerate(block_by_device[device_id]):
173
+ assert (
174
+ b.op_name == merged_blocks[row].op_name
175
+ ), f"Op name mismatch at row {row}: device {device_id} has {b.op_name} != {merged_blocks[row].op_name}"
176
+ merged_blocks[row].times += b.times
177
+
178
+ return merged_blocks, signposts_seen
179
+
180
+
181
+ def write_blocks_to_csv(blocks, csv_file):
182
+ with open(csv_file, "w") as f:
183
+ writer = csv.writer(f)
184
+ writer.writerow(["Op", "Time (us)"])
185
+ for block in blocks:
186
+ writer.writerow([block.op_name, block.time()])
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()
code/models/tt_transformers/scripts/repack_weights_70b.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2023 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """
6
+ Llama2-70B weights are saved as 8 sharded checkpoints. Loading weights for a
7
+ single layer is slow since we load all 80 layers into memory to construct the
8
+ model. This script repacks the weights into checkpoints chunked by layers to
9
+ speed up development.
10
+ """
11
+ import argparse
12
+ import math
13
+ import shutil
14
+ from collections import defaultdict
15
+ from pathlib import Path
16
+
17
+ import torch
18
+ from tqdm import tqdm
19
+
20
+
21
+ def layer_num(key):
22
+ if "layers" in key:
23
+ return int(key.split("layers.")[1].split(".")[0])
24
+ return 0
25
+
26
+
27
+ def chunk_key(key, chunk_size):
28
+ """
29
+ Return the chunk number that a key should go into
30
+ """
31
+ chunk_id = layer_num(key) // chunk_size
32
+ print(f"Key: {key} -> chunk_id: {chunk_id}")
33
+ return chunk_id
34
+
35
+
36
+ def repack(in_dir, out_dir, chunk_size, num_layers, hidden_size):
37
+ """
38
+ Repack llama2-70b weights into checkpoints chunked by layers.
39
+ Non-layer weights are saved in the first checkpoint.
40
+ """
41
+ num_chunks = math.ceil(num_layers / chunk_size)
42
+ print(f"Repacking {num_layers} layers into {num_chunks} chunks of size {chunk_size}")
43
+ checkpoints = sorted(Path(in_dir).glob("*.pth"))
44
+ merged_checkpoints = defaultdict(list)
45
+ assert len(checkpoints) > 0, f"no checkpoint files found in {in_dir}"
46
+ print(f"Loading {len(checkpoints)} checkpoint files")
47
+ for ckpt in tqdm(checkpoints):
48
+ print(f"Checkpoint file: {ckpt}")
49
+ loaded_ckpt = torch.load(ckpt, map_location="cpu")
50
+ for key, value in loaded_ckpt.items():
51
+ merged_checkpoints[key].append(value)
52
+
53
+ # concat checkpoint values
54
+ chunks = [dict() for _ in range(num_chunks)]
55
+ for key, value in merged_checkpoints.items():
56
+ if len(value) == 1 or "norm" in key:
57
+ val = value[0]
58
+ else:
59
+ if (key == "tok_embeddings.weight" or key == "output.weight") and value[0].shape[1] == hidden_size:
60
+ # Concatenate along dimension 0 for llama3 token embeddings weight and lm head
61
+ val = torch.cat(value, dim=0)
62
+ else:
63
+ # cat_dim is index of the smallest dimension in value[0].shape
64
+ cat_dim = torch.argmin(torch.tensor(value[0].shape))
65
+ val = torch.cat(value, dim=cat_dim)
66
+
67
+ chunk_id = chunk_key(key, chunk_size)
68
+ chunks[chunk_id][key] = val
69
+
70
+ # save chunks and copy params.json if needed
71
+ out_dir = Path(out_dir)
72
+ out_dir.mkdir(parents=True, exist_ok=True)
73
+ params_file = Path(in_dir) / "params.json"
74
+ if params_file.exists() and not (out_dir / "params.json").exists():
75
+ shutil.copy(params_file, out_dir)
76
+ print(f"Copied params.json to {out_dir}")
77
+ for i, chunk in enumerate(chunks):
78
+ # each chunk file name should tell which layers are in it
79
+ start_layer = i * chunk_size
80
+ end_layer = (i + 1) * chunk_size - 1
81
+ end_layer = min(end_layer, num_layers - 1)
82
+ out_file = out_dir / f"layers_{start_layer}-{end_layer}.pth"
83
+ torch.save(chunk, out_file)
84
+ print(f"Saved {out_file}")
85
+
86
+
87
+ if __name__ == "__main__":
88
+ # Take in command line arguments
89
+ parser = argparse.ArgumentParser(description="Repack llama2-70b weights")
90
+ parser.add_argument("in_dir", type=str, help="input directory")
91
+ parser.add_argument("out_dir", type=str, help="output directory")
92
+ parser.add_argument("chunk_size", type=int, default=10, help="number of layers per chunk")
93
+ parser.add_argument("-n", "--num_layers", type=int, default=80, help="total number of layers")
94
+ parser.add_argument("-hs", "--hidden_size", type=int, default=8192, help="hidden size of the model")
95
+ args = parser.parse_args()
96
+ repack(args.in_dir, args.out_dir, args.chunk_size, args.num_layers, args.hidden_size)
code/models/tt_transformers/scripts/repack_weights_90b.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """
6
+ Llama-3.2-90B weights are saved as 8 sharded checkpoints. Loading weights for a
7
+ single layer is slow since we load all layers into memory to construct the
8
+ model. This script repacks the weights into checkpoints chunked by layers to
9
+ speed up development.
10
+ """
11
+ import argparse
12
+ import asyncio
13
+ import json
14
+ import math
15
+ import shutil
16
+ from collections import defaultdict
17
+ from pathlib import Path
18
+
19
+ import torch
20
+ from tqdm import tqdm
21
+
22
+ from models.tt_transformers.tt.load_checkpoints import is_param_replicated_across_shards
23
+
24
+
25
+ def layer_num(key):
26
+ if "layers" in key:
27
+ return int(key.split("layers.")[1].split(".")[0])
28
+ return -1
29
+
30
+
31
+ def chunk_key(key, chunk_size):
32
+ """
33
+ Return the chunk number that a key should go into
34
+ """
35
+ layer_id = layer_num(key)
36
+ assert layer_id >= 0, f"Unexpected key {key}"
37
+ chunk_id = layer_id // chunk_size
38
+ print(f"Key: {key} -> chunk_id: {chunk_id}")
39
+ return chunk_id
40
+
41
+
42
+ def get_unified_tensor(key, value, hidden_size):
43
+ res = None
44
+ if len(value) == 1 or is_param_replicated_across_shards(key):
45
+ res = value[0]
46
+ else:
47
+ if key.endswith("tok_embeddings.weight") or key.endswith("output.weight"):
48
+ assert value[0].shape[1] == hidden_size
49
+ res = torch.cat(value, dim=0)
50
+ else:
51
+ cat_dim = torch.argmin(torch.tensor(value[0].shape))
52
+ res = torch.cat(value, dim=cat_dim)
53
+
54
+ assert res is not None, f"Failed to unify tensor for key {key}"
55
+ return res
56
+
57
+
58
+ def copy_file_if_no_exist(src_path: Path, dst_path: Path, file_name: str) -> None:
59
+ src_file = src_path / file_name
60
+ if src_file.exists() and not (dst_path / file_name).exists():
61
+ shutil.copy(src_file, dst_path)
62
+ print(f"Copied {file_name} to {dst_path}")
63
+
64
+
65
+ async def torch_save_async(chunk, file_full_path):
66
+ loop = asyncio.get_running_loop()
67
+ await loop.run_in_executor(None, torch.save, chunk, file_full_path)
68
+
69
+
70
+ async def repack(in_dir, out_dir, chunk_size, stop_after: int = None):
71
+ """
72
+ Repack llama3.2-90b weights into checkpoints chunked by layers.
73
+ Non-layer weights are saved in the first checkpoint.
74
+
75
+ Args:
76
+ in_dir: input directory containing llama3.2-90b weights from Meta
77
+ out_dir: output directory to save the chunked checkpoints
78
+ chunk_size: number of layers per chunk
79
+ stop_at: stop repacking at this many chunks
80
+ """
81
+ assert stop_after is None or stop_after > 0, f"Invalid stop_at value: {stop_after}"
82
+
83
+ # load model params
84
+ params_file = Path(in_dir) / "params.json"
85
+ assert params_file.exists(), f"params.json not found in {in_dir}"
86
+ with open(params_file, "r") as f:
87
+ params = json.load(f)
88
+ num_layers = params["n_layers"]
89
+ hidden_size = params["dim"]
90
+
91
+ # chunk the vision_model and the first FIVE decoder layers into the first checkpoint
92
+ # the rest of the decoder layers are chunked based on chunk_size
93
+
94
+ # first load the Meta checkpoints
95
+ checkpoints = sorted(Path(in_dir).glob("*.pth"))
96
+ merged_checkpoints = defaultdict(list)
97
+ assert len(checkpoints) > 0, f"no checkpoint files found in {in_dir}"
98
+ print(f"Loading {len(checkpoints)} checkpoint files:")
99
+ for ckpt in tqdm(checkpoints, leave=True):
100
+ tqdm.write(f"Checkpoint file: {ckpt}")
101
+ loaded_ckpt = torch.load(ckpt, map_location="cpu")
102
+ for key, value in loaded_ckpt.items():
103
+ merged_checkpoints[key].append(value)
104
+
105
+ # next we iterate over the merged checkpoints and get all the vision model tensors,
106
+ # the first decoder layer tensors, and all the non-layer tensors
107
+ num_decoder_layers_in_first_chunk = 1
108
+ chunk = {}
109
+ for key in list(merged_checkpoints.keys()):
110
+ if (
111
+ key.startswith("vision_model")
112
+ or layer_num(key) in range(num_decoder_layers_in_first_chunk)
113
+ or "layers." not in key
114
+ ):
115
+ chunk[key] = get_unified_tensor(key, merged_checkpoints[key], hidden_size)
116
+ del merged_checkpoints[key]
117
+
118
+ save_tasks = []
119
+ # save the first chunk
120
+ out_dir = Path(out_dir)
121
+ out_dir.mkdir(parents=True, exist_ok=True)
122
+ copy_file_if_no_exist(Path(in_dir), out_dir, "params.json")
123
+ copy_file_if_no_exist(Path(in_dir), out_dir, "tokenizer.model")
124
+ out_file = out_dir / f"vision-model-and-layers_{0}-{num_decoder_layers_in_first_chunk - 1}.pth"
125
+ save_tasks.append(asyncio.create_task(torch_save_async(chunk, out_file)))
126
+ print(f"Saved the following layers in {out_file}:")
127
+ for key in chunk.keys():
128
+ print("\t" + key)
129
+ del chunk
130
+
131
+ if stop_after is not None and stop_after == 1:
132
+ await wait_with_progress(save_tasks, desc="Writing chunked checkpoints to files")
133
+ return # early return to stop at the first chunk
134
+
135
+ # save the rest of the merged checkpoints into chunks
136
+ num_chunks = math.ceil((num_layers - num_decoder_layers_in_first_chunk) / chunk_size)
137
+ # set stop_after to num_chunks if it is None, which means repacking all layers
138
+ stop_after = num_chunks if stop_after is None else stop_after - 1 # [INFO] -1 because already saved the 1st chunk
139
+
140
+ chunks = [list() for _ in range(num_chunks)]
141
+ for key in merged_checkpoints.keys():
142
+ assert key.startswith("text_model"), f"Unexpected key: {key}"
143
+ layer_id = layer_num(key)
144
+ assert layer_id != -1, f"Unexpected key: {key}"
145
+ chunk_id = (layer_id - num_decoder_layers_in_first_chunk) // chunk_size # the first few layers is already saved
146
+ chunks[chunk_id].append(key)
147
+
148
+ print(f"Repacking {num_layers} layers into {num_chunks} chunks of size {chunk_size}")
149
+ for chunk_id in tqdm(range(num_chunks)):
150
+ if chunk_id >= stop_after:
151
+ break
152
+
153
+ chunk = {}
154
+ for key in chunks[chunk_id]:
155
+ chunk[key] = get_unified_tensor(key, merged_checkpoints[key], hidden_size)
156
+ del merged_checkpoints[key]
157
+
158
+ # save the chunk
159
+ start_layer = chunk_id * chunk_size + num_decoder_layers_in_first_chunk
160
+ end_layer = (chunk_id + 1) * chunk_size + num_decoder_layers_in_first_chunk - 1
161
+ end_layer = min(end_layer, num_layers - 1)
162
+ out_file = out_dir / f"layers_{start_layer}-{end_layer}.pth"
163
+ save_tasks.append(asyncio.create_task(torch_save_async(chunk, out_file)))
164
+ print(f"Saving the following layers in {out_file}:")
165
+ for key in chunk.keys():
166
+ print("\t" + key)
167
+ del chunk
168
+
169
+ await wait_with_progress(save_tasks, desc="Writing chunked checkpoints to files")
170
+
171
+
172
+ async def wait_with_progress(tasks, desc):
173
+ """Wait for tasks to finish, updating a progress bar as each completes."""
174
+ total = len(tasks)
175
+ with tqdm(total=total, desc=desc, leave=True) as pbar:
176
+ pending = set(tasks)
177
+ while pending:
178
+ done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
179
+ pbar.update(len(done))
180
+
181
+
182
+ if __name__ == "__main__":
183
+ # Take in command line arguments
184
+ parser = argparse.ArgumentParser(description="Repack llama3.2-90b weights")
185
+ parser.add_argument("in_dir", type=str, help="input directory")
186
+ parser.add_argument("out_dir", type=str, help="output directory")
187
+ parser.add_argument("chunk_size", type=int, default=10, help="number of layers per chunk")
188
+ parser.add_argument(
189
+ "--stop_after", type=int, default=None, help="stop repacking after this many chunks are saved (default to all)"
190
+ )
191
+ args = parser.parse_args()
192
+
193
+ asyncio.run(repack(args.in_dir, args.out_dir, args.chunk_size, args.stop_after))
code/models/tt_transformers/tests/conftest.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2023 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ import gc
5
+
6
+ import pytest
7
+
8
+ from models.tt_transformers.tt.model_config import parse_optimizations
9
+
10
+ # transformers 5.x removed Cache.get_usable_length, but some trust_remote_code reference models
11
+ # still call it -- e.g. microsoft/Phi-3-mini-128k-instruct's modeling_phi3.py does
12
+ # `kv_seq_len += past_key_value.get_usable_length(kv_seq_len, self.layer_idx)`, which raises
13
+ # AttributeError under transformers 5.10.2. For an unbounded cache (DynamicCache) the old method
14
+ # simply returned get_seq_length(layer_idx), so restore it as that alias to keep those reference
15
+ # models working. Scoped to DynamicCache only -- bounded caches had different (max-length) logic.
16
+ try:
17
+ from transformers.cache_utils import DynamicCache
18
+
19
+ if not hasattr(DynamicCache, "get_usable_length"):
20
+
21
+ def _get_usable_length(self, new_seq_length=0, layer_idx=0):
22
+ return self.get_seq_length(layer_idx)
23
+
24
+ DynamicCache.get_usable_length = _get_usable_length
25
+ except Exception: # defensive: transformers cache internals may move
26
+ pass
27
+
28
+
29
+ @pytest.fixture(autouse=True)
30
+ def ensure_gc():
31
+ gc.collect()
32
+
33
+
34
+ def pytest_addoption(parser):
35
+ parser.addoption(
36
+ "--optimizations",
37
+ action="store",
38
+ default=None,
39
+ type=parse_optimizations,
40
+ help="Precision and fidelity configuration diffs over default (i.e., accuracy)",
41
+ )
42
+
43
+ parser.addoption(
44
+ "--decoder_config_file",
45
+ action="store",
46
+ default=None,
47
+ type=str,
48
+ help="Provide a JSON file defining per-decoder precision and fidelity settings",
49
+ )
50
+ parser.addoption(
51
+ "--use_hf_rope",
52
+ action="store_true",
53
+ default=False,
54
+ help="Whether to use HF-style rope, if not passed, the default mllama will be used",
55
+ )
code/models/tt_transformers/tests/generate_reference_outputs.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2023 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ import argparse
5
+ import bz2
6
+ import os
7
+
8
+ import torch
9
+ from loguru import logger
10
+ from transformers import AutoConfig, AutoModelForCausalLM, AutoTokenizer
11
+
12
+ from models.tt_transformers.tt.model_config import ModelArgs
13
+
14
+
15
+ def generate_reference_outputs(total_length, output_file, hf_model_name=None):
16
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
17
+ logger.info(f"Using device: {device}")
18
+
19
+ if hf_model_name:
20
+ # HuggingFace path
21
+ tokenizer = AutoTokenizer.from_pretrained(hf_model_name)
22
+ config = AutoConfig.from_pretrained(hf_model_name)
23
+ # Qwen only: add rope scaling to the config
24
+ # https://huggingface.co/Qwen/Qwen2.5-7B-Instruct#processing-long-texts
25
+ if "Qwen" in hf_model_name:
26
+ config.rope_scaling = {"factor": 4.0, "original_max_position_embeddings": 32768, "type": "yarn"}
27
+ model = AutoModelForCausalLM.from_pretrained(
28
+ hf_model_name, config=config, torch_dtype=torch.float32 if device == "cpu" else None, device_map="auto"
29
+ )
30
+ model.eval()
31
+ model_args = ModelArgs(mesh_device=None, cache_hf=True)
32
+
33
+ else:
34
+ # Original path - load reference model
35
+ model_args = ModelArgs(mesh_device=None, cache_hf=True)
36
+ model_args.max_seq_len = total_length
37
+ tokenizer = model_args.tokenizer
38
+ assert tokenizer is not None, "Tokenizer must be provided for non-dummy weights"
39
+
40
+ reference_model = model_args.reference_transformer(load_checkpoint=True, wrap=False)
41
+ reference_model.to(device) # Move model to device
42
+ reference_model.eval() # Set to evaluation mode
43
+ embd = reference_model.model.embed_tokens
44
+ embd.to(device) # Move embedding to device
45
+
46
+ # Load the book text and encode tokens
47
+ current_file_path = os.path.abspath(__file__)
48
+ current_file_dir = os.path.dirname(current_file_path)
49
+ prompt_file = os.path.join(current_file_dir, "tale-of-two-cities.txt.bz2")
50
+
51
+ with bz2.open(prompt_file, "rt", encoding="utf-8") as f:
52
+ text = f.read()
53
+
54
+ # Encode text to tokens
55
+ encoded_tokens = model_args.encode_prompt(text, instruct=False)
56
+ encoded_tokens_tensor = torch.tensor(encoded_tokens, device=device).unsqueeze(0) # Move to device
57
+
58
+ print(f"{'Progress':<15}{'Correct':<8}{'Actual':<15}{'Top 5 Predictions':<75}")
59
+ print("-" * 113)
60
+
61
+ # Initialize lists to store results
62
+ all_top1_correct = []
63
+ all_top5_correct = []
64
+ all_top5_tokens = []
65
+ segment_accuracies = []
66
+ chunk_size = 1024
67
+
68
+ with torch.no_grad():
69
+ for chunk_start in range(0, total_length - 1, chunk_size):
70
+ chunk_end = min(chunk_start + chunk_size, total_length)
71
+ # Get input and target chunks, ensuring they have matching lengths
72
+ chunk_tokens = encoded_tokens_tensor[:, chunk_start:chunk_end]
73
+ chunk_next_tokens = encoded_tokens[chunk_start + 1 : chunk_end + 1]
74
+ actual_chunk_size = min(len(chunk_tokens[0]), len(chunk_next_tokens))
75
+
76
+ # Trim input chunk if needed
77
+ chunk_tokens = chunk_tokens[:, :actual_chunk_size]
78
+
79
+ # Process chunk based on model type
80
+ chunk_tokens = chunk_tokens.to(device)
81
+ if hf_model_name:
82
+ outputs = model(chunk_tokens)
83
+ ref_output = outputs.logits
84
+ else:
85
+ pt_decode_input = embd(chunk_tokens).view(1, actual_chunk_size, -1)
86
+ ref_output = reference_model(pt_decode_input, start_pos=chunk_start)
87
+
88
+ # Compute top-5 predictions
89
+ probs = torch.softmax(ref_output, dim=-1)
90
+ _, chunk_top5_tokens = torch.topk(probs, k=5, dim=-1) # Shape: [1, chunk_size, 5]
91
+ chunk_top5_tokens = chunk_top5_tokens.squeeze(0) # Shape: [chunk_size, 5]
92
+
93
+ # Get next tokens tensor, ensuring same length as predictions
94
+ chunk_next_tokens_tensor = torch.tensor(chunk_next_tokens[:actual_chunk_size], device=device)
95
+
96
+ # Calculate correctness
97
+ chunk_top1_correct = chunk_top5_tokens[:, 0] == chunk_next_tokens_tensor
98
+ chunk_top5_correct = torch.any(chunk_top5_tokens == chunk_next_tokens_tensor.unsqueeze(1), dim=1)
99
+
100
+ # Store results
101
+ all_top1_correct.extend(chunk_top1_correct.tolist())
102
+ all_top5_correct.extend(chunk_top5_correct.tolist())
103
+ all_top5_tokens.append(chunk_top5_tokens)
104
+
105
+ # Print predictions for this chunk
106
+ for i in range(len(chunk_next_tokens)):
107
+ global_pos = chunk_start + i
108
+ next_token = chunk_next_tokens[i]
109
+
110
+ sanitize = lambda x: x.replace("\n", "").replace("\r", "").replace("\x0c", "")
111
+ actual_token = sanitize(tokenizer.decode([next_token]))
112
+ top5_tokens = [sanitize(tokenizer.decode([t.item()])) for t in chunk_top5_tokens[i]]
113
+ correct = "x" if chunk_top1_correct[i] else ("-" if chunk_top5_correct[i] else " ")
114
+ top5_str = " ".join(f"{t:<14}" for t in top5_tokens)
115
+
116
+ progress_str = f"{global_pos+1}/{total_length-1}"
117
+ print(f"{progress_str:<15}{correct:<8}{actual_token:<15}{top5_str}")
118
+
119
+ # Calculate and store segment accuracies every 100 tokens
120
+ if (global_pos + 1) % 100 == 0 or global_pos == total_length - 2:
121
+ start_idx = (global_pos // 100) * 100
122
+ end_idx = min(start_idx + 100, len(all_top1_correct))
123
+ segment_top1_acc = sum(all_top1_correct[start_idx:end_idx]) / (end_idx - start_idx) * 100
124
+ segment_top5_acc = sum(all_top5_correct[start_idx:end_idx]) / (end_idx - start_idx) * 100
125
+ if len(segment_accuracies) <= global_pos // 100:
126
+ segment_accuracies.append((segment_top1_acc, segment_top5_acc))
127
+
128
+ # Concatenate all top5 tokens into a single tensor
129
+ all_top5_tokens = torch.cat(all_top5_tokens, dim=0) # Shape: [total_tokens, 5]
130
+
131
+ # Move tensors back to CPU before saving
132
+ data = {
133
+ "top5_tokens": all_top5_tokens.cpu(),
134
+ "reference_tokens": encoded_tokens_tensor[:, :total_length].clone().cpu(),
135
+ }
136
+
137
+ torch.save(data, output_file)
138
+ logger.info(f"Saved reference outputs to {output_file}")
139
+
140
+ # Print all segment accuracy summaries as a table
141
+ print("\nSegment Accuracy Summaries:")
142
+ print(f"{'Tokens':<15}{'Top-1 Accuracy':<20}{'Top-5 Accuracy':<20}")
143
+ print("-" * 55)
144
+ for i, (top1_acc, top5_acc) in enumerate(segment_accuracies):
145
+ start_token = i * 100 + 1
146
+ end_token = min((i + 1) * 100, total_length)
147
+ print(f"{f'{start_token}-{end_token}':<15}{f'{top1_acc:.2f}%':<20}{f'{top5_acc:.2f}%':<20}")
148
+
149
+ # Calculate overall accuracy
150
+ overall_top1_acc = sum(acc[0] for acc in segment_accuracies) / len(segment_accuracies)
151
+ overall_top5_acc = sum(acc[1] for acc in segment_accuracies) / len(segment_accuracies)
152
+ print("-" * 55)
153
+ print(f"{'Overall':<15}{f'{overall_top1_acc:.2f}%':<20}{f'{overall_top5_acc:.2f}%':<20}")
154
+
155
+
156
+ # New main function with argparse
157
+ def main():
158
+ parser = argparse.ArgumentParser(description="Generate reference outputs for LLaMA accuracy testing.")
159
+ parser.add_argument("--total_length", type=int, default=1024, help="Total length of tokens to process")
160
+ parser.add_argument(
161
+ "--output_file", type=str, default="reference_outputs.pt", help="Output file path for reference data"
162
+ )
163
+ parser.add_argument(
164
+ "--model", type=str, help="Optional: HuggingFace model name (e.g., 'meta-llama/Llama-3.1-8B-Instruct')"
165
+ )
166
+ args = parser.parse_args()
167
+
168
+ generate_reference_outputs(total_length=args.total_length, output_file=args.output_file, hf_model_name=args.model)
169
+
170
+
171
+ if __name__ == "__main__":
172
+ main()
code/models/tt_transformers/tests/generate_reference_outputs.sh ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Parse command line arguments
4
+ TOTAL_LENGTH=1024 # Default value
5
+ while [[ $# -gt 0 ]]; do
6
+ case $1 in
7
+ --total-length)
8
+ TOTAL_LENGTH="$2"
9
+ shift 2
10
+ ;;
11
+ --help|-h)
12
+ echo "Usage: $0 [OPTIONS]"
13
+ echo
14
+ echo "Generate reference outputs for Llama models"
15
+ echo
16
+ echo "Options:"
17
+ echo " --total-length N Set the total sequence length (default: 1024)"
18
+ echo " --help, -h Show this help message"
19
+ exit 0
20
+ ;;
21
+ *)
22
+ echo "Unknown option: $1"
23
+ echo "Use --help to see available options"
24
+ exit 1
25
+ ;;
26
+ esac
27
+ done
28
+
29
+ # Define model directories from environment variables with fallbacks
30
+ HF_MODELS=(
31
+ "${LLAMA_32_1B_DIR:-meta-llama/Llama-3.2-1B-Instruct}"
32
+ "${LLAMA_32_3B_DIR:-meta-llama/Llama-3.2-3B-Instruct}"
33
+ "${LLAMA_31_8B_DIR:-meta-llama/Llama-3.1-8B-Instruct}"
34
+ "${LLAMA_32_11B_DIR:-meta-llama/Llama-3.2-11B-Vision-Instruct}"
35
+ "${LLAMA_33_70B_DIR:-meta-llama/Llama-3.3-70B-Instruct}"
36
+ "${LLAMA_32_90B_DIR:-meta-llama/Llama-3.2-90B-Vision-Instruct}"
37
+ "${QWEN_25_7B_DIR:-Qwen/Qwen2.5-7B-Instruct}"
38
+ "${QWEN_25_72B_DIR:-Qwen/Qwen2.5-72B-Instruct}"
39
+ "${QWEN_25_32B_DIR:-Qwen/Qwen2.5-32B-Instruct}"
40
+ "${MIXTRAL_8X7B_DIR:-mistralai/Mixtral-8x7B-Instruct-v0.1}"
41
+ "${QWEN_25_CODER_32B_DIR:-Qwen/Qwen2.5-Coder-32B-Instruct}"
42
+ )
43
+
44
+ # Create reference_outputs directory if it doesn't exist
45
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
46
+ OUTPUT_DIR="${SCRIPT_DIR}/reference_outputs"
47
+ mkdir -p "$OUTPUT_DIR"
48
+
49
+ # Function to get model name from directory path
50
+ get_model_name() {
51
+ local dir_name=$(basename "$1")
52
+ # If the path ends in /repacked, use the parent directory name instead
53
+ if [ "$dir_name" = "repacked" ]; then
54
+ dir_name=$(basename "$(dirname "$1")")
55
+ fi
56
+ echo "$dir_name"
57
+ }
58
+
59
+ # Loop through each LLAMA directory
60
+ for DIR in "${HF_MODELS[@]}"; do
61
+ # TBD: do check using HF_HOME
62
+ # if [ ! -d "$DIR" ]; then
63
+ # echo "Warning: Directory $DIR does not exist, skipping..."
64
+ # continue
65
+ # fi
66
+
67
+ # Get model size for output filename
68
+ MODEL_NAME=$(get_model_name "$DIR")
69
+ OUTPUT_FILE="${OUTPUT_DIR}/${MODEL_NAME}_full.refpt"
70
+
71
+ echo "Generating reference outputs for ${MODEL_SIZE} model..."
72
+ echo "Using weights from: ${DIR}"
73
+ echo "Output will be saved to: ${OUTPUT_FILE}"
74
+
75
+ # Set HF_MODEL environment variable and run the Python script
76
+ HF_MODEL="$DIR" python3 "${SCRIPT_DIR}/generate_reference_outputs.py" \
77
+ --total_length "$TOTAL_LENGTH" \
78
+ --output_file "$OUTPUT_FILE" \
79
+ --model "$DIR"
80
+ done
81
+
82
+ echo "All reference outputs have been generated!"
code/models/tt_transformers/tests/test_attention_prefill.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2023 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ import os
5
+
6
+ import pytest
7
+ import torch
8
+ from loguru import logger
9
+
10
+ import ttnn
11
+ from models.common.utility_functions import comp_allclose, comp_pcc
12
+ from models.tt_transformers.tests.test_utils import get_ref_model_dype
13
+ from models.tt_transformers.tt.attention import Attention
14
+ from models.tt_transformers.tt.ccl import TT_CCL
15
+ from models.tt_transformers.tt.common import Mode, PagedAttentionConfig, get_rot_transformation_mat, precompute_freqs
16
+ from models.tt_transformers.tt.model_config import ModelArgs
17
+ from models.tt_transformers.tt.prefetcher import Prefetcher
18
+ from models.tt_transformers.tt.rope import get_rot_mats, get_rot_mats_hf
19
+
20
+
21
+ @torch.no_grad()
22
+ @pytest.mark.parametrize(
23
+ "mesh_device",
24
+ [
25
+ {"N150": (1, 1), "N300": (1, 2), "T3K": (1, 8), "TG": (8, 4)}.get(
26
+ os.environ.get("MESH_DEVICE"), len(ttnn.get_device_ids())
27
+ )
28
+ ],
29
+ indirect=True,
30
+ )
31
+ # Model and attention prefill tests should run both with and without paged attention to debug any issues that may occur with default attention
32
+ @pytest.mark.parametrize(
33
+ "paged_attention",
34
+ (
35
+ True,
36
+ False,
37
+ ),
38
+ ids=(
39
+ "paged_attention",
40
+ "default_attention",
41
+ ),
42
+ )
43
+ @pytest.mark.parametrize(
44
+ "page_params",
45
+ [{"page_block_size": 32, "page_max_num_blocks": 1024}],
46
+ )
47
+ @pytest.mark.parametrize(
48
+ "max_seq_len",
49
+ (
50
+ 256, # 4096,
51
+ # 1024 * 32,
52
+ # 1024 * 64,
53
+ ),
54
+ )
55
+ @pytest.mark.parametrize(
56
+ "use_prefetcher",
57
+ ([False]),
58
+ )
59
+ @pytest.mark.parametrize("use_hf_rope", (True, False), ids=("hf_rope", "mllama_rope"))
60
+ @pytest.mark.parametrize("device_params", [{"fabric_config": True}], indirect=True)
61
+ def test_attention_inference(
62
+ max_seq_len,
63
+ paged_attention,
64
+ page_params,
65
+ mesh_device,
66
+ use_hf_rope,
67
+ reset_seeds,
68
+ ensure_gc,
69
+ use_prefetcher,
70
+ ):
71
+ dtype = ttnn.bfloat8_b
72
+ pcc = 0.99
73
+ batch_size = 1 # For prefill we only support batch_size = 1
74
+
75
+ # In prefill mode, we do not use prefetcher but we test the prefetcher interface for completeness and
76
+ num_tensors = 0
77
+ prefetcher = Prefetcher(mesh_device, num_tensors=num_tensors, num_layers=1) if use_prefetcher else None
78
+ if use_prefetcher:
79
+ prefetcher.init(mode=Mode.PREFILL)
80
+
81
+ model_args = ModelArgs(
82
+ mesh_device, max_batch_size=batch_size, max_seq_len=max_seq_len, cache_hf=True, use_hf_rope=use_hf_rope
83
+ )
84
+ model_args.n_layers = 1
85
+ state_dict = model_args.load_state_dict()
86
+
87
+ # Ref model needs partial state dict, but our models use full state dict keys as cached weight names
88
+ first_layer_prefix = model_args.get_state_dict_prefix("Attention", 0) + "."
89
+ partial_state_dict = {
90
+ k[len(first_layer_prefix) :]: v for k, v in state_dict.items() if (k.startswith(first_layer_prefix))
91
+ }
92
+ reference_model = model_args.reference_attention(load_checkpoint=True)
93
+
94
+ rot_mats_fn = get_rot_mats_hf if model_args.use_hf_rope else get_rot_mats
95
+
96
+ # pre-compute the rotational embedding matrix and send to device
97
+ rot_mats = rot_mats_fn(
98
+ head_dim=model_args.head_dim,
99
+ device=mesh_device,
100
+ seq_len=max_seq_len,
101
+ theta=model_args.rope_theta,
102
+ rope_scaling=model_args.rope_scaling,
103
+ )
104
+
105
+ transformation_mats = {}
106
+ if not model_args.use_hf_rope:
107
+ transformation_mat_torch = get_rot_transformation_mat(model_args.head_dim)
108
+ transformation_mats_prefill = ttnn.as_tensor(
109
+ transformation_mat_torch,
110
+ dtype=ttnn.bfloat16,
111
+ layout=ttnn.TILE_LAYOUT,
112
+ device=mesh_device,
113
+ memory_config=ttnn.DRAM_MEMORY_CONFIG,
114
+ mesh_mapper=ttnn.ReplicateTensorToMesh(mesh_device),
115
+ )
116
+ transformation_mats = {"prefill": transformation_mats_prefill}
117
+
118
+ generation_start_pos = 0
119
+ generation_length = 3
120
+ all_tests_pass = True
121
+
122
+ # Setup page table
123
+ page_table_tt = None
124
+ paged_attention_config = None
125
+
126
+ if paged_attention:
127
+ paged_attention_config = PagedAttentionConfig(
128
+ block_size=page_params["page_block_size"],
129
+ max_num_blocks=page_params["page_max_num_blocks"],
130
+ )
131
+ # Implied shuffling of blocks
132
+ permutation = torch.randperm(paged_attention_config.max_num_blocks)
133
+ # Page table which maps virtual blocks to physical
134
+ reverse_permutation = torch.argsort(permutation)
135
+ page_table = reverse_permutation.reshape(
136
+ model_args.max_batch_size, paged_attention_config.max_num_blocks // model_args.max_batch_size
137
+ )
138
+ page_table_tt = ttnn.from_torch(
139
+ page_table,
140
+ device=mesh_device,
141
+ dtype=ttnn.int32,
142
+ layout=ttnn.ROW_MAJOR_LAYOUT,
143
+ mesh_mapper=ttnn.ReplicateTensorToMesh(mesh_device),
144
+ )
145
+
146
+ tt_ccl = TT_CCL(mesh_device)
147
+ tt_model = Attention(
148
+ mesh_device,
149
+ tt_ccl,
150
+ model_args,
151
+ state_dict,
152
+ weight_cache_path=model_args.weight_cache_path(dtype),
153
+ layer_num=0,
154
+ dtype=dtype,
155
+ transformation_mats=transformation_mats,
156
+ configuration=model_args,
157
+ paged_attention_config=paged_attention_config,
158
+ prefetcher=prefetcher,
159
+ )
160
+
161
+ pt_attention_input = (
162
+ torch.rand(
163
+ batch_size, max_seq_len, model_args.dim, dtype=get_ref_model_dype(reference_model, model_args.model_name)
164
+ )
165
+ * 2
166
+ ) - 1
167
+ tt_attention_input = pt_attention_input.clone()
168
+ attention_input = model_args.prepare_residual_tensor_prefill(
169
+ tt_attention_input,
170
+ force_replicated=False if model_args.is_galaxy else True,
171
+ )
172
+
173
+ tt_out = tt_model(
174
+ attention_input,
175
+ current_pos=None,
176
+ rot_mats=rot_mats,
177
+ user_id=0,
178
+ mode=Mode.PREFILL,
179
+ page_table=page_table_tt,
180
+ )
181
+ tt_out = ttnn.to_torch(
182
+ tt_out, mesh_composer=ttnn.ConcatMesh2dToTensor(mesh_device, dims=(1, 3), mesh_shape=model_args.cluster_shape)
183
+ )
184
+ tt_output_torch = tt_out[:, 0:1, :, : model_args.dim].view(batch_size, max_seq_len, -1) # [ batch, seq, hidden_dim]
185
+ positions = torch.LongTensor(range(max_seq_len))
186
+
187
+ cos, sin = precompute_freqs(
188
+ model_args.head_dim,
189
+ model_args.max_seq_len * 2,
190
+ model_args.rope_theta,
191
+ model_args.rope_scaling.factor if model_args.rope_scaling else None,
192
+ model_args.rope_scaling.original_max_position_embeddings if model_args.rope_scaling else None,
193
+ model_args.rope_scaling.rope_type.value if model_args.rope_scaling else "llama3",
194
+ )
195
+ freqs_cis_i = torch.complex(cos, sin)[positions]
196
+
197
+ attn_mask = torch.full((max_seq_len, max_seq_len), torch.finfo(torch.float32).min)
198
+ attn_mask_torch = torch.triu(attn_mask, diagonal=1)
199
+ reference_output = reference_model(pt_attention_input, positions[0], freqs_cis_i, mask=attn_mask_torch)
200
+
201
+ passing, pcc_message = comp_pcc(reference_output, tt_output_torch, pcc)
202
+
203
+ logger.info(comp_allclose(reference_output, tt_output_torch))
204
+ logger.info(f"PCC: {pcc_message}")
205
+ if passing:
206
+ logger.info(f"Attention Passed!")
207
+ else:
208
+ logger.warning(f"Attention Failed!")
209
+ all_tests_pass = False
210
+
211
+ check_kv_cache = True # May want to disable: Issue #10648
212
+ if check_kv_cache:
213
+ # PyTorch output --------------------------------------------------------------------
214
+ pytorch_layer_present = [
215
+ reference_model.cache_k.clone().permute(0, 2, 1, 3), # [batch_size, n_kv_heads, seq, head_dim]
216
+ reference_model.cache_v.clone().permute(0, 2, 1, 3), # [batch_size, n_kv_heads, seq, head_dim]
217
+ ]
218
+ # TT hardware execution -------------------------------------------------------------
219
+ if paged_attention:
220
+ tt_layer_present = [
221
+ (
222
+ ttnn.to_torch(
223
+ cache,
224
+ mesh_composer=ttnn.ConcatMesh2dToTensor(
225
+ mesh_device,
226
+ dims=(1, 3) if model_args.is_galaxy else (0, 1),
227
+ mesh_shape=model_args.cluster_shape,
228
+ ),
229
+ )[reverse_permutation][:, : model_args.n_kv_heads, :, : model_args.head_dim]
230
+ .reshape(
231
+ model_args.max_batch_size,
232
+ paged_attention_config.max_num_blocks // model_args.max_batch_size,
233
+ model_args.n_kv_heads,
234
+ paged_attention_config.block_size,
235
+ model_args.head_dim,
236
+ )
237
+ .transpose(1, 2)
238
+ .reshape(model_args.max_batch_size, model_args.n_kv_heads, -1, model_args.head_dim)[
239
+ :batch_size, ...
240
+ ]
241
+ )
242
+ for cache in tt_model.layer_past
243
+ ]
244
+ else:
245
+ tt_layer_present = [
246
+ ttnn.to_torch(
247
+ cache,
248
+ mesh_composer=ttnn.ConcatMesh2dToTensor(
249
+ mesh_device,
250
+ dims=(1, 0) if model_args.is_galaxy else (0, 1),
251
+ mesh_shape=model_args.cluster_shape,
252
+ ),
253
+ )[:batch_size, :, :, :]
254
+ for cache in tt_model.layer_past
255
+ ]
256
+
257
+ for i, (cache_pt, cache_tt) in enumerate(zip(pytorch_layer_present, tt_layer_present)):
258
+ cache_length_to_check = min(model_args.max_seq_len, generation_start_pos + generation_length + 1)
259
+ cache_pt = cache_pt[:, :, generation_start_pos:cache_length_to_check, :]
260
+ cache_tt = cache_tt[:, :, generation_start_pos:cache_length_to_check, :]
261
+ does_pass, output_pcc = comp_pcc(cache_pt, cache_tt, pcc)
262
+ if i == 0:
263
+ logger.info(f"K cache output: {output_pcc}")
264
+ else:
265
+ logger.info(f"V cache output: {output_pcc}")
266
+
267
+ if does_pass:
268
+ logger.info(f"KV Cache Passed!")
269
+ else:
270
+ logger.warning(f"KV Cache Failed! PCC value is lower than {pcc}")
271
+ all_tests_pass = False
272
+
273
+ if all_tests_pass:
274
+ logger.info("Attention output Passed!")
275
+ else:
276
+ logger.warning("Attention output Failed!")
277
+ assert all_tests_pass, f"PCC value is lower than {pcc} for some of the outputs. Check Warnings!"
code/models/tt_transformers/tests/test_batched_prefill_slots.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Batched prefill lays its device rows out by physical slot, not by prefill position.
5
+
6
+ ``empty_slots[i]`` is the device slot that owns request ``i``'s per-slot state, and
7
+ every slot-indexed buffer the batched path builds (``prefill_ids``,
8
+ ``padded_last_token_idx``, the padded page table) is bounded by ``padded_batch``. vLLM
9
+ hands out the slot a request already owns, so a batch of N requests can land on slots
10
+ above N and the device batch has to span them. The arrays handed back to the caller
11
+ stay in prefill order, so the readback reads by slot and writes by position. Pure host
12
+ index bookkeeping, no device execution.
13
+ """
14
+
15
+ import torch
16
+
17
+ from models.common.sampling import SamplingParams, slice_sampling_params
18
+ from models.common.sampling.tt_log_probs import LogProbsResult
19
+ from models.tt_transformers.tt.generator import batched_prefill_padded_batch, gather_batched_prefill_samples
20
+
21
+
22
+ def test_dense_slots_keep_todays_batch_shape():
23
+ """The common case is unchanged, so existing shapes and traces are reused."""
24
+ assert batched_prefill_padded_batch(7, list(range(7)), 32) == 8
25
+ assert batched_prefill_padded_batch(2, [0, 1], 32) == 2
26
+ assert batched_prefill_padded_batch(32, list(range(32)), 32) == 32
27
+
28
+
29
+ def test_batch_spans_the_highest_slot_in_use():
30
+ """A live off-batch request holding a low slot pushes a prefill onto a high one."""
31
+ # THE BUG: seven requests whose slots reach 7. The count-based rule returned 8,
32
+ # which is fine here, but a request on slot 20 got a 1-row batch.
33
+ assert batched_prefill_padded_batch(7, [0, 1, 2, 3, 4, 5, 7], 32) == 8
34
+ assert batched_prefill_padded_batch(1, [20], 32) == 32
35
+ assert batched_prefill_padded_batch(3, [3, 4, 5], 32) == 8
36
+
37
+
38
+ def test_no_slots_means_the_request_count_is_the_span():
39
+ """Callers that omit the slots get ``range(N)``, so N bounds the rows."""
40
+ assert batched_prefill_padded_batch(4, None, 32) == 4
41
+ assert batched_prefill_padded_batch(4, [], 32) == 4
42
+
43
+
44
+ class _SlotTaggedLogProbs(LogProbsResult):
45
+ """Stands in for a device top-k result: reports which slot it was read from."""
46
+
47
+ def __init__(self):
48
+ super().__init__(topk_logprobs=None, topk_indices=None, topk_logprobs_host=None, topk_indices_host=None)
49
+
50
+ def extract_user(self, user_batch_idx: int):
51
+ return f"slot{int(user_batch_idx)}"
52
+
53
+
54
+ def test_samples_come_back_in_prefill_order_not_slot_order():
55
+ """Read the device row by slot, write the caller's row by position.
56
+
57
+ Row i of the device batch holds slot i's sample, so a request on slot 5 has to
58
+ end up at output row 0 if it prefilled first.
59
+ """
60
+ slots = [5, 0, 3]
61
+ # Device rows: index == slot, so slot 5 sampled token 105, slot 0 token 100, ...
62
+ tokens_host = torch.tensor([100, 101, 102, 103, 104, 105, 106, 107])
63
+ plain_log_probs_host = torch.tensor([-0.0, -0.1, -0.2, -0.3, -0.4, -0.5, -0.6, -0.7])
64
+ output_tokens = torch.zeros(len(slots), 1, dtype=torch.int64)
65
+ output_log_probs = [None] * len(slots)
66
+
67
+ gather_batched_prefill_samples(slots, tokens_host, None, plain_log_probs_host, output_tokens, output_log_probs)
68
+
69
+ assert [int(t) for t in output_tokens.reshape(-1)] == [105, 100, 103]
70
+ assert [round(float(lp), 1) for lp in output_log_probs] == [-0.5, -0.0, -0.3]
71
+
72
+
73
+ def test_a_slot_at_the_request_count_does_not_overflow_the_output():
74
+ """THE CRASH: three requests reaching slot 7 wrote past a 3-row output."""
75
+ slots = [0, 1, 7]
76
+ tokens_host = torch.tensor([200, 201, 202, 203, 204, 205, 206, 207])
77
+ output_tokens = torch.zeros(len(slots), 1, dtype=torch.int64)
78
+ output_log_probs = [None] * len(slots)
79
+
80
+ gather_batched_prefill_samples(slots, tokens_host, None, None, output_tokens, output_log_probs)
81
+
82
+ assert [int(t) for t in output_tokens.reshape(-1)] == [200, 201, 207]
83
+ assert output_log_probs == [None, None, None]
84
+
85
+
86
+ def test_topk_logprobs_are_extracted_from_the_slot_row():
87
+ slots = [4, 1]
88
+ tokens_host = torch.arange(8)
89
+ output_tokens = torch.zeros(len(slots), 1, dtype=torch.int64)
90
+ output_log_probs = [None] * len(slots)
91
+
92
+ gather_batched_prefill_samples(slots, tokens_host, _SlotTaggedLogProbs(), None, output_tokens, output_log_probs)
93
+
94
+ assert output_log_probs == ["slot4", "slot1"]
95
+
96
+
97
+ def test_slice_sampling_params_gives_each_chunk_its_own_requests():
98
+ """A chunked prefill must not hand every chunk the first N requests' params."""
99
+ params = SamplingParams(
100
+ temperature=[0.1, 0.2, 0.3, 0.4], top_k=[1, 2, 3, 4], top_p=[0.5, 0.6, 0.7, 0.8], seed=[11, 12, 13, 14]
101
+ )
102
+
103
+ second = slice_sampling_params(params, 2, 4)
104
+
105
+ assert second.temperature == [0.3, 0.4]
106
+ assert second.top_k == [3, 4]
107
+ assert second.top_p == [0.7, 0.8]
108
+ assert second.seed == [13, 14]
109
+ assert params.temperature == [0.1, 0.2, 0.3, 0.4]
110
+ assert slice_sampling_params(None, 0, 2) is None
111
+
112
+
113
+ def test_a_span_no_bucket_covers_reports_the_span():
114
+ """The caller's ``> max_batch_size`` guard has to fire and pick sequential prefill.
115
+
116
+ Reporting ``max_batch_size`` instead would leave batched prefill enabled and
117
+ scatter into a row the buffers do not have.
118
+ """
119
+ assert batched_prefill_padded_batch(2, [40], 32) == 41
120
+ assert batched_prefill_padded_batch(2, [40], 32) > 32
121
+ # A wider model still covers the slot, so batching stays on as it did before.
122
+ assert batched_prefill_padded_batch(2, [40], 64) == 64
code/models/tt_transformers/tests/test_ci_dispatch.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2023 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ import os
5
+
6
+ import pytest
7
+ from loguru import logger
8
+
9
+ from models.tt_transformers.tt.common import get_hf_tt_cache_path
10
+
11
+
12
+ # This test will run all the nightly fast dispatch tests for all supported TTT models in CI [N150 / N300 only]
13
+ @pytest.mark.parametrize(
14
+ "model_weights",
15
+ [
16
+ "meta-llama/Llama-3.2-1B-Instruct",
17
+ "meta-llama/Llama-3.2-3B-Instruct",
18
+ "meta-llama/Llama-3.1-8B-Instruct",
19
+ "meta-llama/Llama-3.2-11B-Vision-Instruct",
20
+ "mistralai/Mistral-7B-Instruct-v0.3",
21
+ ],
22
+ ids=[
23
+ "ttt-llama3.2-1B",
24
+ "ttt-llama3.2-3B",
25
+ "ttt-llama3.1-8B",
26
+ "ttt-llama3.2-11B",
27
+ "ttt-mistral-7B-v0.3",
28
+ ],
29
+ )
30
+ def test_ci_dispatch(model_weights):
31
+ logger.info(f"Running fast dispatch tests for {model_weights}")
32
+
33
+ os.environ["HF_MODEL"] = model_weights
34
+ os.environ["TT_CACHE_PATH"] = get_hf_tt_cache_path(model_weights)
35
+
36
+ # Pass the exit code of pytest to proper keep track of failures during runtime
37
+ exit_code = pytest.main(
38
+ [
39
+ "models/tt_transformers/tests/test_embedding.py",
40
+ "models/tt_transformers/tests/test_rms_norm.py",
41
+ "models/tt_transformers/tests/test_mlp.py",
42
+ "models/tt_transformers/tests/test_attention.py",
43
+ "models/tt_transformers/tests/test_attention_prefill.py",
44
+ "models/tt_transformers/tests/test_decoder.py",
45
+ "models/tt_transformers/tests/test_decoder_prefill.py",
46
+ ]
47
+ + ["-x"] # Fail if one of the tests fails
48
+ + (["--timeout", "600"] if "mistral" in model_weights.lower() else [])
49
+ )
50
+ if exit_code == pytest.ExitCode.TESTS_FAILED:
51
+ pytest.fail(
52
+ f"One or more CI dispatch tests failed for {model_weights}. Please check the log above for more info",
53
+ pytrace=False,
54
+ )
code/models/tt_transformers/tests/test_decoder.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2023 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ import os
5
+
6
+ import pytest
7
+ import torch
8
+ from loguru import logger
9
+
10
+ import ttnn
11
+ from models.common.utility_functions import comp_allclose, comp_pcc
12
+ from models.tt_transformers.tests.test_utils import get_ref_model_dype
13
+ from models.tt_transformers.tt.ccl import TT_CCL
14
+ from models.tt_transformers.tt.common import Mode, PagedAttentionConfig, precompute_freqs
15
+ from models.tt_transformers.tt.decoder import TransformerBlock
16
+ from models.tt_transformers.tt.model_config import ModelArgs
17
+ from models.tt_transformers.tt.prefetcher import Prefetcher
18
+ from models.tt_transformers.tt.rope import HfRotarySetup, RotarySetup
19
+
20
+
21
+ @torch.no_grad()
22
+ @pytest.mark.parametrize(
23
+ "use_prefetcher",
24
+ ([False]),
25
+ )
26
+ @pytest.mark.parametrize(
27
+ "mesh_device",
28
+ [
29
+ {"N150": (1, 1), "N300": (1, 2), "T3K": (1, 8), "TG": (8, 4)}.get(
30
+ os.environ.get("MESH_DEVICE"), len(ttnn.get_device_ids())
31
+ )
32
+ ],
33
+ indirect=True,
34
+ )
35
+ @pytest.mark.parametrize(
36
+ "paged_attention",
37
+ (
38
+ True,
39
+ # False
40
+ ),
41
+ ids=(
42
+ "paged_attention",
43
+ # "default_attention"
44
+ ),
45
+ )
46
+ @pytest.mark.parametrize(
47
+ "page_params",
48
+ [{"page_block_size": 32, "page_max_num_blocks": 1024}],
49
+ )
50
+ @pytest.mark.parametrize(
51
+ "batch_size",
52
+ (1, 32),
53
+ )
54
+ @pytest.mark.parametrize(
55
+ "max_seq_len",
56
+ (256,), # For decode-only unit test, there's no need to run with large sequence lengths
57
+ )
58
+ @pytest.mark.parametrize(
59
+ "generation_length",
60
+ (10,), # For decode-only unit test, there's no need to run with large sequence lengths
61
+ )
62
+ @pytest.mark.parametrize("device_params", [{"fabric_config": True}], indirect=True)
63
+ def test_decoder_inference(
64
+ max_seq_len,
65
+ batch_size,
66
+ paged_attention,
67
+ page_params,
68
+ mesh_device,
69
+ reset_seeds,
70
+ ensure_gc,
71
+ generation_length,
72
+ use_prefetcher,
73
+ ):
74
+ dtype = ttnn.bfloat8_b
75
+
76
+ mode = Mode.DECODE
77
+ num_tensors = 5 if use_prefetcher else 0
78
+ prefetcher = Prefetcher(mesh_device, num_tensors=num_tensors, num_layers=1) if use_prefetcher else None
79
+
80
+ if use_prefetcher:
81
+ prefetcher.init(mode=mode)
82
+
83
+ model_args = ModelArgs(
84
+ mesh_device,
85
+ max_batch_size=batch_size,
86
+ max_seq_len=max_seq_len,
87
+ cache_hf=True,
88
+ prefetcher=prefetcher,
89
+ use_hf_rope=False,
90
+ )
91
+ model_args.n_layers = 1
92
+
93
+ state_dict = model_args.load_state_dict()
94
+ reference_model = model_args.reference_decoder(load_checkpoint=True)
95
+
96
+ generation_start_pos = 0
97
+ all_tests_pass = True
98
+
99
+ # Setup RoPE transformation matrices
100
+ DefaultRopeSetup = HfRotarySetup if model_args.use_hf_rope else RotarySetup
101
+ rope_setup = DefaultRopeSetup(
102
+ mesh_device,
103
+ model_args.max_batch_size,
104
+ model_args.head_dim,
105
+ model_args.max_seq_len,
106
+ model_args.rope_theta,
107
+ model_args.rope_scaling,
108
+ model_args.use_qk_fused,
109
+ prefetcher=prefetcher,
110
+ )
111
+
112
+ if model_args.rope_theta_local is not None:
113
+ rope_setup_local = RotarySetup(
114
+ mesh_device,
115
+ model_args.max_batch_size,
116
+ model_args.head_dim,
117
+ model_args.max_seq_len,
118
+ model_args.rope_theta_local,
119
+ None,
120
+ # Match model.py: when fused QK is on, the local rope must also double
121
+ # its cos/sin batch so one tensor serves both Q and K (Gemma-2 sliding layers).
122
+ use_qk_fused=model_args.use_qk_fused,
123
+ )
124
+ else:
125
+ rope_setup_local = None
126
+
127
+ transformation_mats = rope_setup.get_both_trans_mats()
128
+
129
+ # Prepare page table for paged attention
130
+ page_table_tt = None
131
+ paged_attention_config = None
132
+
133
+ if paged_attention:
134
+ paged_attention_config = PagedAttentionConfig(
135
+ block_size=page_params["page_block_size"],
136
+ max_num_blocks=page_params["page_max_num_blocks"],
137
+ )
138
+ # Implied shuffling of blocks
139
+ permutation = torch.randperm(paged_attention_config.max_num_blocks)
140
+ # Page table which maps virtual blocks to physical
141
+ reverse_permutation = torch.argsort(permutation)
142
+ page_table = reverse_permutation.reshape(
143
+ model_args.max_batch_size, paged_attention_config.max_num_blocks // model_args.max_batch_size
144
+ )
145
+ page_table_tt = ttnn.from_torch(
146
+ page_table,
147
+ device=mesh_device,
148
+ dtype=ttnn.int32,
149
+ layout=ttnn.ROW_MAJOR_LAYOUT,
150
+ mesh_mapper=ttnn.ShardTensor2dMesh(
151
+ mesh_device,
152
+ dims=(None, -2) if (model_args.is_galaxy and batch_size > 1) else (None, None),
153
+ mesh_shape=model_args.cluster_shape,
154
+ ),
155
+ )
156
+
157
+ # Initialize TT model
158
+ tt_ccl = TT_CCL(mesh_device)
159
+ tt_model = TransformerBlock(
160
+ args=model_args,
161
+ mesh_device=mesh_device,
162
+ tt_ccl=tt_ccl,
163
+ dtype=dtype,
164
+ state_dict=state_dict,
165
+ layer_num=0,
166
+ weight_cache_path=model_args.weight_cache_path(dtype),
167
+ transformation_mats=transformation_mats,
168
+ paged_attention_config=paged_attention_config,
169
+ prefetcher=prefetcher,
170
+ )
171
+ if use_prefetcher:
172
+ tt_model.prefetcher.prefetch()
173
+
174
+ seqlen = 1
175
+
176
+ # Precompute freqs_cis for reference model
177
+ cos, sin = precompute_freqs(
178
+ model_args.head_dim,
179
+ model_args.max_seq_len * 2,
180
+ model_args.rope_theta,
181
+ model_args.rope_scaling.factor if model_args.rope_scaling else None,
182
+ model_args.rope_scaling.original_max_position_embeddings if model_args.rope_scaling else None,
183
+ model_args.rope_scaling.rope_type.value if model_args.rope_scaling else "llama3",
184
+ )
185
+ freqs_cis = torch.complex(cos, sin)
186
+
187
+ # Initial positions
188
+ current_pos = torch.tensor([generation_start_pos for _ in range(batch_size)])
189
+ current_pos_tensor = ttnn.from_torch(
190
+ current_pos,
191
+ device=mesh_device,
192
+ dtype=ttnn.int32,
193
+ mesh_mapper=ttnn.ShardTensor2dMesh(
194
+ mesh_device,
195
+ dims=(None, 0) if (model_args.is_galaxy and batch_size > 1) else (None, None),
196
+ mesh_shape=model_args.cluster_shape,
197
+ ),
198
+ )
199
+ for i in range(generation_length):
200
+ logger.info(f"[Decoder] Generating token {i}")
201
+
202
+ if prefetcher is not None:
203
+ prefetcher.run()
204
+
205
+ # input = torch.randn(1, 32, 4096)
206
+ pt_decode_input = (
207
+ torch.rand(
208
+ batch_size, seqlen, model_args.dim, dtype=get_ref_model_dype(reference_model, model_args.model_name)
209
+ )
210
+ * 2
211
+ ) - 1
212
+ tt_decode_input = pt_decode_input.clone()
213
+
214
+ decode_input = model_args.prepare_residual_tensor_decode(
215
+ tt_decode_input,
216
+ model_args.get_residual_mem_config(mode, prefetcher),
217
+ )
218
+
219
+ # Get cos/sin matrices for the current position of each user
220
+ rot_mats = rope_setup.get_rot_mats(current_pos)
221
+ rot_mats_local = None if rope_setup_local is None else rope_setup_local.get_rot_mats(current_pos)
222
+
223
+ # Run TT model
224
+ tt_out = tt_model(
225
+ decode_input,
226
+ current_pos_tensor,
227
+ rot_mats_global=rot_mats,
228
+ rot_mats_local=rot_mats_local,
229
+ mode=mode,
230
+ page_table=page_table_tt,
231
+ )
232
+
233
+ tt_out = ttnn.to_torch(
234
+ tt_out,
235
+ mesh_composer=ttnn.ConcatMesh2dToTensor(mesh_device, dims=(1, 3), mesh_shape=model_args.cluster_shape),
236
+ )
237
+
238
+ tt_output_torch = tt_out[:, 0:1, : model_args.max_batch_size, : model_args.dim].view(-1, 1, model_args.dim)
239
+
240
+ # In this test all users have the same position
241
+ freqs_cis_i = freqs_cis[current_pos[0], :].unsqueeze(0)
242
+
243
+ # Reference model
244
+ ref_output = reference_model(pt_decode_input, current_pos[0], freqs_cis_i, mask=None)
245
+ if ref_output.dim() == 2:
246
+ ref_output = ref_output.unsqueeze(1)
247
+
248
+ # For some model variants the HF decoder returns output only for the first batch item.
249
+ # Since all users share the same position in this test, compare the first ref_output.shape[0]
250
+ # items from TT output to ref_output.
251
+ batch_cmp = ref_output.shape[0]
252
+ tt_output_cmp = tt_output_torch[:batch_cmp]
253
+ passing, pcc_message = comp_pcc(ref_output, tt_output_cmp)
254
+
255
+ logger.info(comp_allclose(ref_output, tt_output_cmp))
256
+ logger.info(f"PCC: {pcc_message}")
257
+
258
+ if passing:
259
+ logger.info("Decoder Block Passed!")
260
+ else:
261
+ logger.warning("Decoder Block Failed!")
262
+ all_tests_pass = False
263
+
264
+ # Increment position
265
+ current_pos = torch.tensor([generation_start_pos + i + 1 for _ in range(batch_size)])
266
+ current_pos_tensor = ttnn.from_torch(
267
+ current_pos,
268
+ device=mesh_device,
269
+ dtype=ttnn.int32,
270
+ mesh_mapper=ttnn.ShardTensor2dMesh(
271
+ mesh_device,
272
+ dims=(None, 0) if (model_args.is_galaxy and batch_size > 1) else (None, None),
273
+ mesh_shape=model_args.cluster_shape,
274
+ ),
275
+ )
276
+
277
+ if all_tests_pass:
278
+ logger.info(f"All {generation_length} decode iterations Passed!")
279
+ else:
280
+ logger.warning("One or more iterations of decode Failed!")
281
+ assert all_tests_pass, f"PCC value is lower than {0.99} for some of the outputs. Check Warnings!"
code/models/tt_transformers/tests/test_device_perf.py ADDED
@@ -0,0 +1,362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ import json
5
+ import os
6
+
7
+ import pandas as pd
8
+ import pytest
9
+ from loguru import logger
10
+
11
+ from models.perf.benchmarking_utils import BenchmarkData, BenchmarkProfiler
12
+ from models.perf.device_perf_utils import run_device_perf
13
+ from models.tt_transformers.tests.test_utils import (
14
+ merge_device_rows,
15
+ print_dict,
16
+ process_measurements,
17
+ split_compile_and_trace,
18
+ verify_value_within_margin,
19
+ )
20
+ from tools.tracy.common import PROFILER_DEFAULT_OP_SUPPORT_COUNT
21
+ from tools.tracy.process_model_log import get_latest_ops_log_filename
22
+
23
+
24
+ # This pytest flag is necessary to ensure that we do NOT open the device in the main process for device perf tests that run
25
+ # the test inside a subprocess since UMD does not allow multiple subprocesses opening the device at the same time.
26
+ @pytest.mark.no_reset_default_device
27
+ @pytest.mark.timeout(600)
28
+ @pytest.mark.parametrize("export_measurements", [True, False])
29
+ @pytest.mark.parametrize("batch_size", [1, 32])
30
+ @pytest.mark.parametrize("data_parallel", [1, 2, 4, 8])
31
+ @pytest.mark.parametrize("num_layers", [2, 10])
32
+ @pytest.mark.parametrize("num_runs", [2])
33
+ @pytest.mark.parametrize("max_seq_len", [1024, 32768, 131072])
34
+ @pytest.mark.parametrize("max_generated_tokens", [2])
35
+ @pytest.mark.parametrize("model_name", ["llama3_70b", "llama3_8b", "qwen3_32b"]) # Add more models here as needed
36
+ @pytest.mark.parametrize("mode", ["prefill", "decode"])
37
+ def test_device_perf_one_iter(
38
+ num_layers,
39
+ model_name,
40
+ batch_size,
41
+ data_parallel,
42
+ max_seq_len,
43
+ mode,
44
+ num_runs,
45
+ max_generated_tokens,
46
+ export_measurements,
47
+ ):
48
+ cmd = f"pytest models/tt_transformers/demo/simple_text_demo.py -k 'device-perf and performance' --num_layers {num_layers} --data_parallel {data_parallel} --max_seq_len {max_seq_len} --max_generated_tokens {max_generated_tokens} --paged_attention 1 --batch_size {batch_size} --mode {mode} --use_prefetcher True"
49
+ cols = ["DEVICE FW", "DEVICE KERNEL", "DEVICE BRISC KERNEL"]
50
+ device_analysis_types = ["device_kernel_duration", "device_kernel_first_to_last_start"]
51
+ subdir = f"ttt-device-perf-{mode}"
52
+ profiler = BenchmarkProfiler()
53
+ benchmark_data = BenchmarkData()
54
+ profiler.start("run")
55
+ profiler.start("decoder-perf-op-metrics")
56
+
57
+ # Load perf targets
58
+ perf_targets = {}
59
+ try:
60
+ with open(f"models/tt_transformers/tests/perf_targets/device_perf_{mode}.json", "r") as f:
61
+ perf_targets = json.load(f)
62
+ except FileNotFoundError:
63
+ logger.error(
64
+ f"Perf targets file not found, device perf test will proceed without performance target comparison"
65
+ )
66
+
67
+ # Large sequence lengths dispatch many more programs than the default profiler buffer
68
+ # (1333 slots) can hold, causing buffer overflow and missing device perf data.
69
+ # Scale op_support_count proportionally with max_seq_len (empirically, seq=131072 needs ~10000).
70
+ op_support_count = max(
71
+ int(PROFILER_DEFAULT_OP_SUPPORT_COUNT * 1.333), # preserve previous minimum
72
+ int(max_seq_len / 10),
73
+ )
74
+
75
+ _ = run_device_perf(
76
+ cmd,
77
+ subdir,
78
+ num_iterations=1,
79
+ cols=cols,
80
+ batch_size=batch_size,
81
+ device_analysis_types=device_analysis_types,
82
+ op_support_count=op_support_count,
83
+ )
84
+
85
+ profiler.end("decoder-perf-op-metrics")
86
+ profiler.end("run")
87
+
88
+ # Parse the latest ops CSV and aggregate per-op metrics
89
+ filename = get_latest_ops_log_filename(subdir)
90
+ df = pd.read_csv(filename)
91
+ df = df[df["OP TYPE"].isin(["tt_dnn_device"])]
92
+ df = merge_device_rows(df)
93
+
94
+ # Split compile and trace
95
+ (
96
+ df_model_compilation,
97
+ df_model_trace,
98
+ df_first_layer_compilation,
99
+ df_first_layer_trace,
100
+ df_mid_layers_compilation,
101
+ df_mid_layers_trace,
102
+ df_model_tail_compilation,
103
+ df_model_tail_trace,
104
+ ) = split_compile_and_trace(
105
+ df,
106
+ mode=mode,
107
+ num_runs=num_runs,
108
+ num_layers=num_layers,
109
+ )
110
+
111
+ (
112
+ kernel_agg_first_layer_compile,
113
+ dispatch_agg_first_layer_compile,
114
+ firstlast_agg_first_layer_compile,
115
+ ) = process_measurements(df_first_layer_compilation, 1)
116
+ (
117
+ kernel_agg_first_layer_trace,
118
+ dispatch_agg_first_layer_trace,
119
+ firstlast_agg_first_layer_trace,
120
+ ) = process_measurements(df_first_layer_trace, 1)
121
+
122
+ if num_layers > 1:
123
+ (
124
+ kernel_agg_mid_layers_compile,
125
+ dispatch_agg_mid_layers_compile,
126
+ firstlast_agg_mid_layers_compile,
127
+ ) = process_measurements(
128
+ df_mid_layers_compilation, num_layers - 1
129
+ ) # we dont count the first layer
130
+
131
+ (
132
+ kernel_agg_mid_layers_trace,
133
+ dispatch_agg_mid_layers_trace,
134
+ firstlast_agg_mid_layers_trace,
135
+ ) = process_measurements(df_mid_layers_trace, num_layers - 1)
136
+
137
+ if df_model_tail_compilation is not None:
138
+ (
139
+ kernel_agg_model_tail_compile,
140
+ dispatch_agg_model_tail_compile,
141
+ firstlast_agg_model_tail_compile,
142
+ ) = process_measurements(df_model_tail_compilation, 1)
143
+ (
144
+ kernel_agg_model_tail_trace,
145
+ dispatch_agg_model_tail_trace,
146
+ firstlast_agg_model_tail_trace,
147
+ ) = process_measurements(df_model_tail_trace, 1)
148
+
149
+ # Print measurements
150
+ print_dict(kernel_agg_first_layer_compile, "KERNEL AVERAGE DURATION FOR FIRST LAYER COMPILE")
151
+ print_dict(kernel_agg_first_layer_trace, "KERNEL AVERAGE DURATION FOR FIRST LAYER TRACE")
152
+
153
+ if num_layers > 1:
154
+ print_dict(kernel_agg_mid_layers_compile, "KERNEL AVERAGE DURATION FOR MID LAYERS COMPILE")
155
+ print_dict(kernel_agg_mid_layers_trace, "KERNEL AVERAGE DURATION FOR MID LAYERS TRACE")
156
+ print_dict(dispatch_agg_mid_layers_trace, "DISPATCH AVERAGE DURATION FOR MID LAYERS TRACE")
157
+ print_dict(firstlast_agg_mid_layers_trace, "FIRST TO LAST AVERAGE START TIME FOR MID LAYERS TRACE")
158
+
159
+ if df_model_tail_compilation is not None:
160
+ print_dict(kernel_agg_model_tail_compile, "KERNEL AVERAGE DURATION FOR MODEL TAIL COMPILE")
161
+ print_dict(kernel_agg_model_tail_trace, "KERNEL AVERAGE DURATION FOR MODEL TAIL TRACE")
162
+ print_dict(dispatch_agg_model_tail_trace, "DISPATCH AVERAGE DURATION FOR MODEL TAIL TRACE")
163
+ print_dict(firstlast_agg_model_tail_trace, "FIRST TO LAST AVERAGE START TIME FOR MODEL TAIL TRACE")
164
+
165
+ # Prefer trace for collectives, compile for others
166
+ def is_collective(op_code: str) -> bool:
167
+ return any(x in op_code for x in ("AllGather", "ReduceScatter", "AllReduce", "Matmul_RS"))
168
+
169
+ # Prepare export structure and default margins
170
+ perf_measurements_export: dict[str, dict[str, dict[str, float]]] = {}
171
+
172
+ # Export metrics for an op group (first layer, mid layers, model tail)
173
+ def export_group(
174
+ group_name: str,
175
+ kernel_agg_compile: dict,
176
+ kernel_agg_trace: dict,
177
+ dispatch_agg_trace: dict,
178
+ firstlast_agg_trace: dict | None,
179
+ ):
180
+ all_passing = True
181
+ perf_measurements_export[group_name] = {}
182
+ op_codes = set(list(kernel_agg_compile["avg"].keys()) + list(kernel_agg_trace["avg"].keys()))
183
+ for op_code in op_codes:
184
+ # kernel avg
185
+ k_avg_trace = kernel_agg_trace["avg"].get(op_code)
186
+ k_min_trace = kernel_agg_trace["min"].get(op_code)
187
+ k_max_trace = kernel_agg_trace["max"].get(op_code)
188
+ k_avg_comp = kernel_agg_compile["avg"].get(op_code)
189
+ k_min_comp = kernel_agg_compile["min"].get(op_code)
190
+ k_max_comp = kernel_agg_compile["max"].get(op_code)
191
+
192
+ if is_collective(op_code):
193
+ k_avg = k_avg_trace if k_avg_trace is not None else k_avg_comp
194
+ k_min = k_min_trace if k_min_trace is not None else k_min_comp
195
+ k_max = k_max_trace if k_max_trace is not None else k_max_comp
196
+ else:
197
+ k_avg = k_avg_comp if k_avg_comp is not None else k_avg_trace
198
+ k_min = k_min_comp if k_min_comp is not None else k_min_trace
199
+ k_max = k_max_comp if k_max_comp is not None else k_max_trace
200
+
201
+ if k_avg is not None:
202
+ benchmark_data.add_measurement(
203
+ profiler, 0, "decoder-perf-op-metrics", f"{op_code}-{group_name}-kernel-avg", float(k_avg)
204
+ )
205
+ if k_min is not None:
206
+ benchmark_data.add_measurement(
207
+ profiler, 0, "decoder-perf-op-metrics", f"{op_code}-{group_name}-kernel-min", float(k_min)
208
+ )
209
+ if k_max is not None:
210
+ benchmark_data.add_measurement(
211
+ profiler, 0, "decoder-perf-op-metrics", f"{op_code}-{group_name}-kernel-max", float(k_max)
212
+ )
213
+
214
+ # Initialize export entry for this op
215
+ export_entry: dict[str, float] = {}
216
+ if k_avg is not None:
217
+ export_entry["kernel_duration"] = float(k_avg)
218
+
219
+ # Check that perf_targets, group_name, and op_code exist and keys exist
220
+ if perf_targets and group_name in perf_targets and op_code in perf_targets[group_name]:
221
+ passing = verify_value_within_margin(
222
+ k_avg,
223
+ perf_targets[group_name][op_code]["kernel_duration"],
224
+ perf_targets[group_name][op_code]["kernel_duration_relative_margin"],
225
+ op_code,
226
+ "kernel",
227
+ )
228
+ all_passing = all_passing and passing
229
+ else:
230
+ logger.warning(f"Warning: {op_code}-{group_name}-kernel not found in perf_targets")
231
+ # dispatch from trace only
232
+ d_avg = dispatch_agg_trace["avg"].get(op_code)
233
+ d_min = dispatch_agg_trace["min"].get(op_code)
234
+ d_max = dispatch_agg_trace["max"].get(op_code)
235
+ if d_avg is not None:
236
+ benchmark_data.add_measurement(
237
+ profiler, 0, "decoder-perf-op-metrics", f"{op_code}-{group_name}-op_to_op-avg", float(d_avg)
238
+ )
239
+ if d_min is not None:
240
+ benchmark_data.add_measurement(
241
+ profiler, 0, "decoder-perf-op-metrics", f"{op_code}-{group_name}-op_to_op-min", float(d_min)
242
+ )
243
+ if d_max is not None:
244
+ benchmark_data.add_measurement(
245
+ profiler, 0, "decoder-perf-op-metrics", f"{op_code}-{group_name}-op_to_op-max", float(d_max)
246
+ )
247
+
248
+ if d_avg is not None:
249
+ export_entry["op_to_op"] = float(d_avg)
250
+
251
+ if perf_targets and group_name in perf_targets and op_code in perf_targets[group_name]:
252
+ passing = verify_value_within_margin(
253
+ d_avg,
254
+ perf_targets[group_name][op_code]["op_to_op"],
255
+ perf_targets[group_name][op_code]["op_to_op_duration_relative_margin"],
256
+ op_code,
257
+ "op_to_op",
258
+ )
259
+ all_passing = all_passing and passing
260
+ else:
261
+ logger.warning(f"Warning: {op_code}-{group_name}-op_to_op not found in perf_targets")
262
+
263
+ # first_to_last from trace only (if provided)
264
+ if firstlast_agg_trace is not None:
265
+ fl_avg = firstlast_agg_trace["avg"].get(op_code)
266
+ fl_min = firstlast_agg_trace["min"].get(op_code)
267
+ fl_max = firstlast_agg_trace["max"].get(op_code)
268
+ if fl_avg is not None:
269
+ benchmark_data.add_measurement(
270
+ profiler,
271
+ 0,
272
+ "decoder-perf-op-metrics",
273
+ f"{op_code}-{group_name}-first_to_last-avg",
274
+ float(fl_avg),
275
+ )
276
+ if fl_min is not None:
277
+ benchmark_data.add_measurement(
278
+ profiler,
279
+ 0,
280
+ "decoder-perf-op-metrics",
281
+ f"{op_code}-{group_name}-first_to_last-min",
282
+ float(fl_min),
283
+ )
284
+ if fl_max is not None:
285
+ benchmark_data.add_measurement(
286
+ profiler,
287
+ 0,
288
+ "decoder-perf-op-metrics",
289
+ f"{op_code}-{group_name}-first_to_last-max",
290
+ float(fl_max),
291
+ )
292
+
293
+ if fl_avg is not None:
294
+ export_entry["first_to_last_start"] = float(fl_avg)
295
+
296
+ if perf_targets and group_name in perf_targets and op_code in perf_targets[group_name]:
297
+ passing = verify_value_within_margin(
298
+ fl_avg,
299
+ perf_targets[group_name][op_code]["first_to_last_start"],
300
+ perf_targets[group_name][op_code]["first_to_last_start_relative_margin"],
301
+ op_code,
302
+ "first_to_last_start",
303
+ )
304
+ all_passing = all_passing and passing
305
+ else:
306
+ logger.warning(f"Warning: {op_code}-{group_name}-first_to_last not found in perf_targets")
307
+ # Save the entry for this op if we collected any metrics
308
+ if export_entry:
309
+ perf_measurements_export[group_name][op_code] = export_entry
310
+ return all_passing
311
+
312
+ # Export per-op metrics for each group
313
+ all_passing = True
314
+ all_passing = all_passing and export_group(
315
+ group_name=f"decoder-first",
316
+ kernel_agg_compile=kernel_agg_first_layer_compile,
317
+ kernel_agg_trace=kernel_agg_first_layer_trace,
318
+ dispatch_agg_trace=dispatch_agg_first_layer_trace,
319
+ firstlast_agg_trace=firstlast_agg_first_layer_trace,
320
+ )
321
+
322
+ if num_layers > 1:
323
+ all_passing = all_passing and export_group(
324
+ group_name=f"decoder-mid",
325
+ kernel_agg_compile=kernel_agg_mid_layers_compile,
326
+ kernel_agg_trace=kernel_agg_mid_layers_trace,
327
+ dispatch_agg_trace=dispatch_agg_mid_layers_trace,
328
+ firstlast_agg_trace=firstlast_agg_mid_layers_trace,
329
+ )
330
+ if df_model_tail_compilation is not None:
331
+ all_passing = all_passing and export_group(
332
+ group_name=f"model-tail",
333
+ kernel_agg_compile=kernel_agg_model_tail_compile,
334
+ kernel_agg_trace=kernel_agg_model_tail_trace,
335
+ dispatch_agg_trace=dispatch_agg_model_tail_trace,
336
+ firstlast_agg_trace=None, # align with decoder tail export (no first_to_last)
337
+ )
338
+
339
+ # Write out current measurements in the perf-measurements JSON schema
340
+ if export_measurements:
341
+ try:
342
+ output_dir = os.path.join(os.path.dirname(__file__), "perf_measurements")
343
+ os.makedirs(output_dir, exist_ok=True)
344
+ output_path = os.path.join(
345
+ output_dir,
346
+ f"device_perf_measurements_{model_name}_{mode}_bs{batch_size}_dp{data_parallel}_layers{num_layers}_seq{max_seq_len}.json",
347
+ )
348
+ with open(output_path, "w", encoding="utf-8") as f:
349
+ json.dump(perf_measurements_export, f, indent=4, sort_keys=True)
350
+ logger.info(f"Wrote perf measurements export to {output_path}")
351
+ except Exception as e:
352
+ logger.error(f"Failed to write perf measurements export: {e}")
353
+
354
+ # Save partial run
355
+ benchmark_data.save_partial_run_json(
356
+ profiler,
357
+ run_type="ttnn_decoder_unit",
358
+ ml_model_name=f"{model_name}-{mode}-{data_parallel}dp-{num_layers}layers-{max_seq_len}seq",
359
+ )
360
+
361
+ # No strict assertions on perf; test succeeds if profiling and export ran
362
+ assert True
code/models/tt_transformers/tests/test_hybrid_attention_for_causal_lm.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Unit tests for ``HybridAttentionForCausalLM``.
4
+
5
+ The class is the vLLM wrapper base for hybrid attention models (Gemma3,
6
+ Gemma4, GPT-OSS, ...). The bulk of its responsibility is the
7
+ ``get_kv_cache_spec`` classmethod that translates ``layer_types`` from
8
+ HF config into per-layer KVCacheSpecs that upstream's hybrid kv cache
9
+ manager groups by attention type. This test pins that translation
10
+ across the typical patterns we'll see on real models.
11
+ """
12
+
13
+ import sys
14
+ from types import SimpleNamespace
15
+ from unittest.mock import MagicMock
16
+
17
+ import pytest
18
+ import torch
19
+
20
+ # Stub ttnn so importing generator_vllm doesn't blow up on the local
21
+ # tt-metal C++ extension. We don't exercise any real ttnn behaviour here.
22
+ sys.modules.setdefault("ttnn", MagicMock(name="ttnn-test-mock"))
23
+ sys.modules.setdefault("ttnn._ttnn", MagicMock(name="ttnn._ttnn-test-mock"))
24
+
25
+
26
+ def _make_vllm_config(layer_types, sliding_window=1024, num_kv_heads=8, head_size=128):
27
+ text_config = SimpleNamespace(layer_types=layer_types, sliding_window=sliding_window)
28
+ hf_config = SimpleNamespace(text_config=text_config)
29
+ cfg = MagicMock()
30
+ cfg.model_config.hf_config = hf_config
31
+ cfg.model_config.dtype = torch.bfloat16
32
+ cfg.model_config.get_num_kv_heads.return_value = num_kv_heads
33
+ cfg.model_config.get_head_size.return_value = head_size
34
+ cfg.cache_config.cache_dtype = "auto"
35
+ cfg.cache_config.block_size = 64
36
+ return cfg
37
+
38
+
39
+ def test_spec_emits_one_entry_per_layer():
40
+ """KV cache groups temporarily disabled: every layer is FullAttentionSpec
41
+ regardless of layer_types entry. Reverts to one uniform spec until the
42
+ bounded-sliding-cache decode bug is fixed."""
43
+ from vllm.v1.kv_cache_interface import FullAttentionSpec, SlidingWindowSpec
44
+
45
+ from models.tt_transformers.tt.generator_vllm import HybridAttentionForCausalLM
46
+
47
+ layers = ["sliding_attention"] * 5 + ["full_attention"] + ["sliding_attention"] * 5
48
+ spec = HybridAttentionForCausalLM.get_kv_cache_spec(_make_vllm_config(layers))
49
+
50
+ assert len(spec) == len(layers)
51
+ for i in range(len(layers)):
52
+ name = f"model.layers.{i}.self_attn"
53
+ assert name in spec
54
+ assert isinstance(spec[name], FullAttentionSpec)
55
+ assert not isinstance(spec[name], SlidingWindowSpec)
56
+
57
+
58
+ def test_spec_gemma3_27b_pattern():
59
+ """All layers are FullAttentionSpec while kv cache groups are disabled."""
60
+ from vllm.v1.kv_cache_interface import FullAttentionSpec, SlidingWindowSpec
61
+
62
+ from models.tt_transformers.tt.generator_vllm import HybridAttentionForCausalLM
63
+
64
+ pattern = ["sliding_attention"] * 5 + ["full_attention"]
65
+ layers = pattern * 10 # 60 layers
66
+ spec = HybridAttentionForCausalLM.get_kv_cache_spec(_make_vllm_config(layers))
67
+
68
+ full_count = sum(isinstance(v, FullAttentionSpec) for v in spec.values())
69
+ sliding_count = sum(isinstance(v, SlidingWindowSpec) for v in spec.values())
70
+ assert full_count == 60
71
+ assert sliding_count == 0
72
+
73
+
74
+ def test_spec_gpt_oss_alternating_pattern():
75
+ from vllm.v1.kv_cache_interface import FullAttentionSpec, SlidingWindowSpec
76
+
77
+ from models.tt_transformers.tt.generator_vllm import HybridAttentionForCausalLM
78
+
79
+ layers = ["sliding_attention", "full_attention"] * 12 # 24 layers
80
+ spec = HybridAttentionForCausalLM.get_kv_cache_spec(_make_vllm_config(layers))
81
+
82
+ full_count = sum(isinstance(v, FullAttentionSpec) for v in spec.values())
83
+ sliding_count = sum(isinstance(v, SlidingWindowSpec) for v in spec.values())
84
+ assert full_count == 24
85
+ assert sliding_count == 0
86
+
87
+
88
+ def test_spec_uniform_full_attention_still_works():
89
+ """All-full layer_types → single-type config; spec generation still succeeds."""
90
+ from vllm.v1.kv_cache_interface import FullAttentionSpec, SlidingWindowSpec
91
+
92
+ from models.tt_transformers.tt.generator_vllm import HybridAttentionForCausalLM
93
+
94
+ spec = HybridAttentionForCausalLM.get_kv_cache_spec(_make_vllm_config(["full_attention"] * 4))
95
+ assert all(isinstance(v, FullAttentionSpec) for v in spec.values())
96
+ assert not any(isinstance(v, SlidingWindowSpec) for v in spec.values())
97
+
98
+
99
+ def test_spec_propagates_kv_heads_and_head_size():
100
+ from models.tt_transformers.tt.generator_vllm import HybridAttentionForCausalLM
101
+
102
+ spec = HybridAttentionForCausalLM.get_kv_cache_spec(
103
+ _make_vllm_config(["full_attention", "sliding_attention"], num_kv_heads=4, head_size=256)
104
+ )
105
+
106
+ for layer_spec in spec.values():
107
+ assert layer_spec.num_kv_heads == 4
108
+ assert layer_spec.head_size == 256
109
+ assert layer_spec.block_size == 64
110
+ assert layer_spec.dtype == torch.bfloat16
111
+
112
+
113
+ def test_spec_missing_layer_types_raises():
114
+ from models.tt_transformers.tt.generator_vllm import HybridAttentionForCausalLM
115
+
116
+ cfg = _make_vllm_config(["full_attention"])
117
+ cfg.model_config.hf_config.text_config.layer_types = None
118
+
119
+ with pytest.raises(ValueError, match="layer_types"):
120
+ HybridAttentionForCausalLM.get_kv_cache_spec(cfg)
121
+
122
+
123
+ def test_spec_unknown_layer_type_raises():
124
+ from models.tt_transformers.tt.generator_vllm import HybridAttentionForCausalLM
125
+
126
+ cfg = _make_vllm_config(["full_attention", "rotary_chunked_xyz"])
127
+
128
+ with pytest.raises(ValueError, match="Unsupported layer_type"):
129
+ HybridAttentionForCausalLM.get_kv_cache_spec(cfg)
130
+
131
+
132
+ def test_subclass_must_override_prefill_and_decode():
133
+ """The base class's prefill_forward / decode_forward are explicit
134
+ NotImplementedError stubs — subclasses must provide model-specific
135
+ routing that consumes ``page_tables_per_group``."""
136
+ from models.tt_transformers.tt.generator_vllm import HybridAttentionForCausalLM
137
+
138
+ instance = HybridAttentionForCausalLM.__new__(HybridAttentionForCausalLM)
139
+
140
+ with pytest.raises(NotImplementedError, match="prefill_forward"):
141
+ instance.prefill_forward()
142
+ with pytest.raises(NotImplementedError, match="decode_forward"):
143
+ instance.decode_forward()
code/models/tt_transformers/tests/test_interleaved_to_sharded.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2023 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ import os
5
+
6
+ import pytest
7
+ import torch
8
+ from loguru import logger
9
+
10
+ import ttnn
11
+ from models.tt_transformers.tt.ccl import TT_CCL
12
+ from models.tt_transformers.tt.model_config import ModelArgs
13
+
14
+
15
+ @torch.no_grad()
16
+ @pytest.mark.parametrize(
17
+ "mesh_device",
18
+ [
19
+ {"N150": (1, 1), "N300": (1, 2), "T3K": (1, 8), "TG": (8, 4)}.get(
20
+ os.environ.get("MESH_DEVICE"), len(ttnn.get_device_ids())
21
+ )
22
+ ],
23
+ indirect=True,
24
+ )
25
+ @pytest.mark.parametrize("device_params", [{"fabric_config": True}], indirect=True)
26
+ def test_decoder_inference(mesh_device, reset_seeds):
27
+ model_args = ModelArgs(mesh_device, cache_hf=True)
28
+ state_dict = torch.load(model_args.consolidated_weights_path, map_location=torch.device("cpu"))
29
+
30
+ # Ref model needs partial state dict, but our models use full state dict keys as cached weight names
31
+ first_layer_prefix = model_args.get_state_dict_prefix("TransformerBlock", 0)
32
+ partial_state_dict = {
33
+ k[len(first_layer_prefix) :]: v for k, v in state_dict.items() if (k.startswith(first_layer_prefix))
34
+ }
35
+ reference_model = model_args.reference_decoder()
36
+ reference_model.load_state_dict(partial_state_dict)
37
+
38
+ generation_length = 10
39
+
40
+ seqlen = 1
41
+ batch = model_args.max_batch_size
42
+
43
+ tt_ccl = TT_CCL(mesh_device)
44
+ for i in range(generation_length):
45
+ logger.info(f"[Decoder] Generating token {i}")
46
+
47
+ # input = torch.randn(1, 32, 4096)
48
+ pt_decode_input = (torch.rand(batch, seqlen, model_args.dim) * 2) - 1
49
+ tt_decode_input = pt_decode_input.clone()
50
+
51
+ decode_input = model_args.prepare_residual_tensor_decode(
52
+ tt_decode_input,
53
+ ttnn.L1_MEMORY_CONFIG,
54
+ )
55
+
56
+ dim = 2048
57
+
58
+ attn_input_grid = ttnn.CoreGrid(y=2, x=8)
59
+ mem_cfg = ttnn.create_sharded_memory_config(
60
+ (
61
+ 32,
62
+ dim // attn_input_grid.num_cores,
63
+ ),
64
+ attn_input_grid,
65
+ ttnn.ShardStrategy.WIDTH,
66
+ ttnn.ShardOrientation.ROW_MAJOR,
67
+ use_height_and_width_as_shard_shape=True,
68
+ )
69
+
70
+ # Run TT model
71
+ tt_out = ttnn.experimental.all_gather_async(
72
+ decode_input,
73
+ persistent_output_buffer=None,
74
+ dim=3,
75
+ multi_device_global_semaphore=tt_ccl.get_and_cycle_ag_semaphore_handles(),
76
+ num_links=1,
77
+ topology=model_args.ccl_topology(),
78
+ memory_config=mem_cfg,
79
+ barrier_semaphore=tt_ccl.get_and_cycle_barrier_semaphore_handle(),
80
+ chunks_per_sync=10,
81
+ num_workers_per_link=2,
82
+ num_buffers_per_channel=2,
83
+ )
84
+
85
+ debug_max = lambda t: ttnn.to_torch(
86
+ t, mesh_composer=ttnn.ConcatMeshToTensor(model_args.mesh_device, dim=-1)
87
+ ).max()
88
+ logger.info(f"decode_input max: {debug_max(decode_input)=}, {decode_input.memory_config()=}")
89
+ logger.info(f"tt_out max: {debug_max(tt_out)=}, {tt_out.memory_config()=}")
code/models/tt_transformers/tests/test_llama90b_decoder_json.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Llama 3.2 90B decoder JSON must match ModelOptimizations baseline except LI_QKV_DECODE (issue #36378)."""
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from models.tt_transformers.tt.model_config import (
9
+ DecodersPrecision,
10
+ MathFidelitySetting,
11
+ ModelOptimizations,
12
+ OpGroup,
13
+ TensorGroup,
14
+ parse_decoder_json,
15
+ )
16
+
17
+ _MN = "Llama-3.2-90B-Instruct"
18
+ _TT_ROOT = Path(__file__).resolve().parents[1]
19
+ _ACC = _TT_ROOT / "model_params" / _MN / "accuracy_decoder_config.json"
20
+ _PERF = _TT_ROOT / "model_params" / _MN / "performance_decoder_config.json"
21
+
22
+
23
+ def _tensor_map(m: ModelOptimizations):
24
+ return {
25
+ k.name: m.tensor_dtype_settings[k].name
26
+ for k in TensorGroup
27
+ if k in m.tensor_dtype_settings and m.tensor_dtype_settings[k] is not None
28
+ }
29
+
30
+
31
+ def _op_map(m: ModelOptimizations):
32
+ return {k.name: m.op_fidelity_settings[k].name for k in OpGroup if k in m.op_fidelity_settings}
33
+
34
+
35
+ def _assert_json_matches_baseline_except_qkv_decode(json_path: Path, opt_fn):
36
+ baseline = opt_fn(_MN)
37
+ b_tensor = _tensor_map(baseline)
38
+ b_op = _op_map(baseline)
39
+ loaded = parse_decoder_json(json_path, default_optimization=opt_fn)
40
+ n = len(loaded.decoder_optimizations)
41
+ assert n == 80
42
+ for di in range(n):
43
+ j = loaded.decoder_optimizations[di]
44
+ j_tensor = _tensor_map(j)
45
+ j_op = _op_map(j)
46
+ assert j_tensor == b_tensor, f"decoder {di} tensor_dtype mismatch"
47
+ for ok, vb in b_op.items():
48
+ jv = j_op.get(ok)
49
+ if ok == OpGroup.LI_QKV_DECODE.name:
50
+ assert (
51
+ vb == MathFidelitySetting.HIFI2.name and jv == MathFidelitySetting.HIFI2_NOL1ACC.name
52
+ ), f"decoder {di} LI_QKV_DECODE expected HIFI2->HIFI2_NOL1ACC, got {vb}->{jv}"
53
+ else:
54
+ assert jv == vb, f"decoder {di} op {ok} baseline={vb} json={jv}"
55
+
56
+
57
+ @pytest.mark.skipif(not _ACC.is_file(), reason="accuracy_decoder_config.json not present")
58
+ def test_llama90b_accuracy_decoder_json_matches_baseline_except_qkv_decode():
59
+ _assert_json_matches_baseline_except_qkv_decode(_ACC, ModelOptimizations.accuracy)
60
+
61
+
62
+ @pytest.mark.skipif(not _PERF.is_file(), reason="performance_decoder_config.json not present")
63
+ def test_llama90b_performance_decoder_json_matches_baseline_except_qkv_decode():
64
+ _assert_json_matches_baseline_except_qkv_decode(_PERF, ModelOptimizations.performance)
65
+
66
+
67
+ def test_llama90b_decoders_precision_without_json_layers_match_accuracy_baseline():
68
+ """When no JSON is used, DecodersPrecision repeats optimization_level(model_name) per layer."""
69
+ base = ModelOptimizations.accuracy(_MN)
70
+ dp = DecodersPrecision(80, _MN, base)
71
+ for i in range(80):
72
+ assert dp.decoder_optimizations[i]._full_name == base._full_name
code/models/tt_transformers/tests/test_lm_head.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2023 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import os
6
+
7
+ import pytest
8
+ import torch
9
+ from loguru import logger
10
+
11
+ import ttnn
12
+ from models.common.utility_functions import comp_allclose, comp_pcc
13
+ from models.tt_transformers.tt.ccl import TT_CCL
14
+ from models.tt_transformers.tt.common import Mode
15
+ from models.tt_transformers.tt.lm_head import LMHead
16
+ from models.tt_transformers.tt.model_config import ModelArgs
17
+ from models.tt_transformers.tt.prefetcher import Prefetcher
18
+
19
+
20
+ @torch.no_grad()
21
+ @pytest.mark.parametrize(
22
+ "use_prefetcher",
23
+ ([False]),
24
+ )
25
+ @pytest.mark.parametrize(
26
+ "seq_len",
27
+ (32,),
28
+ )
29
+ @pytest.mark.parametrize(
30
+ "batch_size",
31
+ (1,),
32
+ )
33
+ @pytest.mark.parametrize(
34
+ "mesh_device",
35
+ [
36
+ {"N150": (1, 1), "N300": (1, 2), "T3K": (1, 8), "TG": (8, 4)}.get(
37
+ os.environ.get("MESH_DEVICE"), len(ttnn.get_device_ids())
38
+ )
39
+ ],
40
+ indirect=True,
41
+ )
42
+ @pytest.mark.parametrize("device_params", [{"fabric_config": True}], indirect=True)
43
+ def test_lm_head_inference(seq_len, batch_size, mesh_device, use_prefetcher, reset_seeds):
44
+ dtype = ttnn.bfloat8_b
45
+
46
+ prefetcher = Prefetcher(mesh_device, num_tensors=0, num_layers=1) if use_prefetcher else None
47
+
48
+ if use_prefetcher:
49
+ prefetcher.init(mode=Mode.DECODE)
50
+
51
+ model_args = ModelArgs(
52
+ mesh_device, max_batch_size=batch_size, max_seq_len=seq_len, cache_hf=True, prefetcher=prefetcher
53
+ )
54
+ model_args.n_layers = 1
55
+
56
+ state_dict = model_args.load_state_dict()
57
+
58
+ state_dict_prefix = model_args.get_state_dict_prefix("", None)
59
+ # Ref model needs partial state dict, but our models use full state dict keys as cached weight names
60
+ partial_state_dict = {
61
+ "weight": state_dict[f"{state_dict_prefix}output.weight"],
62
+ }
63
+
64
+ model_args.WEIGHTS_DTYPE = dtype
65
+ reference_model = model_args.reference_lm_head()
66
+ reference_model.load_state_dict(partial_state_dict)
67
+
68
+ tt_ccl = TT_CCL(mesh_device)
69
+ tt_model = LMHead(
70
+ args=model_args,
71
+ mesh_device=mesh_device,
72
+ tt_ccl=tt_ccl,
73
+ dtype=dtype,
74
+ state_dict=state_dict,
75
+ state_dict_prefix=state_dict_prefix,
76
+ weight_cache_path=model_args.weight_cache_path(dtype),
77
+ max_columns_per_device=model_args.max_columns_per_device_lm_head,
78
+ prefetcher=prefetcher,
79
+ )
80
+
81
+ torch_input = torch.randn(1, 1, seq_len, model_args.dim, dtype=torch.bfloat16)
82
+ reference_output = reference_model(torch_input)
83
+ tt_input = ttnn.from_torch(
84
+ torch_input,
85
+ device=mesh_device,
86
+ mesh_mapper=ttnn.ShardTensor2dMesh(mesh_device, dims=(None, None), mesh_shape=model_args.cluster_shape),
87
+ dtype=ttnn.bfloat8_b,
88
+ memory_config=model_args.get_lm_head_input_mem_config(Mode.PREFILL, prefetcher),
89
+ layout=ttnn.TILE_LAYOUT,
90
+ )
91
+ tt_output = tt_model(tt_input)
92
+ tt_output_torch = ttnn.to_torch(
93
+ tt_output,
94
+ mesh_composer=ttnn.ConcatMesh2dToTensor(
95
+ mesh_device, model_args.cluster_shape, dims=(3, 1) if model_args.is_galaxy else (1, 3)
96
+ ),
97
+ )
98
+ tt_output_torch = tt_output_torch[:, 0:1, :, : model_args.vocab_size]
99
+
100
+ pcc_required = 0.99
101
+ passing, pcc_message = comp_pcc(reference_output, tt_output_torch, pcc_required)
102
+
103
+ logger.info(comp_allclose(reference_output, tt_output_torch))
104
+ logger.info(f"PCC: {pcc_message}")
105
+ if passing:
106
+ logger.info("LM_Head Passed!")
107
+ else:
108
+ logger.warning("LM_Head Failed!")
109
+
110
+ assert passing, f"LM_Head output does not meet PCC requirement {pcc_required}: {pcc_message}."
code/models/tt_transformers/tests/test_lm_head_cache.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from pathlib import Path
6
+
7
+ from models.tt_transformers.tt.lm_head import _lm_head_cache_file_name
8
+
9
+
10
+ def test_galaxy_2d_lm_head_uses_distinct_cache_key():
11
+ cache_root = Path("weights")
12
+ common = {
13
+ "dummy_weights": False,
14
+ "num_splits": 1,
15
+ "shard": 0,
16
+ "width": 131072,
17
+ "mode": 0,
18
+ "mesh_shape": (8, 4),
19
+ }
20
+
21
+ flat = _lm_head_cache_file_name(cache_root, galaxy_2d=False, **common)
22
+ galaxy_2d = _lm_head_cache_file_name(cache_root, galaxy_2d=True, **common)
23
+
24
+ assert flat.name == "output_lm_head_1_split_shard_0_131072_mode_0"
25
+ assert galaxy_2d.name == "output_lm_head_1_split_shard_0_131072_mode_0_galaxy_2d_8x4_v1"
26
+ assert flat != galaxy_2d
27
+
28
+
29
+ def test_dummy_lm_head_does_not_use_weight_cache():
30
+ assert (
31
+ _lm_head_cache_file_name(
32
+ Path("weights"),
33
+ dummy_weights=True,
34
+ num_splits=1,
35
+ shard=0,
36
+ width=131072,
37
+ mode=0,
38
+ galaxy_2d=True,
39
+ mesh_shape=(8, 4),
40
+ )
41
+ is None
42
+ )
code/models/tt_transformers/tests/test_mha_wo_sharding.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: (c) 2025 Tenstorrent AI ULC
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Verify that the wo output projection weight is correctly sharded for MHA
6
+ models (n_heads == n_kv_heads) on multi-device meshes.
7
+
8
+ Regression test for: ShardTensor2dMesh producing incorrect weight shapes
9
+ when n_heads == n_kv_heads on N300 (1x2 mesh).
10
+ """
11
+
12
+ import os
13
+
14
+ import pytest
15
+ import torch
16
+
17
+ import ttnn
18
+
19
+
20
+ @torch.no_grad()
21
+ @pytest.mark.parametrize(
22
+ "mesh_device",
23
+ [
24
+ {"N150": (1, 1), "N300": (1, 2), "T3K": (1, 8), "TG": (8, 4)}.get(
25
+ os.environ.get("MESH_DEVICE"), len(ttnn.get_device_ids())
26
+ )
27
+ ],
28
+ indirect=True,
29
+ )
30
+ @pytest.mark.parametrize("device_params", [{"fabric_config": True}], indirect=True)
31
+ def test_mha_wo_sharding(mesh_device):
32
+ """On a multi-device mesh, wo sharded via ShardTensorToMesh(dim=2) must
33
+ produce per-device shape [1, 1, n_heads*head_dim // num_devices, dim]
34
+ and the subsequent ttnn.linear must not crash."""
35
+
36
+ num_devices = mesh_device.get_num_devices()
37
+ if num_devices < 2:
38
+ pytest.skip("MHA sharding test requires >= 2 devices")
39
+
40
+ n_heads = 16
41
+ head_dim = 128
42
+ dim = 2048
43
+ seq_len = 128
44
+ n_local_heads = n_heads // num_devices
45
+ qkv_inner = n_local_heads * head_dim
46
+
47
+ pt_wo = torch.randn(1, 1, n_heads * head_dim, dim)
48
+ wo = ttnn.as_tensor(
49
+ pt_wo,
50
+ dtype=ttnn.bfloat16,
51
+ layout=ttnn.TILE_LAYOUT,
52
+ device=mesh_device,
53
+ memory_config=ttnn.DRAM_MEMORY_CONFIG,
54
+ mesh_mapper=ttnn.ShardTensorToMesh(mesh_device, dim=2),
55
+ )
56
+
57
+ expected_sharded_dim = (n_heads * head_dim) // num_devices
58
+ assert wo.shape[-2] == expected_sharded_dim, f"wo dim 2 should be {expected_sharded_dim}, got {wo.shape[-2]}"
59
+
60
+ attn_output = ttnn.as_tensor(
61
+ torch.randn(1, 1, seq_len, qkv_inner),
62
+ dtype=ttnn.bfloat16,
63
+ layout=ttnn.TILE_LAYOUT,
64
+ device=mesh_device,
65
+ memory_config=ttnn.DRAM_MEMORY_CONFIG,
66
+ mesh_mapper=ttnn.ReplicateTensorToMesh(mesh_device),
67
+ )
68
+
69
+ output = ttnn.linear(
70
+ attn_output,
71
+ wo,
72
+ dtype=ttnn.bfloat16,
73
+ memory_config=ttnn.DRAM_MEMORY_CONFIG,
74
+ )
75
+
76
+ assert output.shape[-1] == dim, f"output width should be {dim}, got {output.shape[-1]}"
77
+ assert output.shape[-2] == seq_len, f"output seq_len should be {seq_len}, got {output.shape[-2]}"
78
+
79
+ ttnn.deallocate(wo)
80
+ ttnn.deallocate(attn_output)
81
+ ttnn.deallocate(output)
code/models/tt_transformers/tests/test_mlp.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2023 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import os
6
+
7
+ import pytest
8
+ import torch
9
+ from loguru import logger
10
+
11
+ import ttnn
12
+ from models.common.utility_functions import comp_allclose, comp_pcc
13
+ from models.tt_transformers.tests.test_utils import get_ref_model_dype
14
+ from models.tt_transformers.tt.ccl import TT_CCL
15
+ from models.tt_transformers.tt.common import Mode
16
+ from models.tt_transformers.tt.mlp import MLP
17
+ from models.tt_transformers.tt.model_config import ModelArgs
18
+ from models.tt_transformers.tt.prefetcher import Prefetcher
19
+
20
+
21
+ @torch.no_grad()
22
+ @pytest.mark.parametrize(
23
+ "use_prefetcher",
24
+ ([False]),
25
+ )
26
+ @pytest.mark.parametrize(
27
+ "mesh_device",
28
+ [
29
+ {"N150": (1, 1), "N300": (1, 2), "T3K": (1, 8), "TG": (8, 4)}.get(
30
+ os.environ.get("MESH_DEVICE"), len(ttnn.get_device_ids())
31
+ )
32
+ ],
33
+ indirect=True,
34
+ )
35
+ @pytest.mark.parametrize(
36
+ "seq_len",
37
+ (64 * 1024, 32 * 1024, 512, 32),
38
+ )
39
+ @pytest.mark.parametrize(
40
+ "batch_size",
41
+ (1,),
42
+ )
43
+ @pytest.mark.parametrize("device_params", [{"fabric_config": True}], indirect=True)
44
+ def test_mlp_inference(seq_len, batch_size, mesh_device, reset_seeds, ensure_gc, use_prefetcher):
45
+ dtype = ttnn.bfloat8_b
46
+ mode = Mode.DECODE if seq_len <= 32 else Mode.PREFILL
47
+
48
+ # Setup prefetcher (FF1, FF2, FF3 weights are prefetched)
49
+ num_tensors = 3 if mode == Mode.DECODE else 0
50
+ prefetcher = Prefetcher(mesh_device, num_tensors=num_tensors, num_layers=1) if use_prefetcher else None
51
+
52
+ if use_prefetcher:
53
+ prefetcher.init(mode)
54
+
55
+ model_args = ModelArgs(
56
+ mesh_device,
57
+ max_batch_size=batch_size,
58
+ max_seq_len=128,
59
+ cache_hf=True,
60
+ prefetcher=prefetcher,
61
+ )
62
+ model_args.n_layers = 1
63
+ state_dict = model_args.load_state_dict()
64
+
65
+ # Ref model needs partial state dict, but our models use full state dict keys as cached weight names
66
+ first_layer_prefix = model_args.get_state_dict_prefix("MLP", 0)
67
+ partial_state_dict = {
68
+ k[len(first_layer_prefix) + 1 :]: v for k, v in state_dict.items() if (k.startswith(first_layer_prefix))
69
+ }
70
+
71
+ reference_model = model_args.reference_mlp()
72
+ reference_model.load_state_dict(partial_state_dict)
73
+ if model_args.is_90b:
74
+ # float32 ~3x faster than bfloat16.
75
+ # bfloat16 fails on CI (32k and 64k seq_len) with "This test seems to have hung... Timing out test case"
76
+ reference_model.to(torch.float32)
77
+
78
+ tt_ccl = TT_CCL(mesh_device)
79
+ tt_model = MLP(
80
+ mesh_device=mesh_device,
81
+ tt_ccl=tt_ccl,
82
+ args=model_args,
83
+ state_dict=state_dict,
84
+ weight_cache_path=model_args.weight_cache_path(dtype),
85
+ layer_num=0,
86
+ dtype=dtype,
87
+ model_config=model_args.get_model_config(),
88
+ prefetcher=prefetcher,
89
+ )
90
+
91
+ # Run prefetcher if it is used
92
+ if prefetcher is not None and mode == Mode.DECODE:
93
+ prefetcher.prefetch()
94
+ prefetcher.run()
95
+
96
+ torch_input = torch.randn(
97
+ 1, 1, seq_len, model_args.dim, dtype=get_ref_model_dype(reference_model, model_args.model_name)
98
+ )
99
+ reference_output = reference_model(torch_input)
100
+
101
+ tt_input = ttnn.from_torch(
102
+ torch_input,
103
+ device=mesh_device,
104
+ mesh_mapper=ttnn.ShardTensor2dMesh(
105
+ mesh_device,
106
+ dims=(None, 3) if model_args.is_galaxy else (None, None),
107
+ mesh_shape=model_args.cluster_shape,
108
+ ), # When both dims are None, the mapper used is `ReplicateTensorToMesh`
109
+ dtype=ttnn.bfloat8_b,
110
+ memory_config=model_args.get_mlp_input_mem_config(mode, prefetcher),
111
+ layout=ttnn.TILE_LAYOUT,
112
+ )
113
+ logger.info("Run MLP")
114
+ tt_output = tt_model(tt_input, mode)
115
+
116
+ tt_output_torch = ttnn.to_torch(
117
+ tt_output,
118
+ mesh_composer=ttnn.ConcatMesh2dToTensor(mesh_device, dims=(1, 3), mesh_shape=model_args.cluster_shape),
119
+ )
120
+
121
+ tt_output_torch = tt_output_torch[:, :1, :, :]
122
+
123
+ pcc_required = 0.99
124
+ passing, pcc_message = comp_pcc(reference_output, tt_output_torch, pcc_required)
125
+
126
+ logger.info(comp_allclose(reference_output, tt_output_torch))
127
+ logger.info(f"PCC: {pcc_message}")
128
+ if passing:
129
+ logger.info("MLP Passed!")
130
+ else:
131
+ logger.warning("MLP Failed!")
132
+
133
+ assert passing, f"MLP output does not meet PCC requirement {pcc_required}: {pcc_message}."
code/models/tt_transformers/tests/test_model_config_utils.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import pytest
6
+
7
+ from models.tt_transformers.tt.model_config import compute_padded_vocab_size, should_pad_sampling_logits_to_power_of_2
8
+
9
+
10
+ @pytest.mark.parametrize(
11
+ ("vocab_size", "num_devices", "expected"),
12
+ [
13
+ (151936, 1, 151936),
14
+ (151936, 4, 151936),
15
+ (151936, 8, 152064),
16
+ (151936, 32, 152576),
17
+ (32001, 2, 32064),
18
+ ],
19
+ )
20
+ def test_compute_padded_vocab_size(vocab_size, num_devices, expected):
21
+ padded_vocab_size = compute_padded_vocab_size(vocab_size, num_devices)
22
+
23
+ assert padded_vocab_size == expected
24
+ assert padded_vocab_size >= vocab_size
25
+ assert padded_vocab_size % (32 * num_devices) == 0
26
+ assert (padded_vocab_size // num_devices) % 32 == 0
27
+
28
+
29
+ def test_compute_padded_vocab_size_rejects_invalid_num_devices():
30
+ with pytest.raises(ValueError, match="num_devices must be >= 1"):
31
+ compute_padded_vocab_size(32000, 0)
32
+
33
+
34
+ @pytest.mark.parametrize(
35
+ ("base_model_name", "padded_vocab_size", "sampling_splits", "expected"),
36
+ [
37
+ ("Llama-3.1-70B", 128256, 4, True),
38
+ ("Llama-3.1-70B", 131072, 4, False),
39
+ ("Llama-3.1-8B", 128256, 4, False),
40
+ ],
41
+ )
42
+ def test_should_pad_sampling_logits_to_power_of_2(base_model_name, padded_vocab_size, sampling_splits, expected):
43
+ assert should_pad_sampling_logits_to_power_of_2(base_model_name, padded_vocab_size, sampling_splits) is expected
44
+
45
+
46
+ def test_should_pad_sampling_logits_to_power_of_2_rejects_invalid_sampling_splits():
47
+ with pytest.raises(ValueError, match="sampling_splits must be >= 1"):
48
+ should_pad_sampling_logits_to_power_of_2("Llama-3.1-70B", 128256, 0)
code/models/tt_transformers/tests/test_model_prefill.py ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2024 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ import bz2
5
+ import os
6
+
7
+ import pytest
8
+ import torch
9
+ from loguru import logger
10
+
11
+ import ttnn
12
+ from models.common.utility_functions import comp_pcc
13
+ from models.tt_transformers.tt.common import PagedAttentionConfig, create_tt_model
14
+ from models.tt_transformers.tt.generator import Generator
15
+ from models.tt_transformers.tt.model_config import DecodersPrecision
16
+
17
+
18
+ @torch.no_grad()
19
+ @pytest.mark.timeout(900)
20
+ @pytest.mark.models_performance_bare_metal
21
+ @pytest.mark.parametrize("use_prefetcher", ([False]))
22
+ @pytest.mark.parametrize(
23
+ "mesh_device",
24
+ [
25
+ {"N150": (1, 1), "N300": (1, 2), "T3K": (1, 8), "TG": (8, 4)}.get(
26
+ os.environ.get("MESH_DEVICE"), len(ttnn.get_device_ids())
27
+ )
28
+ ],
29
+ indirect=True,
30
+ )
31
+ # Model and attention prefill tests should run both with and without paged attention to debug any issues that may occur with default attention
32
+ @pytest.mark.parametrize(
33
+ "paged_attention",
34
+ (
35
+ True,
36
+ # False,
37
+ ),
38
+ ids=(
39
+ "paged_attention",
40
+ # "default_attention",
41
+ ),
42
+ )
43
+ @pytest.mark.parametrize(
44
+ "page_params",
45
+ [{"page_block_size": 32, "page_max_num_blocks": 1024}],
46
+ )
47
+ @pytest.mark.parametrize(
48
+ "seq_len",
49
+ (128, 256, 3072, 4096, 8192, 16384, 32768),
50
+ ids=["128", "256", "3k", "4k", "8k", "16k", "32k"],
51
+ )
52
+ @pytest.mark.parametrize(
53
+ "max_seq_len",
54
+ (128 * 1024,),
55
+ ids=[
56
+ "max128k",
57
+ ],
58
+ )
59
+ @pytest.mark.parametrize(
60
+ "optimizations",
61
+ [
62
+ lambda model_args: DecodersPrecision.performance(model_args.n_layers, model_args.model_name),
63
+ lambda model_args: DecodersPrecision.accuracy(model_args.n_layers, model_args.model_name),
64
+ ],
65
+ ids=["performance", "accuracy"],
66
+ )
67
+ @pytest.mark.parametrize(
68
+ "num_layers",
69
+ (1, None),
70
+ ids=["1layer", "all_layers"],
71
+ )
72
+ @pytest.mark.parametrize("device_params", [{"fabric_config": True}], indirect=True)
73
+ def test_model_inference(
74
+ paged_attention,
75
+ page_params,
76
+ optimizations,
77
+ seq_len,
78
+ max_seq_len,
79
+ num_layers,
80
+ mesh_device,
81
+ reset_seeds,
82
+ ensure_gc,
83
+ is_ci_env,
84
+ request,
85
+ use_prefetcher,
86
+ ):
87
+ test_id = request.node.callspec.id
88
+ use_hf_rope = request.config.getoption("--use_hf_rope")
89
+ if is_ci_env:
90
+ if "accuracy" in test_id:
91
+ pytest.skip("CI test only runs performance mode to reduce CI pipeline load")
92
+
93
+ # TODO: Save ref outputs to avoid running reference model for large seq_len
94
+ if seq_len > 8192:
95
+ pytest.skip("CI test only runs up to 8192 seq_len to avoid out of ram issues for ref model")
96
+ if use_hf_rope:
97
+ if num_layers != 1 and seq_len != 256:
98
+ pytest.skip("When HF rope is used CI only runs full model for 256 seq len to reduce CI pipeline load")
99
+
100
+ elif num_layers != 1 and seq_len != 4096:
101
+ pytest.skip("CI only runs full model for 4k seq len to reduce CI pipeline load")
102
+
103
+ hf_model_env = os.getenv("HF_MODEL", "")
104
+ if ("Llama" in hf_model_env) and ("Vision" in hf_model_env) and (num_layers is None):
105
+ pytest.skip("Skipping Llama Vision full model test: no CrossAttention functionality in this test.")
106
+
107
+ run_ref_pt = True # Flag to run reference PyTorch model and compare PCC
108
+ dtype = ttnn.bfloat8_b
109
+ batch_size = 1 # For prefill we only support batch_size = 1
110
+
111
+ # Use instruct weights instead of general weights
112
+ instruct = True
113
+
114
+ paged_attention_config = (
115
+ PagedAttentionConfig(
116
+ block_size=page_params["page_block_size"],
117
+ max_num_blocks=page_params["page_max_num_blocks"],
118
+ )
119
+ if paged_attention
120
+ else None
121
+ )
122
+
123
+ # Load TTNN model
124
+ logger.info(f"Loading TT model...")
125
+ model_args, tt_model, tt_kv_cache, state_dict = create_tt_model(
126
+ mesh_device,
127
+ instruct=instruct,
128
+ max_batch_size=batch_size,
129
+ optimizations=optimizations,
130
+ max_seq_len=max_seq_len,
131
+ paged_attention_config=paged_attention_config,
132
+ dtype=dtype,
133
+ num_layers=num_layers,
134
+ use_prefetcher=use_prefetcher,
135
+ use_hf_rope=use_hf_rope,
136
+ )
137
+
138
+ if (
139
+ model_args.base_model_name.startswith("Mistral-")
140
+ or model_args.base_model_name.startswith("Qwen3-")
141
+ or model_args.base_model_name.startswith("Phi-3-mini-")
142
+ or model_args.base_model_name.startswith("phi-4")
143
+ ):
144
+ # TODO: Per layer KV cache fetching is not implemented for all models
145
+ # See issue https://github.com/tenstorrent/tt-metal/issues/19806"
146
+ cache_pcc = False
147
+ else:
148
+ cache_pcc = True
149
+
150
+ # This sets the minimum PCC for each iteration based on optimization mode
151
+ # TODO: See issue https://github.com/tenstorrent/tt-metal/issues/19806
152
+ perf_out_pcc_map = {"Mistral-7B-Instruct-v0.3": 0.73}
153
+ acc_out_pcc_map = {
154
+ "Mistral-7B-Instruct-v0.3": 0.75,
155
+ "Phi-3-mini-128k-instruct": 0.89,
156
+ }
157
+ kv_cache_pcc_map = {"Mistral-7B-Instruct-v0.3": 0.75}
158
+
159
+ if num_layers == 1:
160
+ expec_out_pcc = 0.97
161
+ expec_kv_cache_pcc = 0.99
162
+ else:
163
+ if "accuracy" in test_id:
164
+ default_expec_out_pcc = 0.91 # TODO Look on improving PCC
165
+ expec_out_pcc = acc_out_pcc_map.get(model_args.model_name, default_expec_out_pcc)
166
+ else: # performance mode
167
+ assert "performance" in test_id
168
+ default_expec_out_pcc = 0.869 # TODO Look on improving PCC
169
+ expec_out_pcc = perf_out_pcc_map.get(model_args.model_name, default_expec_out_pcc)
170
+
171
+ default_expec_kv_cache_pcc = 0.88
172
+ expec_kv_cache_pcc = kv_cache_pcc_map.get(model_args.model_name, default_expec_kv_cache_pcc)
173
+
174
+ processor = model_args.processor
175
+ tokenizer = model_args.tokenizer
176
+ generator = Generator([tt_model], [model_args], mesh_device, processor=processor, tokenizer=tokenizer)
177
+ logger.info("Finished loading TT model.")
178
+
179
+ # Create page table if paged attention is enabled
180
+ if paged_attention:
181
+ # Implied shuffling of blocks
182
+ permutation = torch.randperm(paged_attention_config.max_num_blocks)
183
+ # Page table which maps virtual blocks to physical
184
+ reverse_permutation = torch.argsort(permutation)
185
+ page_table = reverse_permutation.reshape(
186
+ model_args.max_batch_size, paged_attention_config.max_num_blocks // model_args.max_batch_size
187
+ )
188
+ else:
189
+ page_table = None
190
+
191
+ # Load prompt
192
+ current_file_path = os.path.abspath(__file__)
193
+ current_file_dir = os.path.dirname(current_file_path)
194
+ prompt_file = os.path.join(current_file_dir, "tale-of-two-cities.txt.bz2")
195
+ with bz2.open(prompt_file, "rt", encoding="utf-8") as f:
196
+ prompt = f.read()
197
+ encoded_prompt = model_args.encode_prompt(prompt, instruct=instruct)[:seq_len]
198
+ logger.info(f"Prompt length: {len(encoded_prompt)} tokens")
199
+
200
+ # Load reference model
201
+ if run_ref_pt:
202
+ logger.info("Loading reference model...")
203
+ # On a warm ttnn cache, create_tt_model defers the HF load and returns a dataless
204
+ # placeholder state_dict. The host reference embedding below still needs the REAL weights,
205
+ # so reload them here when the state_dict is a warm-cache placeholder (either flavor) or
206
+ # genuinely empty. Branch on is_placeholder, NOT truthiness: tt_transformers'
207
+ # _PlaceholderStateDict is falsy but the shared CachedStateDict is truthy, so `if not
208
+ # state_dict` alone would silently stop reloading -- and build this torch reference from
209
+ # torch.empty garbage -- if the loaders are ever collapsed onto the shared class. (#45400)
210
+ if getattr(state_dict, "is_placeholder", False) or not state_dict:
211
+ state_dict = model_args.load_state_dict()
212
+ state_dict_prefix = model_args.get_state_dict_prefix("", None)
213
+ reference_model = model_args.reference_transformer(load_checkpoint=True)
214
+ # Embedding on host
215
+ embd = model_args.reference_embedding()
216
+ if model_args.is_llama_vision():
217
+ weight = torch.cat(
218
+ [
219
+ state_dict[f"{state_dict_prefix}tok_embeddings.weight"],
220
+ state_dict[f"{state_dict_prefix}learnable_embedding.weight"],
221
+ ],
222
+ dim=0,
223
+ )
224
+ else:
225
+ weight = state_dict[f"{state_dict_prefix}tok_embeddings.weight"]
226
+ embd.load_state_dict({"emb.weight": weight})
227
+ logger.info("Finished loading reference model.")
228
+
229
+ # Select the first token from the prompt for initial decoding
230
+ encoded_prompt_tensor = torch.tensor(encoded_prompt) # [:,0]
231
+ tt_prefill_input = encoded_prompt_tensor.unsqueeze(0)
232
+ prompt_lens = [seq_len]
233
+ start_pos = 0
234
+
235
+ # Run TT model
236
+ logger.info(f"Running TT model...")
237
+ tt_output_torch = generator.prefill_forward_text(
238
+ tt_prefill_input,
239
+ page_table=page_table,
240
+ kv_cache=[tt_kv_cache],
241
+ prompt_lens=prompt_lens,
242
+ )
243
+ logger.info(f"Finished running TT model.")
244
+
245
+ if run_ref_pt:
246
+ # Run reference model
247
+ logger.info(f"Running reference model...")
248
+ pt_prefill_input = embd(encoded_prompt_tensor).view(batch_size, seq_len, -1)
249
+ ref_output = reference_model(pt_prefill_input, start_pos)
250
+ ref_output = ref_output[:, -1:, :] # Get last token since TT model only returns the last token
251
+ logger.info(f"Finished running reference model.")
252
+
253
+ # Measure PCC if also running reference model
254
+ all_tests_pass = True
255
+
256
+ # Check output pcc
257
+ passing, pcc_message = comp_pcc(ref_output, tt_output_torch, expec_out_pcc)
258
+ logger.info(f"Output PCC: {pcc_message}")
259
+ if not passing:
260
+ all_tests_pass = False
261
+ logger.warning(f"Output PCC {pcc_message} is lower than {expec_out_pcc}")
262
+
263
+ # Compare KV caches
264
+ if cache_pcc:
265
+ for i in range(model_args.n_layers):
266
+ pytorch_layer_present = [
267
+ reference_model.cache_k[i].clone().permute(0, 2, 1, 3), # [batch_size, n_kv_heads, seq, head_dim]
268
+ reference_model.cache_v[i].clone().permute(0, 2, 1, 3), # [batch_size, n_kv_heads, seq, head_dim]
269
+ ]
270
+
271
+ tt_layer_present = []
272
+ if paged_attention:
273
+ for layer_past in tt_model.layers[i].attention.layer_past:
274
+ tt_layer_present.append(
275
+ ttnn.to_torch(
276
+ layer_past,
277
+ mesh_composer=ttnn.ConcatMesh2dToTensor(
278
+ mesh_device,
279
+ dims=(1, 3) if model_args.is_galaxy else (0, 1),
280
+ mesh_shape=model_args.cluster_shape,
281
+ ),
282
+ )[reverse_permutation][:, : model_args.n_kv_heads, :, : model_args.head_dim]
283
+ .reshape(
284
+ model_args.max_batch_size,
285
+ paged_attention_config.max_num_blocks // model_args.max_batch_size,
286
+ model_args.n_kv_heads,
287
+ paged_attention_config.block_size,
288
+ model_args.head_dim,
289
+ )
290
+ .transpose(1, 2)
291
+ .reshape(model_args.max_batch_size, model_args.n_kv_heads, -1, model_args.head_dim)[
292
+ :batch_size, ...
293
+ ]
294
+ )
295
+ else:
296
+ for layer_past in tt_model.layers[i].attention.layer_past_list[0]:
297
+ tt_layer_present.append(
298
+ ttnn.to_torch(
299
+ layer_past,
300
+ mesh_composer=ttnn.ConcatMesh2dToTensor(
301
+ mesh_device,
302
+ dims=(1, 0) if model_args.is_galaxy else (0, 1),
303
+ mesh_shape=model_args.cluster_shape,
304
+ ),
305
+ )
306
+ )
307
+
308
+ for j, (cache_pt, cache_tt) in enumerate(zip(pytorch_layer_present, tt_layer_present)):
309
+ cache_length_to_check = seq_len
310
+ cache_pt = cache_pt[:, :, 0:cache_length_to_check, :]
311
+ cache_tt = cache_tt[:, :, 0:cache_length_to_check, :]
312
+ pcc_passed, output_pcc = comp_pcc(cache_pt, cache_tt, expec_kv_cache_pcc)
313
+ kv_str = "K" if j == 0 else "V"
314
+ logger.info(f"[layer={i+1}] {kv_str} cache PCC: {output_pcc}")
315
+ if not pcc_passed:
316
+ all_tests_pass = False
317
+ logger.warning(f"[layer={i+1}] {kv_str} PCC {output_pcc} is lower than {expec_kv_cache_pcc}")
318
+
319
+ if all_tests_pass:
320
+ logger.info("All PCC checks passed!")
321
+ else:
322
+ assert all_tests_pass, f"PCC is lower than expected for some of the outputs. Check warnings!"
code/models/tt_transformers/tests/test_rope_utils.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import pytest
6
+
7
+ from models.tt_transformers.tt.rope import get_batch_size_per_device_group
8
+
9
+
10
+ @pytest.mark.parametrize(
11
+ ("batch_size", "use_qk_fused", "num_devices", "mesh_shape", "expected"),
12
+ [
13
+ (1, True, 32, (8, 4), 2),
14
+ (1, False, 32, (8, 4), 1),
15
+ (32, True, 32, (8, 4), 16),
16
+ (1, True, 1, (), 2),
17
+ ],
18
+ )
19
+ def test_get_batch_size_per_device_group(batch_size, use_qk_fused, num_devices, mesh_shape, expected):
20
+ assert get_batch_size_per_device_group(batch_size, use_qk_fused, num_devices, mesh_shape, 1) == expected
code/models/tt_transformers/tests/test_utils.py ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import math
6
+ from collections import defaultdict
7
+
8
+ import pandas as pd
9
+ import torch
10
+ from loguru import logger
11
+
12
+ from models.tt_transformers.tt.model_config import HfAttentionWrapper, HfDecoderWrapper, HfModelWrapper
13
+
14
+
15
+ def _extract_dtype_from_state_dict(model):
16
+ """Helper to extract dtype from model's state_dict."""
17
+ try:
18
+ state_dict = model.state_dict()
19
+ for key, param in state_dict.items():
20
+ if "weight" in key:
21
+ print(f"get_ref_model_dype: key={key}, dtype={param.dtype}")
22
+ return param.dtype
23
+ except Exception as e:
24
+ pass
25
+ return None
26
+
27
+
28
+ def get_ref_model_dype(ref_model, model_name):
29
+ default_dype = torch.float32
30
+
31
+ if ref_model is None and model_name is None:
32
+ return default_dype
33
+
34
+ try:
35
+ models_to_check = []
36
+ if isinstance(ref_model, HfAttentionWrapper):
37
+ models_to_check.append(ref_model.attention)
38
+ elif isinstance(ref_model, HfDecoderWrapper):
39
+ models_to_check.append(ref_model.decoder)
40
+ elif isinstance(ref_model, HfModelWrapper):
41
+ models_to_check.append(ref_model.model)
42
+ else:
43
+ models_to_check = [ref_model]
44
+
45
+ # Try all models until one works
46
+ for model in models_to_check:
47
+ if model is not None:
48
+ dtype = _extract_dtype_from_state_dict(model)
49
+ if dtype is not None:
50
+ return dtype
51
+
52
+ except Exception as e:
53
+ pass
54
+
55
+ # try hardcoded dtypes
56
+ if model_name and isinstance(model_name, str):
57
+ model_name_lower = model_name.lower()
58
+ if "mistral-7b" in model_name_lower:
59
+ return torch.bfloat16
60
+ if "llama" in model_name_lower:
61
+ return torch.bfloat16
62
+ if "phi-3-mini" in model_name_lower or "phi-4" in model_name_lower:
63
+ return torch.bfloat16
64
+
65
+ return default_dype
66
+
67
+
68
+ ### UTIL FUNCTIONS FOR DEVICE PERF
69
+ def build_duration_dict(raw_dict, column_name):
70
+ """Build a dictionary of op codes to list of durations."""
71
+ op_code_dict = {}
72
+ for entry in raw_dict:
73
+ if column_name not in entry:
74
+ logger.warning(f"Warning: {entry} does not have column {column_name}")
75
+ op_code = entry["OP CODE"]
76
+ duration = entry[column_name]
77
+ if op_code not in op_code_dict:
78
+ op_code_dict[op_code] = []
79
+ op_code_dict[op_code].append(duration)
80
+ return op_code_dict
81
+
82
+
83
+ def build_duration_per_instance_dict(input_dict, num_layers):
84
+ """Build a dictionary of op codes to list of durations per instance."""
85
+ per_instance_dict = {}
86
+ for op_code in input_dict:
87
+ num_ops_with_op_code = len(input_dict[op_code])
88
+ num_instances = num_ops_with_op_code // num_layers
89
+ if num_ops_with_op_code % num_layers != 0:
90
+ logger.warning(
91
+ f"Warning: {op_code} has {num_ops_with_op_code} ops, not a multiple of {num_layers} layers. Skipping per-instance analysis for this op."
92
+ )
93
+ continue # Skip this op_code instead of asserting
94
+ for iteration_id in range(num_layers):
95
+ for instance_id in range(num_instances):
96
+ op_code_with_id = f"{op_code}_{instance_id}"
97
+ if op_code_with_id not in per_instance_dict:
98
+ per_instance_dict[op_code_with_id] = []
99
+ per_instance_dict[op_code_with_id].append(
100
+ input_dict[op_code][iteration_id * num_instances + instance_id]
101
+ )
102
+ return per_instance_dict
103
+
104
+
105
+ def merge_device_rows(df):
106
+ """
107
+ Merges device rows from a DataFrame into a single row per device.
108
+
109
+ Args:
110
+ df: A DataFrame containing measurements.
111
+
112
+ Returns:
113
+ A DataFrame with merged rows.
114
+ """
115
+ block_by_device = defaultdict(list)
116
+
117
+ for _, row in df.iterrows():
118
+ op_name = row["OP CODE"]
119
+ op_type = row["OP TYPE"]
120
+
121
+ if op_type == "tt_dnn_device":
122
+ device_id = int(row["DEVICE ID"])
123
+ block_by_device[device_id].append((op_name, row.to_dict()))
124
+
125
+ device_ids = sorted(block_by_device.keys())
126
+ merged_blocks = []
127
+ global_index = 0
128
+ while max(len(block_by_device[device_id]) for device_id in device_ids) > 0:
129
+ blocks = []
130
+ op_name = None
131
+ missing_devices = []
132
+ for device_id in device_ids:
133
+ if not len(block_by_device[device_id]):
134
+ logger.warning(f"Warning: Device {device_id} is missing operation {op_name} at index {global_index}")
135
+ continue
136
+ if op_name is None:
137
+ op_name = block_by_device[device_id][0][0]
138
+ elif op_name != block_by_device[device_id][0][0]:
139
+ missing_devices.append(device_id)
140
+ continue
141
+
142
+ blocks.append(block_by_device[device_id].pop(0))
143
+
144
+ if missing_devices:
145
+ logger.warning(
146
+ f"Warning: {op_name} at index {global_index} not present in CSV for {len(missing_devices)} devices {missing_devices} - do not trust data for this op or directly subsequent ops with the same name"
147
+ )
148
+
149
+ if not blocks:
150
+ break
151
+
152
+ if "AllGather" in op_name or "ReduceScatter" in op_name or "AllReduce" in op_name or "Matmul_RS" in op_name:
153
+ # For collective ops, take the average duration over all rows within a block
154
+ device_kernel_durations = [
155
+ d["DEVICE KERNEL DURATION [ns]"]
156
+ for _, d in blocks
157
+ if "DEVICE KERNEL DURATION [ns]" in d and not math.isnan(d["DEVICE KERNEL DURATION [ns]"])
158
+ ]
159
+
160
+ average_duration = (
161
+ sum(device_kernel_durations) / len(device_kernel_durations) if device_kernel_durations else float("nan")
162
+ )
163
+ # Use the first block's data but update its duration with the average
164
+ base_block = blocks[0][1].copy()
165
+ base_block["DEVICE KERNEL DURATION [ns]"] = average_duration
166
+ merged_blocks.append(base_block)
167
+ else:
168
+ # For non-collective ops, take the row with maximum duration
169
+ max_duration_block = max(blocks, key=lambda x: x[1]["DEVICE KERNEL DURATION [ns]"])
170
+ merged_blocks.append(max_duration_block[1])
171
+
172
+ global_index += 1
173
+
174
+ return pd.DataFrame(merged_blocks)
175
+
176
+
177
+ def process_measurements(df, num_layers):
178
+ """
179
+ Given a Dataframe containing op device perf measurements, return the average, min, and max durations per instance on kerne
180
+ dispatch, and first to last start.
181
+
182
+ Args:
183
+ df: A DataFrame containing measurements.
184
+ num_layers: The number of layers in the model.
185
+
186
+ Returns:
187
+ A dictionary of aggregated values.
188
+ - kernel_duration_per_instance_aggregate_dict: A dictionary of aggregated kernel durations per instance.
189
+ - dispatch_duration_per_instance_aggregate_dict: A dictionary of aggregated dispatch durations per instance.
190
+ - first_to_last_start_per_instance_aggregate_dict: A dictionary of aggregated first to last start durations per instance.
191
+ """
192
+ raw_dict = df[
193
+ ["OP CODE", "DEVICE KERNEL DURATION [ns]", "OP TO OP LATENCY [ns]", "DEVICE KERNEL FIRST TO LAST START [ns]"]
194
+ ].to_dict(orient="records")
195
+
196
+ # Kernel duration
197
+ kernel_duration_dict = build_duration_dict(raw_dict, "DEVICE KERNEL DURATION [ns]")
198
+ kernel_duration_per_instance_dict = build_duration_per_instance_dict(kernel_duration_dict, num_layers)
199
+ kernel_duration_per_instance_aggregate_dict = {
200
+ "avg": aggregate_per_instance_dict(kernel_duration_per_instance_dict, lambda v: sum(v) / len(v)),
201
+ "min": aggregate_per_instance_dict(kernel_duration_per_instance_dict, min),
202
+ "max": aggregate_per_instance_dict(kernel_duration_per_instance_dict, max),
203
+ }
204
+
205
+ # Dispatch duration
206
+ dispatch_duration_dict = build_duration_dict(raw_dict, "OP TO OP LATENCY [ns]")
207
+ dispatch_duration_per_instance_dict = build_duration_per_instance_dict(dispatch_duration_dict, num_layers)
208
+ dispatch_duration_per_instance_aggregate_dict = {
209
+ "avg": aggregate_per_instance_dict(dispatch_duration_per_instance_dict, lambda v: sum(v) / len(v)),
210
+ "min": aggregate_per_instance_dict(dispatch_duration_per_instance_dict, min),
211
+ "max": aggregate_per_instance_dict(dispatch_duration_per_instance_dict, max),
212
+ }
213
+ # First to last start
214
+ first_to_last_start_dict = build_duration_dict(raw_dict, "DEVICE KERNEL FIRST TO LAST START [ns]")
215
+ first_to_last_start_per_instance_dict = build_duration_per_instance_dict(first_to_last_start_dict, num_layers)
216
+ first_to_last_start_per_instance_aggregate_dict = {
217
+ "avg": aggregate_per_instance_dict(first_to_last_start_per_instance_dict, lambda v: sum(v) / len(v)),
218
+ "min": aggregate_per_instance_dict(first_to_last_start_per_instance_dict, min),
219
+ "max": aggregate_per_instance_dict(first_to_last_start_per_instance_dict, max),
220
+ }
221
+
222
+ return (
223
+ kernel_duration_per_instance_aggregate_dict,
224
+ dispatch_duration_per_instance_aggregate_dict,
225
+ first_to_last_start_per_instance_aggregate_dict,
226
+ )
227
+
228
+
229
+ def print_dict(input_dict, dict_name):
230
+ # print dict as a readable python dict
231
+ logger.info(f"\n{dict_name} = {{")
232
+ for op_code_with_id in input_dict:
233
+ logger.info(f'"{op_code_with_id}": {input_dict[op_code_with_id]},')
234
+ logger.info("}")
235
+
236
+
237
+ def aggregate_per_instance_dict(input_dict, agg_fn, default=0):
238
+ """
239
+ Aggregates a dictionary of values by a given function.
240
+
241
+ Args:
242
+ input_dict: A dictionary of values to aggregate.
243
+ agg_fn: A function to aggregate the values.
244
+ default: The default value to return if the dictionary is empty.
245
+
246
+ Returns:
247
+ A dictionary of aggregated values.
248
+ """
249
+ result = {}
250
+ for key, values in input_dict.items():
251
+ clean_values = [v if v is not None else 0 for v in values]
252
+ result[key] = agg_fn(clean_values) if clean_values else default
253
+ return result
254
+
255
+
256
+ def find_repeated_runs(ops, num_runs):
257
+ """
258
+ Find the starting index of repeated operation runs in a list.
259
+
260
+ This function scans through a list of operations (`ops`) to find the
261
+ first index (`left`) such that the remaining portion of the list,
262
+ `ops[left:]`, can be evenly divided into `num_runs` contiguous segments
263
+ (runs), all of which are identical.
264
+ """
265
+
266
+ def check_ops(left):
267
+ n = len(ops) - left
268
+ if n % num_runs != 0:
269
+ return False # Can't evenly split
270
+
271
+ run_length = n // num_runs
272
+ first = ops[left : left + run_length]
273
+ for i in range(1, num_runs):
274
+ if ops[left + i * run_length : left + (i + 1) * run_length] != first:
275
+ return False
276
+ return True
277
+
278
+ left = 0
279
+ while left < len(ops):
280
+ if check_ops(left):
281
+ return left
282
+ left += 1
283
+ return -1 # return -1 if not found
284
+
285
+
286
+ def find_repeated_block(ops, min_repeat=2):
287
+ """
288
+ Detect a repeating block (pattern) of operations within a list.
289
+
290
+ This function scans through the list of operations `ops` to find a contiguous
291
+ sub-sequence (block) that repeats consecutively at least `min_repeat` times.
292
+ It returns information about the prefix (head) before the repeated region,
293
+ the size and count of the repeated block, and the suffix (tail) after it.
294
+
295
+ The function assumes that each block represents a "layer" or
296
+ repeating structure (e.g., neural network layer operations).
297
+ It tries multiple possible block sizes (starting from 10) to identify
298
+ the first valid repeated pattern.
299
+
300
+ """
301
+ n = len(ops)
302
+ for block_size in range(10, n // min_repeat + 1): # ignore tiny blocks
303
+ for start in range(n - 2 * block_size):
304
+ block = ops[start : start + block_size]
305
+ next_block = ops[start + block_size : start + 2 * block_size]
306
+
307
+ if block == next_block:
308
+ # Found a repeating pattern
309
+ # Extend it as far as it repeats
310
+ i = start
311
+ while i + block_size <= n and ops[i : i + block_size] == block:
312
+ i += block_size
313
+ repeat_count = (i - start) // block_size
314
+
315
+ head = ops[:start]
316
+ tail = ops[i:]
317
+ return {
318
+ "num_head_ops": len(head),
319
+ "num_layer_block_ops": len(block),
320
+ "num_layers": repeat_count,
321
+ "num_tail_ops": len(tail),
322
+ }
323
+ # No repetition found
324
+ return {
325
+ "num_head_ops": len(ops),
326
+ "num_layer_block_ops": 0,
327
+ "num_layers": 0,
328
+ "num_tail_ops": len(ops),
329
+ }
330
+
331
+
332
+ def split_compile_and_trace(
333
+ df: pd.DataFrame,
334
+ mode: str = "prefill",
335
+ num_runs: int = 1,
336
+ num_layers: int = None,
337
+ ):
338
+ """
339
+ Split a concatenated ops DataFrame into compile and runtime-trace segments,
340
+ and further partition those into first layer, mid layers, and model tail DataFrames.
341
+
342
+ The ops CSV typically contains three consecutive phases: compile, capture/trace,
343
+ and runtime trace. When an extra sampling compile pass is present (to enable
344
+ random sampling), it contributes a fixed number of rows that should not be used
345
+ to determine the thirds split.
346
+
347
+ Parameters:
348
+ df: the input DataFrame (all ops)
349
+ mode: the mode of the test (prefill or decode)
350
+ num_runs: number of runs in the CSV (typically 3: compile, capture, trace)
351
+ num_layers: number of core layers to partition (required for further splits)
352
+
353
+ Returns:
354
+ (
355
+ df_model_compilation, df_model_trace,
356
+ df_first_layer_compilation, df_first_layer_trace,
357
+ df_mid_layers_compilation, df_mid_layers_trace,
358
+ df_model_tail_compilation, df_model_tail_trace
359
+ )
360
+ Any of the additional outputs may be None if slicing arguments are not provided.
361
+ """
362
+
363
+ # Finds the first index such that ops[left:] contains num_runs of identical blocks of ops
364
+ first_run_start = find_repeated_runs(df["OP CODE"].tolist(), num_runs)
365
+ adjusted_len = (len(df) - first_run_start) // num_runs # The number of ops in each run
366
+ first_run_end = first_run_start + adjusted_len
367
+ last_run_start = len(df) - adjusted_len
368
+ df_model_compilation = df[first_run_start:first_run_end]
369
+ df_model_trace = df[last_run_start:]
370
+
371
+ # Find the head and tail of the repeating region in the model compilation/ trace region of ops
372
+ head_tail_ops = find_repeated_block(df_model_compilation["OP CODE"].tolist(), num_layers)
373
+
374
+ # [op_start_index:op_end_index] = all core layers region
375
+ op_start_index = head_tail_ops["num_head_ops"]
376
+ op_end_index = len(df_model_compilation) - head_tail_ops["num_tail_ops"]
377
+ df_layers_compilation = df_model_compilation[op_start_index:op_end_index]
378
+ df_layers_trace = df_model_trace[op_start_index:op_end_index]
379
+
380
+ # First layer: always first 'len/num_layers'
381
+ split_point = int(len(df_layers_compilation) / num_layers)
382
+ df_first_layer_compilation = df_layers_compilation[:split_point]
383
+ df_first_layer_trace = df_layers_trace[:split_point]
384
+
385
+ # Mid layers: remainder of layers region
386
+ if num_layers > 1:
387
+ df_mid_layers_compilation = df_layers_compilation[split_point:]
388
+ df_mid_layers_trace = df_layers_trace[split_point:]
389
+ else:
390
+ df_mid_layers_compilation = None
391
+ df_mid_layers_trace = None
392
+
393
+ # Model tail ops (e.g. lmhead/sampling): [tail_start_index:]
394
+ if op_end_index is not None:
395
+ df_model_tail_compilation = df_model_compilation[op_end_index:]
396
+ df_model_tail_trace = df_model_trace[op_end_index:]
397
+ else:
398
+ df_model_tail_compilation = None
399
+ df_model_tail_trace = None
400
+
401
+ return (
402
+ df_model_compilation,
403
+ df_model_trace,
404
+ df_first_layer_compilation,
405
+ df_first_layer_trace,
406
+ df_mid_layers_compilation,
407
+ df_mid_layers_trace,
408
+ df_model_tail_compilation,
409
+ df_model_tail_trace,
410
+ )
411
+
412
+
413
+ def verify_value_within_margin(value, target, margin, op_code_with_id, perf_type):
414
+ upper_limit = target + margin * target
415
+ lower_limit = target - margin * target
416
+
417
+ passing = True
418
+
419
+ if value > upper_limit:
420
+ passing = False
421
+ logger.warning(
422
+ f"{op_code_with_id} {perf_type}: {value} ns is larger than target "
423
+ f"({target}) ns, difference: "
424
+ f"{abs(value - upper_limit)} ns, margin: "
425
+ f"{margin}, "
426
+ f"relative margin to pass would be: "
427
+ f"{(abs(target - value) / target) if target != 0 else -1}"
428
+ )
429
+ elif value < lower_limit:
430
+ passing = False
431
+ logger.warning(
432
+ f"{op_code_with_id} {perf_type}: {value} ns is smaller than target "
433
+ f"({target}) ns, difference: "
434
+ f"{abs(value - lower_limit)} ns, margin: "
435
+ f"{margin}, "
436
+ f"relative margin to pass would be: "
437
+ f"{(abs(target - value) / target) if target != 0 else -1}"
438
+ )
439
+ return passing
code/models/tt_transformers/tests/test_vllm_kv_cache.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Unit tests for the vLLM-side KV cache allocator helpers in
4
+ ``generator_vllm.py``.
5
+
6
+ Verifies the new per-layer entry point (``allocate_vllm_kv_cache_per_layer``)
7
+ and that the legacy uniform-shape entry point (``allocate_vllm_kv_cache``)
8
+ still delegates to it bit-for-bit.
9
+
10
+ Real ttnn allocation requires a mesh device, so this test mocks
11
+ ``ttnn.as_tensor`` / ``ttnn.ReplicateTensorToMesh`` and the ``dp_model``
12
+ handles. We verify call structure and shape routing, not the resulting
13
+ tensor contents.
14
+ """
15
+
16
+ from pathlib import Path
17
+ from unittest.mock import MagicMock, patch
18
+
19
+ import pytest
20
+ import torch
21
+
22
+
23
+ @pytest.fixture
24
+ def dp_model():
25
+ """One submesh handle whose optimizations return None (so the allocator
26
+ falls back to the bfloat8_b default — keeps the test independent of
27
+ the model's optimization config table)."""
28
+ submesh = MagicMock()
29
+ args = MagicMock()
30
+ args.optimizations = None # Force the bfloat8_b fallback path.
31
+ model = MagicMock()
32
+ model.mesh_device = submesh
33
+ model.args = args
34
+ return [model]
35
+
36
+
37
+ def _make_ttnn_mock():
38
+ ttnn_mock = MagicMock()
39
+ ttnn_mock.as_tensor.side_effect = lambda *a, **kw: ("tt-tensor", kw.get("dtype"), kw.get("cache_file_name"))
40
+ ttnn_mock.bfloat8_b = "bfloat8_b-sentinel"
41
+ ttnn_mock.bfloat16 = "bfloat16-sentinel"
42
+ return ttnn_mock
43
+
44
+
45
+ def test_per_layer_allocates_one_kv_pair_per_unique_tensor(dp_model):
46
+ """Each unique ``tensor_idx`` allocates one (k, v) pair; layers that
47
+ share a ``tensor_idx`` reuse the same handles."""
48
+ from models.tt_transformers.tt import generator_vllm
49
+
50
+ # Layers 0, 1, 2 all use tensor_idx=0,1,2 respectively → three buffers.
51
+ per_layer = [
52
+ ((4, 2, 32, 64), torch.bfloat16, 0),
53
+ ((4, 2, 32, 64), torch.bfloat16, 1),
54
+ ((4, 2, 32, 64), torch.bfloat16, 2),
55
+ ]
56
+
57
+ with patch.object(generator_vllm, "ttnn", new=_make_ttnn_mock()) as ttnn_mock:
58
+ kv_cache = generator_vllm.allocate_vllm_kv_cache_per_layer(
59
+ per_layer, dp_model=dp_model, tt_cache_path=Path("/tmp/tt-test-cache")
60
+ )
61
+
62
+ # One submesh, three layers, two tensors per layer (k, v) = 6 calls.
63
+ assert ttnn_mock.as_tensor.call_count == 6
64
+ assert len(kv_cache) == 1 # one submesh
65
+ assert len(kv_cache[0]) == 3 # three layers
66
+ assert all(len(layer) == 2 for layer in kv_cache[0]) # k, v
67
+
68
+
69
+ def test_shared_tensor_idx_reuses_one_buffer(dp_model):
70
+ """Layers sharing a ``tensor_idx`` (HMA tensor sharing) point at the
71
+ same underlying ttnn handles and only one allocation runs per
72
+ ``tensor_idx``."""
73
+ from models.tt_transformers.tt import generator_vllm
74
+
75
+ # Layers 0 and 2 share tensor 0; layer 1 has its own tensor 1.
76
+ per_layer = [
77
+ ((4, 2, 32, 64), torch.bfloat16, 0),
78
+ ((4, 2, 32, 64), torch.bfloat16, 1),
79
+ ((4, 2, 32, 64), torch.bfloat16, 0),
80
+ ]
81
+
82
+ with patch.object(generator_vllm, "ttnn", new=_make_ttnn_mock()) as ttnn_mock:
83
+ kv_cache = generator_vllm.allocate_vllm_kv_cache_per_layer(
84
+ per_layer, dp_model=dp_model, tt_cache_path=Path("/tmp/tt-test-cache")
85
+ )
86
+
87
+ # 2 unique tensor_idx values × 2 (k, v) = 4 allocations.
88
+ assert ttnn_mock.as_tensor.call_count == 4
89
+ # Layers 0 and 2 must reference the *same* handle list.
90
+ assert kv_cache[0][0] is kv_cache[0][2]
91
+ assert kv_cache[0][0] is not kv_cache[0][1]
92
+
93
+
94
+ def test_per_layer_keys_cache_filename_on_tensor_idx(dp_model):
95
+ """Cache filenames must distinguish independent buffers even when
96
+ shapes are identical, so on-disk caches can't collide across layers
97
+ that don't share a ``tensor_idx``."""
98
+ from models.tt_transformers.tt import generator_vllm
99
+
100
+ per_layer = [
101
+ ((4, 2, 32, 64), torch.bfloat16, 0),
102
+ ((4, 2, 32, 64), torch.bfloat16, 1),
103
+ ]
104
+
105
+ with patch.object(generator_vllm, "ttnn", new=_make_ttnn_mock()) as ttnn_mock:
106
+ generator_vllm.allocate_vllm_kv_cache_per_layer(
107
+ per_layer, dp_model=dp_model, tt_cache_path=Path("/tmp/tt-test-cache")
108
+ )
109
+
110
+ cache_filenames = [str(call.kwargs["cache_file_name"]) for call in ttnn_mock.as_tensor.call_args_list]
111
+ assert sum("_t0" in f for f in cache_filenames) == 2
112
+ assert sum("_t1" in f for f in cache_filenames) == 2
113
+
114
+
115
+ def test_legacy_uniform_shape_delegates_to_per_layer(dp_model):
116
+ """The legacy ``allocate_vllm_kv_cache`` must produce identical output to
117
+ calling ``allocate_vllm_kv_cache_per_layer`` with a per-layer triple
118
+ list (each layer its own ``tensor_idx``), so existing single-group
119
+ callers keep working unchanged."""
120
+ from models.tt_transformers.tt import generator_vllm
121
+
122
+ shape = (4, 2, 32, 64)
123
+ dtype = torch.bfloat16
124
+ num_layers = 3
125
+
126
+ with patch.object(generator_vllm, "ttnn", new=_make_ttnn_mock()) as ttnn_mock:
127
+ legacy = generator_vllm.allocate_vllm_kv_cache(
128
+ shape, dtype, num_layers, dp_model=dp_model, tt_cache_path=Path("/tmp/c")
129
+ )
130
+ legacy_call_count = ttnn_mock.as_tensor.call_count
131
+
132
+ with patch.object(generator_vllm, "ttnn", new=_make_ttnn_mock()) as ttnn_mock:
133
+ per_layer = generator_vllm.allocate_vllm_kv_cache_per_layer(
134
+ [(shape, dtype, i) for i in range(num_layers)],
135
+ dp_model=dp_model,
136
+ tt_cache_path=Path("/tmp/c"),
137
+ )
138
+ per_layer_call_count = ttnn_mock.as_tensor.call_count
139
+
140
+ assert legacy_call_count == per_layer_call_count
141
+ assert len(legacy[0]) == len(per_layer[0]) == num_layers
code/models/tt_transformers/tests/test_warm_cache_marker.py ADDED
@@ -0,0 +1,437 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """
6
+ Lightweight (CPU-only, no device, no model download) tests for the warm ttnn weight-cache
7
+ detector generalized into tt_transformers (issue #45400, generalizes GPT-OSS PR #48531).
8
+
9
+ These exercise the *real* ModelArgs.weight_cache_is_complete / mark_weight_cache_complete /
10
+ placeholder_state_dict logic by binding the unbound methods to a tiny stub whose
11
+ weight_cache_path points at a tmp dir -- so we validate marker round-trip, the shape/dtype
12
+ manifest, staleness rejection, the force-load override, the .tensorbin belt-and-suspenders
13
+ check, and the dataless placeholder state_dict without constructing a full ModelArgs.
14
+ """
15
+
16
+ import json
17
+ from pathlib import Path
18
+ from types import SimpleNamespace
19
+
20
+ import pytest
21
+ import torch
22
+
23
+ from models.tt_transformers.tt.model_config import ModelArgs
24
+
25
+ DTYPE = "bfp8" # opaque here: the stub's weight_cache_path ignores it; marker stores str(dtype)
26
+
27
+ SAMPLE_SD = {
28
+ "tok_embeddings.weight": torch.zeros(4, 8, dtype=torch.bfloat16),
29
+ "layers.0.attention.wo.weight": torch.zeros(8, 8, dtype=torch.float32),
30
+ }
31
+
32
+
33
+ class _FakeArgs:
34
+ """Minimal stand-in exposing exactly what the marker methods touch, with the real methods
35
+ bound so the production logic is under test."""
36
+
37
+ WEIGHT_CACHE_MARKER = ModelArgs.WEIGHT_CACHE_MARKER
38
+ WEIGHT_CACHE_FORMAT_VERSION = ModelArgs.WEIGHT_CACHE_FORMAT_VERSION
39
+ _weight_cache_identity = ModelArgs._weight_cache_identity
40
+ # Bound too: _weight_cache_identity calls it on self, so leaving it out made every gate call
41
+ # raise AttributeError -- and nothing ran this file to notice. (#45400 review, finding B1)
42
+ _weight_cache_build_variant = ModelArgs._weight_cache_build_variant
43
+ weight_cache_is_complete = ModelArgs.weight_cache_is_complete
44
+ mark_weight_cache_complete = ModelArgs.mark_weight_cache_complete
45
+ placeholder_state_dict = ModelArgs.placeholder_state_dict
46
+
47
+ def __init__(self, cache_dir, model_name="Test-Model-8B", n_layers=32, mesh_shape=(1, 8)):
48
+ self._cache_dir = Path(cache_dir)
49
+ self.model_name = model_name
50
+ self.n_layers = n_layers
51
+ self.dummy_weights = False
52
+ self.is_mixture_of_experts = False
53
+ self.mesh_device = SimpleNamespace(shape=mesh_shape)
54
+ # Everything _weight_cache_build_variant reads. get_tensor_dtype deliberately lives on
55
+ # self.optimizations (a DecodersPrecision in production), NOT on the args object: the
56
+ # variant helper shipped calling self.get_tensor_dtype and threw AttributeError on every
57
+ # model, and a stub that put the method on the args would have kept agreeing with that
58
+ # bug. Mirror the production shape so the stub can only pass against correct code.
59
+ self.prefetcher = None
60
+ self.max_batch_size = 1
61
+ self.use_fused_all_gather_matmul = False
62
+ self.use_hf_rope = False
63
+ self.optimizations = SimpleNamespace(
64
+ get_tensor_dtype=lambda decoder_id, tensor, prefetcher=False: "DataType.BFLOAT8_B"
65
+ )
66
+
67
+ def weight_cache_path(self, dtype):
68
+ return self._cache_dir
69
+
70
+
71
+ def _touch_tensorbin(cache_dir):
72
+ (Path(cache_dir) / "some.weight.tensorbin").write_bytes(b"\x00")
73
+
74
+
75
+ @pytest.fixture(autouse=True)
76
+ def _clear_force_env(monkeypatch):
77
+ monkeypatch.delenv("TT_TRANSFORMERS_FORCE_MODEL_LOAD", raising=False)
78
+
79
+
80
+ def test_cold_cache_is_incomplete(tmp_path):
81
+ args = _FakeArgs(tmp_path)
82
+ assert args.weight_cache_is_complete(DTYPE) is False
83
+
84
+
85
+ def test_mark_then_complete_roundtrip(tmp_path):
86
+ args = _FakeArgs(tmp_path)
87
+ _touch_tensorbin(tmp_path) # a real build writes tensor files alongside the marker
88
+ args.mark_weight_cache_complete(DTYPE, SAMPLE_SD)
89
+ assert args.weight_cache_is_complete(DTYPE) is True
90
+
91
+ # Marker payload includes the shape/dtype manifest.
92
+ meta = json.loads(marker_path(tmp_path, args._weight_cache_build_variant()).read_text())
93
+ assert meta["model_name"] == "Test-Model-8B"
94
+ assert meta["n_layers"] == 32
95
+ assert meta["mesh_shape"] == "(1, 8)"
96
+ assert meta["format_version"] == ModelArgs.WEIGHT_CACHE_FORMAT_VERSION
97
+ assert meta["weights"]["tok_embeddings.weight"] == [[4, 8], "torch.bfloat16"]
98
+
99
+
100
+ def test_marker_without_manifest_is_incomplete(tmp_path):
101
+ # A marker with no weight manifest (e.g. an old v1-style write) can't back a warm build.
102
+ args = _FakeArgs(tmp_path)
103
+ _touch_tensorbin(tmp_path)
104
+ args.mark_weight_cache_complete(DTYPE) # no state_dict -> weights == {}
105
+ assert args.weight_cache_is_complete(DTYPE) is False
106
+
107
+
108
+ def test_marker_without_tensorbin_is_incomplete(tmp_path):
109
+ args = _FakeArgs(tmp_path)
110
+ args.mark_weight_cache_complete(DTYPE, SAMPLE_SD)
111
+ # No .tensorbin present -> belt-and-suspenders check fails.
112
+ assert args.weight_cache_is_complete(DTYPE) is False
113
+
114
+
115
+ def test_force_env_disables_skip(tmp_path, monkeypatch):
116
+ args = _FakeArgs(tmp_path)
117
+ _touch_tensorbin(tmp_path)
118
+ args.mark_weight_cache_complete(DTYPE, SAMPLE_SD)
119
+ assert args.weight_cache_is_complete(DTYPE) is True # warm...
120
+ monkeypatch.setenv("TT_TRANSFORMERS_FORCE_MODEL_LOAD", "1")
121
+ assert args.weight_cache_is_complete(DTYPE) is False # ...but forced to cold-load
122
+
123
+
124
+ @pytest.mark.parametrize(
125
+ "mutate",
126
+ [
127
+ pytest.param({"format_version": 999}, id="stale-format"),
128
+ pytest.param({"model_name": "Other-Model"}, id="wrong-model"),
129
+ pytest.param({"n_layers": 1}, id="partial-build"),
130
+ pytest.param({"mesh_shape": "(2, 4)"}, id="wrong-mesh"),
131
+ pytest.param({"weights": {}}, id="empty-manifest"),
132
+ ],
133
+ )
134
+ def test_stale_marker_rejected(tmp_path, mutate):
135
+ args = _FakeArgs(tmp_path)
136
+ _touch_tensorbin(tmp_path)
137
+ args.mark_weight_cache_complete(DTYPE, SAMPLE_SD)
138
+ marker = marker_path(tmp_path, args._weight_cache_build_variant())
139
+ meta = json.loads(marker.read_text())
140
+ meta.update(mutate)
141
+ marker.write_text(json.dumps(meta))
142
+ assert args.weight_cache_is_complete(DTYPE) is False
143
+
144
+
145
+ def test_corrupt_marker_is_incomplete(tmp_path):
146
+ args = _FakeArgs(tmp_path)
147
+ _touch_tensorbin(tmp_path)
148
+ marker_path(tmp_path, args._weight_cache_build_variant()).write_text("{ not json")
149
+ assert args.weight_cache_is_complete(DTYPE) is False
150
+
151
+
152
+ def test_placeholder_state_dict_is_dataless_and_falsy(tmp_path):
153
+ args = _FakeArgs(tmp_path)
154
+ _touch_tensorbin(tmp_path)
155
+ args.mark_weight_cache_complete(DTYPE, SAMPLE_SD)
156
+
157
+ sd = args.placeholder_state_dict(DTYPE)
158
+ # Falsy so reference-building callers (`if not state_dict`) load real weights instead.
159
+ assert not sd
160
+ assert len(sd) == 2
161
+ assert set(sd.keys()) == set(SAMPLE_SD.keys())
162
+ # Reconstructs correct shape/dtype without any real data.
163
+ emb = sd["tok_embeddings.weight"]
164
+ assert tuple(emb.shape) == (4, 8)
165
+ assert emb.dtype == torch.bfloat16
166
+ assert sd["layers.0.attention.wo.weight"].dtype == torch.float32
167
+
168
+
169
+ # ---------------------------------------------------------------------------
170
+ # models/common/weight_cache.py -- the shared helper used by the forked loaders.
171
+ # The tests above bind the tt_transformers ModelArgs methods; these cover the shared
172
+ # module's own behaviour: sidecar capture/rejection, per-file completeness, component
173
+ # matching, atomic publish, and the CachedStateDict contract. (#45400 review)
174
+ # ---------------------------------------------------------------------------
175
+
176
+ from models.common.weight_cache import ( # noqa: E402
177
+ HOST_WEIGHTS_SIDECAR,
178
+ WEIGHT_CACHE_MARKER,
179
+ CachedStateDict,
180
+ build_cached_state_dict,
181
+ mark_weight_cache_complete,
182
+ marker_path,
183
+ normalize_mesh_shape,
184
+ weight_cache_is_complete,
185
+ )
186
+
187
+ SHARED_ID = dict(model_name="unit/test-model", n_layers=2, mesh_shape=(1, 8))
188
+
189
+
190
+ def _seed(tmp_path, *, components=None, is_host_weight=None, sd=None):
191
+ """Write a tensorbin then mark the cache complete, mimicking a real cold build."""
192
+ _touch_tensorbin(tmp_path)
193
+ mark_weight_cache_complete(
194
+ tmp_path, sd if sd is not None else SAMPLE_SD, components=components, is_host_weight=is_host_weight, **SHARED_ID
195
+ )
196
+
197
+
198
+ def test_shared_marker_roundtrip(tmp_path):
199
+ assert weight_cache_is_complete(tmp_path, **SHARED_ID) is False
200
+ _seed(tmp_path)
201
+ assert weight_cache_is_complete(tmp_path, **SHARED_ID) is True
202
+
203
+
204
+ def test_shared_marker_rejects_missing_tensorbin(tmp_path):
205
+ """A recorded tensorbin that later disappears must force a cold load -- otherwise as_tensor
206
+ regenerates it from the placeholder and writes garbage into the cache permanently."""
207
+ _seed(tmp_path)
208
+ for f in tmp_path.glob("*.tensorbin"):
209
+ f.unlink()
210
+ assert weight_cache_is_complete(tmp_path, **SHARED_ID) is False
211
+
212
+
213
+ def test_shared_marker_finds_tensorbins_in_subdirs(tmp_path):
214
+ """Forked loaders nest per-layer weights (qwen36 layers.N/, gemma4 layer_N/)."""
215
+ sub = tmp_path / "layers.0"
216
+ sub.mkdir()
217
+ (sub / "wq_dtype_BFLOAT8_B_layout_TILE.tensorbin").write_bytes(b"x")
218
+ mark_weight_cache_complete(tmp_path, SAMPLE_SD, **SHARED_ID)
219
+ assert weight_cache_is_complete(tmp_path, **SHARED_ID) is True
220
+ (sub / "wq_dtype_BFLOAT8_B_layout_TILE.tensorbin").unlink()
221
+ assert weight_cache_is_complete(tmp_path, **SHARED_ID) is False
222
+
223
+
224
+ def test_components_subset_matching(tmp_path):
225
+ """A text-only seed must not certify a build that also needs the vision tower; the reverse
226
+ (vision seed satisfying a text-only build) is fine."""
227
+ _seed(tmp_path, components=["text"])
228
+ assert weight_cache_is_complete(tmp_path, components=["text"], **SHARED_ID) is True
229
+ assert weight_cache_is_complete(tmp_path, components=["text", "vision"], **SHARED_ID) is False
230
+
231
+ _seed(tmp_path, components=["text", "vision"])
232
+ assert weight_cache_is_complete(tmp_path, components=["text"], **SHARED_ID) is True
233
+
234
+
235
+ def test_sidecar_capture_and_corruption_rejected(tmp_path):
236
+ _seed(tmp_path, is_host_weight=lambda k: k == "tok_embeddings.weight")
237
+ assert (tmp_path / HOST_WEIGHTS_SIDECAR).is_file()
238
+ assert weight_cache_is_complete(tmp_path, **SHARED_ID) is True
239
+
240
+ sd = build_cached_state_dict(tmp_path)
241
+ # The captured host weight is served REAL; everything else is a dataless placeholder.
242
+ assert torch.equal(sd["tok_embeddings.weight"], SAMPLE_SD["tok_embeddings.weight"])
243
+
244
+ # A torn sidecar must degrade to a cold load, not crash every later run.
245
+ (tmp_path / HOST_WEIGHTS_SIDECAR).write_bytes(b"not a torch file")
246
+ assert weight_cache_is_complete(tmp_path, **SHARED_ID) is False
247
+
248
+
249
+ def test_no_temp_files_left_behind(tmp_path):
250
+ _seed(tmp_path, is_host_weight=lambda k: k == "tok_embeddings.weight")
251
+ assert not list(tmp_path.glob("*.tmp*")), "atomic publish must not leave temp files"
252
+
253
+
254
+ def test_mesh_shape_encoding_is_writer_agnostic(tmp_path):
255
+ """Both marker writers must encode the mesh identically or each rejects the other's marker."""
256
+
257
+ class _MeshShape:
258
+ def __init__(self, dims):
259
+ self._dims = dims
260
+
261
+ def __iter__(self):
262
+ return iter(self._dims)
263
+
264
+ def __str__(self):
265
+ return f"MeshShape({list(self._dims)})"
266
+
267
+ assert normalize_mesh_shape(_MeshShape((1, 8))) == normalize_mesh_shape((1, 8))
268
+ _seed(tmp_path)
269
+ identity = dict(SHARED_ID, mesh_shape=_MeshShape((1, 8)))
270
+ assert weight_cache_is_complete(tmp_path, **identity) is True
271
+
272
+
273
+ def test_cached_state_dict_contract():
274
+ manifest = {k: [list(v.shape), str(v.dtype)] for k, v in SAMPLE_SD.items()}
275
+ real = SAMPLE_SD["tok_embeddings.weight"]
276
+ sd = CachedStateDict(manifest, {"tok_embeddings.weight": real})
277
+
278
+ # Truthy and flagged (the tt_transformers placeholder is falsy -- callers must branch on the
279
+ # attribute, not truthiness).
280
+ assert sd
281
+ assert sd.is_placeholder is True
282
+
283
+ # Membership must not materialize a tensor.
284
+ assert "layers.0.attention.wo.weight" in sd
285
+ assert "nope" not in sd
286
+ assert sd.get("nope") is None
287
+
288
+ # Mutable: loaders setdefault KV-shared weights.
289
+ sd["extra"] = torch.zeros(2)
290
+ assert "extra" in sd
291
+ del sd["extra"]
292
+ assert "extra" not in sd
293
+
294
+ del sd["tok_embeddings.weight"]
295
+ assert "tok_embeddings.weight" not in sd
296
+ try:
297
+ sd["tok_embeddings.weight"]
298
+ raise AssertionError("a deleted key must raise KeyError, even when the sidecar still has it")
299
+ except KeyError:
300
+ pass
301
+
302
+
303
+ def test_build_variant_must_match_exactly(tmp_path):
304
+ """Build options that change an as_tensor cache FILENAME (prefetcher, precision) are matched
305
+ exactly, not as a superset: a different variant needs DIFFERENT files, and any it is missing
306
+ would be regenerated from the placeholder rather than cold-loaded."""
307
+ perf = {"prefetcher": False, "precision": "aaaaaaaaaaaa"}
308
+ _touch_tensorbin(tmp_path)
309
+ mark_weight_cache_complete(tmp_path, SAMPLE_SD, build_variant=perf, **SHARED_ID)
310
+
311
+ assert weight_cache_is_complete(tmp_path, build_variant=perf, **SHARED_ID) is True
312
+ # different precision config
313
+ assert (
314
+ weight_cache_is_complete(tmp_path, build_variant={"prefetcher": False, "precision": "bbbb"}, **SHARED_ID)
315
+ is False
316
+ )
317
+ # prefetcher flips the dtypes and adds ring-matmul splits
318
+ assert (
319
+ weight_cache_is_complete(tmp_path, build_variant={"prefetcher": True, "precision": "aaaaaaaaaaaa"}, **SHARED_ID)
320
+ is False
321
+ )
322
+ # a caller that records no variant must not be satisfied by one that did
323
+ assert weight_cache_is_complete(tmp_path, **SHARED_ID) is False
324
+
325
+
326
+ # ---------------------------------------------------------------------------
327
+ # Review-fix coverage (#45400 findings B3 / R1 / R3).
328
+ # ---------------------------------------------------------------------------
329
+
330
+ import models.common.weight_cache as _wc # noqa: E402
331
+
332
+
333
+ def test_variant_markers_coexist(tmp_path):
334
+ """One marker file PER build variant: two variants sharing a cache dir must not evict each
335
+ other's marker. The live case is the Llama CI job running eval-32 with and without the DRAM
336
+ prefetcher against one instruct cache -- a single exactly-matched marker made each leg's seed
337
+ clobber the other's, so both cold-loaded forever with nothing going red. (finding B3)"""
338
+ no_pf = {"prefetcher": False, "precision": "aaaaaaaaaaaa"}
339
+ with_pf = {"prefetcher": True, "precision": "aaaaaaaaaaaa"}
340
+ _touch_tensorbin(tmp_path)
341
+ mark_weight_cache_complete(tmp_path, SAMPLE_SD, build_variant=no_pf, **SHARED_ID)
342
+ mark_weight_cache_complete(tmp_path, SAMPLE_SD, build_variant=with_pf, **SHARED_ID)
343
+
344
+ # Both warm at once -- the second seed did not evict the first.
345
+ assert weight_cache_is_complete(tmp_path, build_variant=no_pf, **SHARED_ID) is True
346
+ assert weight_cache_is_complete(tmp_path, build_variant=with_pf, **SHARED_ID) is True
347
+ assert marker_path(tmp_path, no_pf) != marker_path(tmp_path, with_pf)
348
+ # A variant nobody seeded stays cold.
349
+ assert (
350
+ weight_cache_is_complete(tmp_path, build_variant={"prefetcher": False, "precision": "b"}, **SHARED_ID) is False
351
+ )
352
+ # And each variant's builder reads its own manifest.
353
+ sd = build_cached_state_dict(tmp_path, build_variant=with_pf)
354
+ assert set(sd.keys()) == set(SAMPLE_SD.keys())
355
+
356
+
357
+ def test_unverifiable_variant_fails_closed(tmp_path):
358
+ """A build variant that could not be computed must never certify or match a cache: the gate
359
+ returns False and mark refuses to write, so the run cold-loads instead of risking a
360
+ placeholder persist under an unchecked filename set. (finding R3)"""
361
+ bad = {"unverifiable": True, "error": "RuntimeError: boom"}
362
+ _touch_tensorbin(tmp_path)
363
+ mark_weight_cache_complete(tmp_path, SAMPLE_SD, build_variant=bad, **SHARED_ID)
364
+ assert not list(tmp_path.glob(f"{WEIGHT_CACHE_MARKER}*")), "unverifiable variant must not write a marker"
365
+ # Even with a marker forged at the matching path, the gate rejects the request side.
366
+ mark_weight_cache_complete(tmp_path, SAMPLE_SD, **SHARED_ID) # legit .none marker
367
+ assert weight_cache_is_complete(tmp_path, build_variant=bad, **SHARED_ID) is False
368
+
369
+
370
+ def test_modelargs_variant_error_disables_skip(tmp_path):
371
+ """ModelArgs path: if computing the precision signature raises, the sentinel flows through
372
+ identity -> gate -> False (cold load), and marking is refused -- instead of the old behaviour
373
+ of collapsing to a match-anything 'unknown'. (finding R3)"""
374
+ args = _FakeArgs(tmp_path)
375
+ _touch_tensorbin(tmp_path)
376
+
377
+ def _boom(decoder_id, tensor, prefetcher=False):
378
+ raise RuntimeError("precision config unavailable")
379
+
380
+ args.optimizations = SimpleNamespace(get_tensor_dtype=_boom)
381
+ variant = args._weight_cache_build_variant()
382
+ assert variant.get("unverifiable") is True
383
+ args.mark_weight_cache_complete(DTYPE, SAMPLE_SD)
384
+ assert not list(Path(tmp_path).glob(f"{WEIGHT_CACHE_MARKER}*"))
385
+ assert args.weight_cache_is_complete(DTYPE) is False
386
+
387
+
388
+ def test_hf_rope_is_part_of_the_variant(tmp_path):
389
+ """load_state_dict permutes QKV differently per rope mode under the SAME cache filenames, so a
390
+ marker seeded in one mode must not certify the other. (finding R2)"""
391
+ args = _FakeArgs(tmp_path)
392
+ _touch_tensorbin(tmp_path)
393
+ args.mark_weight_cache_complete(DTYPE, SAMPLE_SD)
394
+ assert args.weight_cache_is_complete(DTYPE) is True
395
+ args.use_hf_rope = True
396
+ assert args.weight_cache_is_complete(DTYPE) is False
397
+
398
+
399
+ def test_sidecar_loaded_once_per_warm_run(tmp_path, monkeypatch):
400
+ """The completeness gate's validation load must be reused by build_cached_state_dict, not
401
+ repeated -- the sidecar can be multi-GB on NAS. The builder consumes the memoized entry so the
402
+ tensors are not pinned afterwards. (finding R1)"""
403
+ _seed(tmp_path, is_host_weight=lambda k: k == "tok_embeddings.weight")
404
+
405
+ real_load = torch.load
406
+ calls = []
407
+
408
+ def counting_load(*a, **k):
409
+ calls.append(a[0] if a else k.get("f"))
410
+ return real_load(*a, **k)
411
+
412
+ monkeypatch.setattr(torch, "load", counting_load)
413
+ _wc._SIDECAR_CACHE.clear()
414
+
415
+ assert weight_cache_is_complete(tmp_path, **SHARED_ID) is True
416
+ sd = build_cached_state_dict(tmp_path)
417
+ assert torch.equal(sd["tok_embeddings.weight"], SAMPLE_SD["tok_embeddings.weight"])
418
+ assert len(calls) == 1, f"sidecar torch.load'ed {len(calls)}x per warm run, expected 1"
419
+ assert not _wc._SIDECAR_CACHE, "builder must consume the memoized sidecar entry"
420
+
421
+
422
+ def test_mark_survives_non_oserror_write_failure(tmp_path, monkeypatch):
423
+ """mark_weight_cache_complete only records completion; a failure to record must warn, not
424
+ raise. torch.save on a read-only mount raises RuntimeError from the C++ serializer (not
425
+ OSError), which crashed every read-only cold run of the sidecar models right after a
426
+ successful build. (finding R5, Gemma-4-E2B bh_p150, run 32511945147)"""
427
+ _touch_tensorbin(tmp_path)
428
+
429
+ def _ro_save(*a, **k):
430
+ raise RuntimeError(
431
+ "[enforce fail at inline_container.cc:747] . open file failed with strerror: Read-only file system"
432
+ )
433
+
434
+ monkeypatch.setattr(torch, "save", _ro_save)
435
+ # Must not raise, and must not publish a marker that claims a sidecar it could not write.
436
+ mark_weight_cache_complete(tmp_path, SAMPLE_SD, is_host_weight=lambda k: k == "tok_embeddings.weight", **SHARED_ID)
437
+ assert weight_cache_is_complete(tmp_path, **SHARED_ID) is False
image/index.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:c4805783cb377cf99c36c43a26f9cf41f4605732b8dd244d9a11211a9d290708","size":856,"annotations":{"io.containerd.image.name":"docker.io/tt-model/qwen3.8-flash-next-p300x2:c4805783cb37","org.opencontainers.image.ref.name":"c4805783cb37"}}]}
image/manifest.json ADDED
@@ -0,0 +1 @@
 
 
1
+ [{"Config":"blobs/sha256/2d4ec0ee7e1d8e2afa34d4462fd36e238adc97abc08950df6826e1ba6ae51b0a","RepoTags":["tt-model/qwen3.8-flash-next-p300x2:c4805783cb37"],"Layers":["blobs/sha256/d544298cabd50e7c86bfef1e52b67f01db6b3a57bfecfe37a851873dee83e52a","blobs/sha256/926bdef455064f157521bce0256de58abdbaab511c6c27e6159d549865f318fa","blobs/sha256/5ab8bdd2eb9167ebfb3bcfbf8c8a130f606bdc70c07b847c0a7fdaa502049043","blobs/sha256/df309695d498e77bd9a50ea7f3050e22efd1eae88c6611dbc89606e7e414ea78","blobs/sha256/e59b07f819fd19b7b5194adc92ccb10272747a70622188426f6ea64ba4775dbd","blobs/sha256/22aac323a4902588424acb82548cb3942084b85d5002631163f0732023598ff6","blobs/sha256/bf170f51420a62eeb56b8f6afb7a86e8f78ff820ccbbf7c9f543dcf76230aab5","blobs/sha256/bfe3bd79fd494c00040a96308433fad0dfaa9c8599707dcfa728cf9aac71c63e","blobs/sha256/e6675335cae939ad7a92ef21ad2b5afcf94f773a947383d7be56ac2af240a2ca","blobs/sha256/0741ec9077e1cca75a2621bc81f592f783376ffcbcd3d28b448336bdd57df894","blobs/sha256/cb9d04479d4428f62ea6193b0afa5c626ad8c5cb85af4bb3707b628747c4f303","blobs/sha256/a8d0a837d932862db7e85f056a9739d2fd6e1a44e4187266c268a6ca3eb2147c","blobs/sha256/2c83578d6015b0ec43dc4f1776e4ee53bd60491c15de2ad76c01b7f4fb6cb1c1","blobs/sha256/be22a17157f88c31873fddab027afdabbc75acbd737e4e6832f5a27147888915","blobs/sha256/5380a77208b9d0a514bc798ae2f366739a45192c19ac74005fea7eb2432a55a5","blobs/sha256/0babaa50033ef09eb22eeb3a79677044a82f7fc85b37ae8ca184d47f27c5c150","blobs/sha256/4637c6b0dd8ff29e9c09c442bcc91a26734b5df20edbcdce665497db5460bb65","blobs/sha256/5ff7f4251980e087f0e4491f352436860694fd3cd5cf13ce6b88c87e9da9ea10","blobs/sha256/4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1","blobs/sha256/d9efe2bed67066cab065e775a92326ebe261059ff9dc356d11fd5a51f778285d","blobs/sha256/532e399e6e08e45df8593c1496794824dcc34b86681a0dde5d8d166b02b5615f","blobs/sha256/a697d5857e3141dfa04762d522373c42fae90b16056bc40371802fa4574d5b25"]}]
image/oci-layout ADDED
@@ -0,0 +1 @@
 
 
1
+ {"imageLayoutVersion":"1.0.0"}
requirements.lock ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ accelerate==1.7.0
2
+ aiofiles==25.1.0
3
+ aiohappyeyeballs==2.7.1
4
+ aiohttp==3.14.3
5
+ aiosignal==1.4.0
6
+ alabaster==0.7.16
7
+ annotated-doc==0.0.5
8
+ annotated-types==0.8.0
9
+ anthropic==0.122.0
10
+ anyio==4.14.2
11
+ apache-tvm-ffi==0.1.9
12
+ argcomplete==3.7.2
13
+ argon2-cffi==25.1.0
14
+ argon2-cffi-bindings==25.1.0
15
+ arrow==1.4.0
16
+ astor==0.8.1
17
+ asttokens==3.0.2
18
+ async-lru==2.3.0
19
+ attrs==26.1.0
20
+ audioread==3.1.0
21
+ av==18.0.0
22
+ babel==2.18.0
23
+ backcall==0.2.0
24
+ backports-zstd==1.6.0
25
+ beautifulsoup4==4.15.0
26
+ beautifultable==1.1.0
27
+ bert-score==0.3.12
28
+ black==26.3.1
29
+ blake3==1.0.9
30
+ bleach==6.4.0
31
+ blessed==1.48.0
32
+ blinker==1.9.0
33
+ blobfile==3.1.0
34
+ bokeh==3.8.2
35
+ boto3==1.43.68
36
+ botocore==1.43.68
37
+ breathe==4.35.0
38
+ brotli==1.2.0
39
+ build==0.10.0
40
+ cachetools==7.1.7
41
+ cbor2==6.1.4
42
+ certifi==2026.7.22
43
+ cffi==2.1.1
44
+ cfgv==3.5.0
45
+ charset-normalizer==3.4.9
46
+ choreographer==1.2.1
47
+ clang-format==19.1.4
48
+ click==8.1.7
49
+ cloudpickle==3.1.2
50
+ comm==0.2.3
51
+ compressed-tensors==0.17.0
52
+ contourpy==1.3.3
53
+ coverage==7.15.4
54
+ cryptography==50.0.0
55
+ cycler==0.12.1
56
+ dacite==1.7.0
57
+ dash==2.15.0
58
+ dash-core-components==2.0.0
59
+ dash-html-components==2.0.0
60
+ dash-table==5.0.0
61
+ datasets==2.21.0
62
+ debugpy==1.8.21
63
+ decorator==5.3.1
64
+ defusedxml==0.7.1
65
+ deprecated==1.3.1
66
+ deprecation==2.1.0
67
+ depyf==0.20.0
68
+ detect-installer==0.1.0
69
+ diffusers==0.38.0
70
+ dill==0.3.8
71
+ diskcache==5.6.3
72
+ distlib==0.4.3
73
+ distro==1.9.0
74
+ dnspython==2.8.0
75
+ docopt==0.6.2
76
+ docstring-parser==0.18.0
77
+ docutils==0.18.1
78
+ editorconfig==0.17.1
79
+ efficientnet-pytorch==0.7.1
80
+ einops==0.6.1
81
+ email-validator==2.3.0
82
+ enlighten==1.12.4
83
+ evaluate==0.4.0
84
+ executing==2.2.1
85
+ fastapi==0.136.3
86
+ fastapi-cli==0.0.32
87
+ fastapi-cloud-cli==0.23.0
88
+ fastar==0.11.0
89
+ faster-fifo==1.5.2
90
+ fastjsonschema==2.22.1
91
+ fastnumbers==5.1.1
92
+ fastsafetensors==0.3.3
93
+ filelock==3.32.2
94
+ flask==3.0.3
95
+ fonttools==4.63.0
96
+ fqdn==1.5.1
97
+ frozenlist==1.8.0
98
+ fsspec==2023.9.2
99
+ ftfy==6.1.1
100
+ future==1.0.0
101
+ gitdb==4.0.12
102
+ gitpython==3.1.55
103
+ glob2==0.7
104
+ googleapis-common-protos==1.75.0
105
+ graphql-core==3.2.11
106
+ graphviz==0.21
107
+ grpcio==1.83.0
108
+ h11==0.16.0
109
+ h2==4.4.1
110
+ hf-xet==1.6.0
111
+ hpack==4.2.0
112
+ httpcore==1.0.9
113
+ httpcore2==2.10.0
114
+ httptools==0.8.0
115
+ httpx==0.28.1
116
+ httpx2==2.10.0
117
+ huggingface-hub==1.16.1
118
+ humanize==4.16.0
119
+ humming-kernels==0.1.6
120
+ hypercorn==0.18.0
121
+ hyperframe==6.1.0
122
+ identify==2.6.19
123
+ idna==3.18
124
+ ijson==3.5.1
125
+ imageio==2.37.4
126
+ imagesize==2.0.0
127
+ importlib-metadata==9.0.0
128
+ inflate64==1.0.4
129
+ iniconfig==2.3.0
130
+ interegular==0.3.3
131
+ ipykernel==7.3.0
132
+ ipython==8.12.3
133
+ ipywidgets==8.1.1
134
+ isoduration==20.11.0
135
+ itsdangerous==2.2.0
136
+ jaraco-classes==3.4.0
137
+ jaraco-context==6.1.2
138
+ jaraco-functools==4.6.0
139
+ jedi==0.20.0
140
+ jeepney==0.9.0
141
+ jinja2==3.1.6
142
+ jinxed==2.1.0
143
+ jiter==0.16.0
144
+ jiwer==3.0.5
145
+ jmespath==1.1.0
146
+ joblib==1.5.3
147
+ jsbeautifier==1.14.7
148
+ json5==0.15.0
149
+ jsonlines==4.0.0
150
+ jsonpointer==3.1.1
151
+ jsonschema==4.26.0
152
+ jsonschema-specifications==2025.9.1
153
+ jupyter-client==8.9.1
154
+ jupyter-core==5.9.1
155
+ jupyter-events==0.12.1
156
+ jupyter-lsp==2.3.1
157
+ jupyter-server==2.20.0
158
+ jupyter-server-terminals==0.5.4
159
+ jupyterlab==4.5.9
160
+ jupyterlab-pygments==0.3.0
161
+ jupyterlab-server==2.28.0
162
+ jupyterlab-widgets==3.0.16
163
+ kagglehub==0.2.9
164
+ kaleido==1.2.0
165
+ keyring==25.7.0
166
+ kiwisolver==1.5.0
167
+ lark==1.2.2
168
+ lazy-loader==0.5
169
+ librosa==0.10.0
170
+ lightning-utilities==0.15.3
171
+ llguidance==1.7.6
172
+ llvmlite==0.47.0
173
+ lm-format-enforcer==0.11.3
174
+ logistro==2.0.1
175
+ loguru==0.6.0
176
+ lxml==6.1.0
177
+ markdown-it-py==3.0.0
178
+ markupsafe==3.0.3
179
+ matplotlib==3.10.9
180
+ matplotlib-inline==0.2.2
181
+ mcp==2.0.0
182
+ mcp-types==2.0.0
183
+ mdit-py-plugins==0.6.1
184
+ mdurl==0.1.2
185
+ medpy==0.5.2
186
+ mistral-common==1.11.7
187
+ mistune==3.3.4
188
+ ml-dtypes==0.5.4
189
+ model-hosting-container-standards==0.1.16
190
+ mongoengine==0.24.2
191
+ more-itertools==11.1.0
192
+ motor==3.5.3
193
+ mpmath==1.3.0
194
+ msgpack==1.2.1
195
+ msgspec==0.21.1
196
+ multidict==6.7.1
197
+ multiprocess==0.70.14
198
+ multivolumefile==0.2.3
199
+ mypy==1.9.0
200
+ mypy-extensions==1.1.0
201
+ myst-parser==3.0.0
202
+ narwhals==2.24.0
203
+ nbclient==0.11.0
204
+ nbconvert==7.17.1
205
+ nbformat==5.11.0
206
+ nbsphinx==0.9.3
207
+ nest-asyncio==1.6.0
208
+ nest-asyncio2==1.7.2
209
+ networkx==3.1
210
+ nh3==0.3.6
211
+ ninja==1.13.0
212
+ nodeenv==1.10.0
213
+ notebook-shim==0.2.4
214
+ numba==0.65.0
215
+ numpy==1.26.4
216
+ open-clip-torch==2.26.1
217
+ openai==3.1.0
218
+ openai-harmony==0.0.8
219
+ opencv-python-headless==4.11.0.86
220
+ opentelemetry-api==1.44.0
221
+ opentelemetry-exporter-otlp==1.44.0
222
+ opentelemetry-exporter-otlp-proto-common==1.44.0
223
+ opentelemetry-exporter-otlp-proto-grpc==1.44.0
224
+ opentelemetry-exporter-otlp-proto-http==1.44.0
225
+ opentelemetry-proto==1.44.0
226
+ opentelemetry-sdk==1.44.0
227
+ opentelemetry-semantic-conventions==0.65b0
228
+ opentelemetry-semantic-conventions-ai==0.5.1
229
+ orjson==3.11.9
230
+ outlines-core==0.2.14
231
+ packaging==26.3
232
+ pandas==2.3.3
233
+ pandoc==2.3
234
+ pandocfilters==1.5.1
235
+ parso==0.8.7
236
+ partial-json-parser==0.2.1.1.post7
237
+ pathspec==1.1.1
238
+ peft==0.19.1
239
+ pexpect==4.9.0
240
+ pickleshare==0.7.5
241
+ pillow==12.3.0
242
+ pkginfo==1.12.1.2
243
+ platformdirs==3.11.0
244
+ plotly==5.18.0
245
+ pluggy==1.6.0
246
+ plumbum==2.0.2
247
+ ply==3.11
248
+ pooch==1.9.0
249
+ pprintpp==0.4.0
250
+ pre-commit==3.5.0
251
+ prefixed==0.9.0
252
+ priority==2.0.0
253
+ prometheus-client==0.26.0
254
+ prometheus-fastapi-instrumentator==8.1.0
255
+ prompt-toolkit==3.0.52
256
+ propcache==0.5.2
257
+ protobuf==5.29.6
258
+ psutil==7.2.2
259
+ psycopg2-binary==2.9.11
260
+ ptyprocess==0.7.0
261
+ pure-eval==0.2.3
262
+ py-cpuinfo==9.0.0
263
+ py7zr==1.1.3
264
+ pyarrow==21.0.0
265
+ pybase64==1.5.0
266
+ pybcj==1.0.8
267
+ pycapnp==2.0.0
268
+ pycountry==26.2.16
269
+ pycparser==3.0
270
+ pycryptodomex==3.23.0
271
+ pydantic==2.13.4
272
+ pydantic-core==2.46.4
273
+ pydantic-extra-types==2.11.1
274
+ pydantic-settings==2.15.0
275
+ pydash==8.0.6
276
+ pyelftools==0.33
277
+ pygments==2.20.0
278
+ pyjwt==2.13.0
279
+ pymongo==4.8.0
280
+ pyparsing==3.3.2
281
+ pyppmd==1.3.1
282
+ pyproject-hooks==1.2.0
283
+ pyro5==5.16
284
+ pytest==9.0.3
285
+ pytest-benchmark==5.2.3
286
+ pytest-cov==7.0.0
287
+ pytest-github-actions-annotate-failures==0.3.0
288
+ pytest-repeat==0.9.4
289
+ pytest-split==0.11.0
290
+ pytest-timeout==2.4.0
291
+ python-dateutil==2.9.0.post0
292
+ python-discovery==1.5.1
293
+ python-dotenv==1.2.2
294
+ python-json-logger==4.1.0
295
+ python-multipart==0.0.32
296
+ pytokens==0.4.1
297
+ pytz==2026.3.post1
298
+ pyyaml==6.0.3
299
+ pyzmq==27.1.0
300
+ quack-kernels==0.5.0
301
+ qwen-vl-utils==0.0.14
302
+ rapidfuzz==3.14.5
303
+ rapidyaml==0.12.0
304
+ rarfile==4.5
305
+ readme-renderer==43.0
306
+ referencing==0.37.0
307
+ regex==2026.7.19
308
+ requests==2.34.2
309
+ requests-toolbelt==1.0.0
310
+ responses==0.18.0
311
+ retrying==1.4.2
312
+ rfc3339-validator==0.1.4
313
+ rfc3986==2.0.0
314
+ rfc3986-validator==0.1.1
315
+ rfc3987-syntax==1.1.0
316
+ rich==15.0.0
317
+ rich-toolkit==0.20.3
318
+ rignore==0.8.1
319
+ rpds-py==2026.6.3
320
+ s3transfer==0.19.2
321
+ safetensors==0.8.0
322
+ scikit-image==0.26.0
323
+ scikit-learn==1.9.0
324
+ scipy==1.17.1
325
+ seaborn==0.13.2
326
+ secretstorage==3.5.0
327
+ send2trash==2.1.0
328
+ sentencepiece==0.2.1
329
+ sentry-sdk==2.68.0
330
+ serpent==1.43
331
+ setproctitle==1.3.7
332
+ setuptools==80.10.2
333
+ shellingham==1.5.4
334
+ simpleitk==2.5.6
335
+ simplejson==4.1.1
336
+ six==1.17.0
337
+ smmap==5.0.3
338
+ sniffio==1.3.1
339
+ snowballstemmer==3.1.1
340
+ sortedcontainers==2.4.0
341
+ soundfile==0.14.0
342
+ soupsieve==2.9.2
343
+ soxr==1.1.0
344
+ sphinx==7.1.2
345
+ sphinx-rtd-theme==1.3.0
346
+ sphinxcontrib-applehelp==2.0.0
347
+ sphinxcontrib-devhelp==2.0.0
348
+ sphinxcontrib-email==0.3.5
349
+ sphinxcontrib-htmlhelp==2.1.0
350
+ sphinxcontrib-jquery==4.1
351
+ sphinxcontrib-jsmath==1.0.1
352
+ sphinxcontrib-qthelp==2.0.0
353
+ sphinxcontrib-serializinghtml==2.0.0
354
+ sse-starlette==3.4.8
355
+ sseclient-py==1.9.0
356
+ stack-data==0.6.3
357
+ starlette==1.6.0
358
+ strawberry-graphql==0.138.1
359
+ supervisor==4.3.0
360
+ sympy==1.14.0
361
+ tabulate==0.9.0
362
+ tblib==3.2.2
363
+ tenacity==9.1.4
364
+ termcolor==3.3.0
365
+ terminado==0.18.1
366
+ texttable==1.7.0
367
+ threadpoolctl==3.6.0
368
+ tifffile==2026.3.3
369
+ tiktoken==0.7.0
370
+ tilelang==0.1.9
371
+ timm==1.0.28
372
+ tinycss2==1.5.1
373
+ tokenizers==0.23.1
374
+ tokenspeed-mla==0.1.2
375
+ tokenspeed-triton==3.8.10.post20260721
376
+ toolz==0.12.0
377
+ torch==2.11.0+cpu
378
+ torch-c-dlpack-ext==0.1.5
379
+ torch-fidelity==0.3.0
380
+ torchmetrics==1.9.0
381
+ torchvision==0.26.0+cpu
382
+ tornado==6.5.8
383
+ tqdm==4.66.3
384
+ traitlets==5.16.1
385
+ transformers==5.16.0
386
+ triton==3.7.1
387
+ truststore==0.10.4
388
+ tt-exalens==0.3.27
389
+ tt-perf-report==1.2.8
390
+ tt-umd==0.9.9
391
+ twine==4.0.2
392
+ typer==0.27.1
393
+ typing-extensions==4.16.0
394
+ typing-inspection==0.4.4
395
+ tzdata==2026.3
396
+ tzlocal==5.4.4
397
+ universal-analytics-python3==1.1.1
398
+ uri-template==1.3.0
399
+ urllib3==2.7.0
400
+ uv==0.9.26
401
+ uvicorn==0.52.3
402
+ uvloop==0.22.1
403
+ virtualenv==21.7.4
404
+ watchfiles==1.2.0
405
+ wcwidth==0.8.2
406
+ webcolors==25.10.0
407
+ webencodings==0.5.1
408
+ websocket-client==1.9.0
409
+ websockets==16.0
410
+ werkzeug==3.0.6
411
+ wheel==0.47.0
412
+ widgetsnbextension==4.0.15
413
+ wrapt==2.3.0
414
+ wsproto==1.3.2
415
+ xgrammar==0.2.3
416
+ xlsxwriter==3.0.8
417
+ xmltodict==1.0.4
418
+ xxhash==3.8.1
419
+ xyzservices==2026.3.0
420
+ yamllint==1.32.0
421
+ yarl==1.24.5
422
+ z3-solver==4.15.4.0
423
+ zipp==4.1.0