Add files using upload-large-folder tool
Browse files- README.md +75 -0
- code/models/common/README.md +319 -0
- code/models/common/auto_compose.py +161 -0
- code/models/common/device_utils.py +46 -0
- code/models/common/distribute_as.py +129 -0
- code/models/common/generation_utils.py +279 -0
- code/models/common/helper_funcs.py +43 -0
- code/models/common/lightweightmodule.py +12 -0
- code/models/common/llama_models.py +212 -0
- code/models/common/metrics.py +488 -0
- code/models/common/model_capabilities.py +28 -0
- code/models/common/rmsnorm.py +258 -0
- code/models/common/tensor_utils.py +229 -0
- code/models/common/utility_functions.py +1266 -0
- code/models/common/utils.py +53 -0
- code/models/common/validation_tools.py +719 -0
- code/models/common/weight_cache.py +426 -0
- code/models/tt_transformers/Host_Mem_Profile.md +88 -0
- code/models/tt_transformers/PERF.md +247 -0
- code/models/tt_transformers/README.md +388 -0
- code/models/tt_transformers/conftest.py +36 -0
- code/models/tt_transformers/host_mem_profiler.py +188 -0
- code/models/tt_transformers/scripts/op_perf_results.py +190 -0
- code/models/tt_transformers/scripts/repack_weights_70b.py +96 -0
- code/models/tt_transformers/scripts/repack_weights_90b.py +193 -0
- code/models/tt_transformers/tests/conftest.py +55 -0
- code/models/tt_transformers/tests/generate_reference_outputs.py +172 -0
- code/models/tt_transformers/tests/generate_reference_outputs.sh +82 -0
- code/models/tt_transformers/tests/test_attention_prefill.py +277 -0
- code/models/tt_transformers/tests/test_batched_prefill_slots.py +122 -0
- code/models/tt_transformers/tests/test_ci_dispatch.py +54 -0
- code/models/tt_transformers/tests/test_decoder.py +281 -0
- code/models/tt_transformers/tests/test_device_perf.py +362 -0
- code/models/tt_transformers/tests/test_hybrid_attention_for_causal_lm.py +143 -0
- code/models/tt_transformers/tests/test_interleaved_to_sharded.py +89 -0
- code/models/tt_transformers/tests/test_llama90b_decoder_json.py +72 -0
- code/models/tt_transformers/tests/test_lm_head.py +110 -0
- code/models/tt_transformers/tests/test_lm_head_cache.py +42 -0
- code/models/tt_transformers/tests/test_mha_wo_sharding.py +81 -0
- code/models/tt_transformers/tests/test_mlp.py +133 -0
- code/models/tt_transformers/tests/test_model_config_utils.py +48 -0
- code/models/tt_transformers/tests/test_model_prefill.py +322 -0
- code/models/tt_transformers/tests/test_rope_utils.py +20 -0
- code/models/tt_transformers/tests/test_utils.py +439 -0
- code/models/tt_transformers/tests/test_vllm_kv_cache.py +141 -0
- code/models/tt_transformers/tests/test_warm_cache_marker.py +437 -0
- image/index.json +1 -0
- image/manifest.json +1 -0
- image/oci-layout +1 -0
- 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
|