Add files using upload-large-folder tool
Browse files- README.md +60 -0
- code/models/common/README.md +319 -0
- code/models/common/auto_compose.py +161 -0
- code/models/common/demos/llama31_8B_demo.py +823 -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 +487 -0
- code/models/common/model_capabilities.py +28 -0
- code/models/common/readiness_check/check_degenerate_output.py +495 -0
- code/models/common/readiness_check/run_prefill_check.py +266 -0
- code/models/common/readiness_check/schema.py +143 -0
- code/models/common/readiness_check/teacher_forcing.py +182 -0
- code/models/common/readiness_check/vllm_prompts.txt +11 -0
- code/models/common/rmsnorm.py +203 -0
- code/models/common/sampling/README.md +113 -0
- code/models/common/sampling/__init__.py +33 -0
- code/models/common/sampling/_utils.py +91 -0
- code/models/common/sampling/generator.py +932 -0
- code/models/common/sampling/sampling_params.py +24 -0
- code/models/common/sampling/tt_log_probs.py +713 -0
- code/models/common/sampling/tt_penalties.py +390 -0
- code/models/common/sampling/tt_sampling.py +655 -0
- code/models/common/tensor_utils.py +174 -0
- code/models/common/tests/conftest.py +265 -0
- code/models/common/tests/requirements.txt +1 -0
- code/models/common/tests/setup.cfg +12 -0
- code/models/common/tests/test_auto_compose.py +416 -0
- code/models/common/tests/test_device_lock.py +111 -0
- code/models/common/tests/test_distribute_as.py +240 -0
- code/models/common/tests/test_executor_parity.py +167 -0
- code/models/common/tests/test_executor_trace_prefill.py +57 -0
- code/models/common/tests/test_lazy_weight.py +988 -0
- code/models/common/tests/test_metrics.py +584 -0
- code/models/common/tests/test_module_input_validation.py +91 -0
- code/models/common/tests/test_sampling.py +598 -0
- code/models/common/tests/test_utils.py +17 -0
- code/models/common/tests/test_validation_tools.py +622 -0
- code/models/common/tests/utils.py +20 -0
- code/models/common/utility_functions.py +1216 -0
- code/models/common/utils.py +53 -0
- code/models/common/validation_tools.py +719 -0
- code/models/demos/gpt_oss/tt/ccl.py +95 -0
- image/index.json +1 -0
- image/manifest.json +1 -0
- image/oci-layout +1 -0
- requirements.lock +215 -0
- tt_kernel_manifest.json +150 -0
README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
tags:
|
| 3 |
+
- blackhole
|
| 4 |
+
- p300x2
|
| 5 |
+
- tt-model-cache
|
| 6 |
+
- tt-model-container
|
| 7 |
+
- vllm-plugin
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# nemotron-3.5-lightning-30b-a3b-nvfp4-tt-p300x2-vllm026
|
| 11 |
+
|
| 12 |
+
UNPUBLISHED TEST VARIANT on stock vLLM 0.26.0 + the standalone vllm-tt-plugin, with NVIDIA's recommended nemotron_v3 reasoning parser and qwen3_coder tool parser. NVIDIA Nemotron 3.5 Lightning 30B-A3B (NVFP4 checkpoint): a hybrid Mamba-Transformer mixture-of-experts reasoning model with a 1,048,576-token context, ported to Tenstorrent Blackhole and served through vLLM on a QuietBox2 (2x p300c, 4 chips).
|
| 13 |
+
|
| 14 |
+
Runs on **p300x2** (mesh `P300x2`) — 1,048,576-token context, up to 32 concurrent sequences.
|
| 15 |
+
|
| 16 |
+
Packaged and published with [tt-model-manager](https://github.com/tenstorrent/tt-model-manager) 0.1.0 (manifest schema 5.1).
|
| 17 |
+
|
| 18 |
+
## Quickstart
|
| 19 |
+
|
| 20 |
+
```bash
|
| 21 |
+
tt-model pull ndaly/Nemotron-3.5-Lightning-30B-A3B-NVFP4-tt-p300x2-vllm026 --with-weights
|
| 22 |
+
tt-model serve ndaly/Nemotron-3.5-Lightning-30B-A3B-NVFP4-tt-p300x2-vllm026
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
`pull --with-weights` downloads the Docker image and the [`nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4`](https://huggingface.co/nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4) weights at `6dbbd757ea75a8ece6e0702872e3ae53f9987728` (into your HF cache; they are not in the image). `serve` starts an OpenAI-compatible server on port 20000 (or the next free port, if that one is busy); the first start compiles kernels for your device, which takes several minutes, and the server is ready when it logs `Application startup complete`.
|
| 26 |
+
|
| 27 |
+
### What was validated
|
| 28 |
+
Full 1M-token context on a QuietBox2 (P300x2 mesh, 4 Blackhole chips). Release validation
|
| 29 |
+
(2026-08-19, kernel 7.0.0-28): TPOT 15.4 ms (~65 tok/s/user) single-stream, TTFT 109 ms,
|
| 30 |
+
22/22 spec-test conformance. This container was re-verified on the same box on 2026-09-09
|
| 31 |
+
(kernel 7.0.0-30): identity, deterministic generation and the separate `reasoning` field
|
| 32 |
+
all pass; TTFT 110 ms; single-stream TPOT measured 42 ms (~24 tok/s/user) — identical to a
|
| 33 |
+
host-native run of the bring-up environment on that day, so the gap is the host, not the
|
| 34 |
+
container. Reasoning is returned in a separate `reasoning` field (`deepseek_r1` parser); with
|
| 35 |
+
`chat_template_kwargs: {"enable_thinking": false}` the parser has no `</think>` to split on
|
| 36 |
+
and the answer lands in `reasoning` with empty `content`.
|
| 37 |
+
|
| 38 |
+
### Try it
|
| 39 |
+
```bash
|
| 40 |
+
curl -s localhost:20000/v1/chat/completions -H 'Content-Type: application/json' -d '{
|
| 41 |
+
"model": "nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4",
|
| 42 |
+
"messages": [{"role": "user", "content": "Merge two sorted lists in Python."}],
|
| 43 |
+
"max_tokens": 2048, "temperature": 0}'
|
| 44 |
+
```
|
| 45 |
+
The first boot converts the NVFP4 weights to device layout and JIT-compiles kernels (~12 min
|
| 46 |
+
on a QuietBox2); both are cached under `~/.cache/tt-model/<name>/` so later boots take ~4 min.
|
| 47 |
+
After any non-clean stop, run `tt-smi -r all` before serving again.
|
| 48 |
+
|
| 49 |
+
## Provenance
|
| 50 |
+
|
| 51 |
+
The exact sources the image was built from — `code/` in this repo is byte-identical to the model code inside the image:
|
| 52 |
+
|
| 53 |
+
| component | built from |
|
| 54 |
+
| --- | --- |
|
| 55 |
+
| tt-metal | a local checkout — commit not published *(dirty tree — the image includes uncommitted changes)* |
|
| 56 |
+
| vLLM | [`v0.26.0`](https://github.com/vllm-project/vllm/releases/tag/v0.26.0) |
|
| 57 |
+
| vllm-tt-plugin | a local checkout — commit not published |
|
| 58 |
+
| `code/` digest | `5787b8dfce9d7e38` (sha256, first 16 hex digits) |
|
| 59 |
+
| built | 2026-09-11T17:47:54+00:00 by tt-model 0.1.0 |
|
| 60 |
+
|
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/demos/llama31_8B_demo.py
ADDED
|
@@ -0,0 +1,823 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
TTTv2 MLP1D Demo Script for Llama 3.1-8B-Instruct
|
| 6 |
+
|
| 7 |
+
Demonstrates the TTTv2 MLP1D module by replacing the MLP layers in Llama 3.1-8B-Instruct model.
|
| 8 |
+
|
| 9 |
+
Measurements:
|
| 10 |
+
- Top-1/Top-5 token accuracy (like `ci-token-matching`)
|
| 11 |
+
- tok/s/u and TTFT performance (like `batch-1` and `batch-32`)
|
| 12 |
+
|
| 13 |
+
Supports:
|
| 14 |
+
- `performance` and `accuracy` optimization modes
|
| 15 |
+
- N150 (1x1), N300 (1x2), T3K (1x8) topologies
|
| 16 |
+
|
| 17 |
+
Usage:
|
| 18 |
+
# Token accuracy test on N150
|
| 19 |
+
MESH_DEVICE=N150 HF_MODEL=meta-llama/Llama-3.1-8B-Instruct \
|
| 20 |
+
python_env/bin/pytest models/common/demos/llama31_8B_demo.py -k "token-accuracy" -v
|
| 21 |
+
|
| 22 |
+
# Batch-1 latency test on N300
|
| 23 |
+
MESH_DEVICE=N300 HF_MODEL=meta-llama/Llama-3.1-8B-Instruct \
|
| 24 |
+
python_env/bin/pytest models/common/demos/llama31_8B_demo.py -k "batch-1" --performance -v
|
| 25 |
+
|
| 26 |
+
# Batch-32 throughput test on T3K
|
| 27 |
+
MESH_DEVICE=T3K HF_MODEL=meta-llama/Llama-3.1-8B-Instruct \
|
| 28 |
+
python_env/bin/pytest models/common/demos/llama31_8B_demo.py -k "batch-32" --performance -v
|
| 29 |
+
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
import json
|
| 33 |
+
import os
|
| 34 |
+
import re
|
| 35 |
+
import subprocess
|
| 36 |
+
import sys
|
| 37 |
+
from contextlib import contextmanager
|
| 38 |
+
|
| 39 |
+
import pytest
|
| 40 |
+
import torch
|
| 41 |
+
from loguru import logger
|
| 42 |
+
|
| 43 |
+
import ttnn
|
| 44 |
+
from models.common.sampling import SamplingParams
|
| 45 |
+
from models.perf.benchmarking_utils import BenchmarkProfiler
|
| 46 |
+
from models.tt_transformers.tt.common import (
|
| 47 |
+
PagedAttentionConfig,
|
| 48 |
+
create_tt_model,
|
| 49 |
+
preprocess_inputs_prefill,
|
| 50 |
+
sample_host,
|
| 51 |
+
)
|
| 52 |
+
from models.tt_transformers.tt.generator import Generator
|
| 53 |
+
from models.tt_transformers.tt.model_config import DecodersPrecision
|
| 54 |
+
|
| 55 |
+
# =============================================================================
|
| 56 |
+
# Constants and Expected Metrics
|
| 57 |
+
# =============================================================================
|
| 58 |
+
|
| 59 |
+
# Expected accuracy metrics from centralized targets for Llama-3.1-8B (top1, top5 only)
|
| 60 |
+
# Performance metrics (tok_s_u, ttft_ms) are collected dynamically by running
|
| 61 |
+
# simple_text_demo.py with the corresponding test case to get real baseline values.
|
| 62 |
+
# Higher is better for tok_s_u, top1, top5
|
| 63 |
+
# Lower is better for ttft_ms
|
| 64 |
+
EXPECTED_METRICS = {
|
| 65 |
+
"performance": {
|
| 66 |
+
"N150": {"top1": 90, "top5": 97, "tok_s_u": 28.3, "ttft_ms": 104},
|
| 67 |
+
"N300": {"top1": 90, "top5": 97, "tok_s_u": 44.2, "ttft_ms": 67},
|
| 68 |
+
"T3K": {"top1": 90, "top5": 98, "tok_s_u": 64.3, "ttft_ms": 53},
|
| 69 |
+
},
|
| 70 |
+
"accuracy": {
|
| 71 |
+
"N150": {"top1": 96, "top5": 100, "tok_s_u": 25.2, "ttft_ms": 138},
|
| 72 |
+
"N300": {"top1": 96, "top5": 100, "tok_s_u": 38.8, "ttft_ms": 79},
|
| 73 |
+
"T3K": {"top1": 97, "top5": 100, "tok_s_u": 60.8, "ttft_ms": 81},
|
| 74 |
+
},
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
# Tolerance for performance validation (5%)
|
| 78 |
+
PERF_TOLERANCE = 0.05
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# =============================================================================
|
| 82 |
+
# Baseline Collection from simple_text_demo.py
|
| 83 |
+
# =============================================================================
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def collect_baseline_from_simple_text_demo(
|
| 87 |
+
device_name: str,
|
| 88 |
+
batch_size: int,
|
| 89 |
+
opt_mode: str,
|
| 90 |
+
) -> dict | None:
|
| 91 |
+
"""
|
| 92 |
+
Run simple_text_demo.py to collect real baseline performance metrics (tok_s_u, ttft_ms).
|
| 93 |
+
|
| 94 |
+
This provides an accurate comparison by measuring TTTv1 MLP performance
|
| 95 |
+
on the same hardware under the same conditions, rather than relying on
|
| 96 |
+
potentially outdated markdown reference values.
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
device_name: Device name (N150, N300, T3K)
|
| 100 |
+
batch_size: Batch size (1 or 32)
|
| 101 |
+
opt_mode: Optimization mode (performance or accuracy)
|
| 102 |
+
|
| 103 |
+
Returns:
|
| 104 |
+
Dict with baseline metrics {tok_s_u, ttft_ms} or None if collection fails
|
| 105 |
+
"""
|
| 106 |
+
# Build the test ID for simple_text_demo
|
| 107 |
+
batch_id = f"batch-{batch_size}"
|
| 108 |
+
test_filter = f"{opt_mode}-{batch_id}"
|
| 109 |
+
|
| 110 |
+
# Get HF_MODEL from environment
|
| 111 |
+
hf_model = os.environ.get("HF_MODEL", "meta-llama/Llama-3.1-8B-Instruct")
|
| 112 |
+
|
| 113 |
+
logger.info(f"Collecting baseline from simple_text_demo.py ({test_filter})...")
|
| 114 |
+
|
| 115 |
+
# Build pytest command (use sys.executable for CI compatibility)
|
| 116 |
+
cmd = [
|
| 117 |
+
sys.executable,
|
| 118 |
+
"-m",
|
| 119 |
+
"pytest",
|
| 120 |
+
"models/tt_transformers/demo/simple_text_demo.py",
|
| 121 |
+
"-k",
|
| 122 |
+
test_filter,
|
| 123 |
+
"-v",
|
| 124 |
+
"--tb=short",
|
| 125 |
+
]
|
| 126 |
+
|
| 127 |
+
env = os.environ.copy()
|
| 128 |
+
env["MESH_DEVICE"] = device_name
|
| 129 |
+
env["HF_MODEL"] = hf_model
|
| 130 |
+
# Unset CI to ensure tests run normally (CI=true may skip or alter test behavior)
|
| 131 |
+
env.pop("CI", None)
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
result = subprocess.run(
|
| 135 |
+
cmd,
|
| 136 |
+
capture_output=True,
|
| 137 |
+
text=True,
|
| 138 |
+
timeout=600, # 10 minute timeout
|
| 139 |
+
env=env,
|
| 140 |
+
cwd=os.getcwd(),
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
output = result.stdout + result.stderr
|
| 144 |
+
|
| 145 |
+
# Parse the output for performance metrics
|
| 146 |
+
# Looking for lines like:
|
| 147 |
+
# "Average Time to First Token (TTFT): 68.09ms"
|
| 148 |
+
# "Average speed: 57.89ms @ 17.28 tok/s/user (17.28 tok/s throughput)"
|
| 149 |
+
ttft_match = re.search(r"Average Time to First Token \(TTFT\):\s*([\d.]+)ms", output)
|
| 150 |
+
speed_match = re.search(r"Average speed:.*@\s*([\d.]+)\s*tok/s/user", output)
|
| 151 |
+
|
| 152 |
+
if ttft_match and speed_match:
|
| 153 |
+
baseline = {
|
| 154 |
+
"ttft_ms": float(ttft_match.group(1)),
|
| 155 |
+
"tok_s_u": float(speed_match.group(1)),
|
| 156 |
+
}
|
| 157 |
+
logger.info(f"Baseline collected: {baseline['tok_s_u']:.2f} tok/s/u, TTFT {baseline['ttft_ms']:.2f}ms")
|
| 158 |
+
return baseline
|
| 159 |
+
else:
|
| 160 |
+
logger.warning(f"Could not parse baseline metrics from simple_text_demo output")
|
| 161 |
+
if "PASSED" not in output and "passed" not in output:
|
| 162 |
+
logger.warning(f"Test may have failed. Exit code: {result.returncode}")
|
| 163 |
+
return None
|
| 164 |
+
|
| 165 |
+
except subprocess.TimeoutExpired:
|
| 166 |
+
logger.warning("Baseline collection timed out after 10 minutes")
|
| 167 |
+
return None
|
| 168 |
+
except Exception as e:
|
| 169 |
+
logger.warning(f"Baseline collection failed: {e}")
|
| 170 |
+
return None
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
# =============================================================================
|
| 174 |
+
# Token Accuracy Helper
|
| 175 |
+
# =============================================================================
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
class TokenAccuracy:
|
| 179 |
+
"""Helper class for measuring token accuracy against reference data."""
|
| 180 |
+
|
| 181 |
+
def __init__(self, model_name: str):
|
| 182 |
+
self.gt_pos = -1
|
| 183 |
+
self.store_predicted_tokens = []
|
| 184 |
+
reference_data_file = os.path.join("models/tt_transformers/tests/reference_outputs/", model_name) + ".refpt"
|
| 185 |
+
if not os.path.exists(reference_data_file):
|
| 186 |
+
raise FileNotFoundError(f"Reference data file not found: {reference_data_file}")
|
| 187 |
+
|
| 188 |
+
logger.info(f"Loading reference data from {reference_data_file}")
|
| 189 |
+
reference_data = torch.load(reference_data_file)
|
| 190 |
+
reference_tokens = reference_data["reference_tokens"]
|
| 191 |
+
split_point = reference_tokens.shape[-1] // 2
|
| 192 |
+
self.input_prompt = reference_tokens[0, :split_point]
|
| 193 |
+
self.reference_tokens = reference_tokens[0, split_point:]
|
| 194 |
+
self.top5_tokens = reference_data["top5_tokens"][split_point - 1 :, :]
|
| 195 |
+
self.maxindex = len(self.reference_tokens) - 1
|
| 196 |
+
|
| 197 |
+
def prepare_ref_tokens(self, tokenizer):
|
| 198 |
+
"""Decode input prompt for reference."""
|
| 199 |
+
return tokenizer.decode(self.input_prompt.tolist())
|
| 200 |
+
|
| 201 |
+
def collect_predicted_tokens(self, tokens):
|
| 202 |
+
"""Collect predicted tokens and return the next reference token (teacher forcing)."""
|
| 203 |
+
self.store_predicted_tokens.append(tokens)
|
| 204 |
+
self.gt_pos += 1
|
| 205 |
+
return self.reference_tokens[min(self.gt_pos, self.maxindex)].unsqueeze(-1).unsqueeze(-1)
|
| 206 |
+
|
| 207 |
+
def compute_accuracy(self) -> tuple[float, float]:
|
| 208 |
+
"""Compute Top-1 and Top-5 accuracy."""
|
| 209 |
+
count = 0
|
| 210 |
+
count_t5 = 0
|
| 211 |
+
matching_sz = min(len(self.reference_tokens), len(self.store_predicted_tokens))
|
| 212 |
+
for i in range(matching_sz):
|
| 213 |
+
if self.top5_tokens[i, 0].item() == self.store_predicted_tokens[i]:
|
| 214 |
+
count += 1
|
| 215 |
+
if self.store_predicted_tokens[i] in self.top5_tokens[i, :]:
|
| 216 |
+
count_t5 += 1
|
| 217 |
+
accuracy_top1 = count / matching_sz
|
| 218 |
+
accuracy_top5 = count_t5 / matching_sz
|
| 219 |
+
|
| 220 |
+
return accuracy_top1 * 100, accuracy_top5 * 100
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
# =============================================================================
|
| 224 |
+
# MLP1D Patching Context Manager
|
| 225 |
+
# =============================================================================
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
@contextmanager
|
| 229 |
+
def patch_mlp_with_mlp1d():
|
| 230 |
+
"""
|
| 231 |
+
Context manager that patches decoder.MLP with MLP1D.from_model_args.
|
| 232 |
+
|
| 233 |
+
The signatures now match, enabling drop-in replacement without adapters.
|
| 234 |
+
"""
|
| 235 |
+
from models.common.modules.mlp.mlp_1d import MLP1D
|
| 236 |
+
from models.tt_transformers.tt import decoder
|
| 237 |
+
|
| 238 |
+
# Store original MLP class
|
| 239 |
+
original_MLP = decoder.MLP
|
| 240 |
+
|
| 241 |
+
# Replace with MLP1D.from_model_args - signatures match!
|
| 242 |
+
decoder.MLP = MLP1D.from_model_args
|
| 243 |
+
|
| 244 |
+
try:
|
| 245 |
+
yield
|
| 246 |
+
finally:
|
| 247 |
+
# Restore original MLP class
|
| 248 |
+
decoder.MLP = original_MLP
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def create_tt_model_with_mlp1d(
|
| 252 |
+
mesh_device,
|
| 253 |
+
instruct: bool,
|
| 254 |
+
max_batch_size: int,
|
| 255 |
+
optimizations,
|
| 256 |
+
max_seq_len: int,
|
| 257 |
+
paged_attention_config,
|
| 258 |
+
dtype,
|
| 259 |
+
state_dict=None,
|
| 260 |
+
num_layers=None,
|
| 261 |
+
):
|
| 262 |
+
"""
|
| 263 |
+
Wrapper around create_tt_model that patches MLP → MLP1D.from_model_args.
|
| 264 |
+
|
| 265 |
+
With matching signatures, this is now a simple class swap without adapters.
|
| 266 |
+
"""
|
| 267 |
+
with patch_mlp_with_mlp1d():
|
| 268 |
+
model_args, model, tt_kv_cache, state_dict = create_tt_model(
|
| 269 |
+
mesh_device,
|
| 270 |
+
instruct=instruct,
|
| 271 |
+
max_batch_size=max_batch_size,
|
| 272 |
+
optimizations=optimizations,
|
| 273 |
+
max_seq_len=max_seq_len,
|
| 274 |
+
paged_attention_config=paged_attention_config,
|
| 275 |
+
dtype=dtype,
|
| 276 |
+
state_dict=state_dict,
|
| 277 |
+
num_layers=num_layers,
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
return model_args, model, tt_kv_cache, state_dict
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
# =============================================================================
|
| 284 |
+
# Performance Validation
|
| 285 |
+
# =============================================================================
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
def validate_metrics(measured: dict, expected: dict, tolerance: float = PERF_TOLERANCE) -> tuple[bool, str]:
|
| 289 |
+
"""
|
| 290 |
+
Validate measured metrics against expected values with tolerance.
|
| 291 |
+
|
| 292 |
+
For performance tests: expected comes from simple_text_demo.py baseline (tok_s_u, ttft_ms).
|
| 293 |
+
For accuracy tests: expected comes from centralized model targets (top1, top5).
|
| 294 |
+
|
| 295 |
+
Returns:
|
| 296 |
+
(passed, message) tuple
|
| 297 |
+
"""
|
| 298 |
+
failures = []
|
| 299 |
+
for metric, expected_val in expected.items():
|
| 300 |
+
if expected_val is None:
|
| 301 |
+
continue
|
| 302 |
+
measured_val = measured.get(metric)
|
| 303 |
+
if measured_val is None:
|
| 304 |
+
continue
|
| 305 |
+
|
| 306 |
+
# Higher is better for tok_s_u, top1, top5
|
| 307 |
+
# Lower is better for ttft_ms
|
| 308 |
+
if metric == "ttft_ms":
|
| 309 |
+
if measured_val > expected_val * (1 + tolerance):
|
| 310 |
+
failures.append(
|
| 311 |
+
f"{metric}: measured {measured_val:.2f} > expected {expected_val * (1 + tolerance):.2f}"
|
| 312 |
+
)
|
| 313 |
+
else:
|
| 314 |
+
if measured_val < expected_val * (1 - tolerance):
|
| 315 |
+
failures.append(
|
| 316 |
+
f"{metric}: measured {measured_val:.2f} < expected {expected_val * (1 - tolerance):.2f}"
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
if failures:
|
| 320 |
+
return False, "; ".join(failures)
|
| 321 |
+
return True, "All metrics within tolerance"
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def get_device_name_from_mesh_shape(mesh_shape: tuple[int, int]) -> str:
|
| 325 |
+
"""Map mesh_shape to device name."""
|
| 326 |
+
mapping = {
|
| 327 |
+
(1, 1): "N150",
|
| 328 |
+
(1, 2): "N300",
|
| 329 |
+
(1, 8): "T3K",
|
| 330 |
+
}
|
| 331 |
+
return mapping.get(mesh_shape, f"Unknown({mesh_shape})")
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
# =============================================================================
|
| 335 |
+
# Test Parametrization
|
| 336 |
+
# =============================================================================
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
def _get_mesh_shape():
|
| 340 |
+
"""Get mesh shape from MESH_DEVICE environment variable."""
|
| 341 |
+
shape_map = {
|
| 342 |
+
"N150": (1, 1),
|
| 343 |
+
"N300": (1, 2),
|
| 344 |
+
"T3K": (1, 8),
|
| 345 |
+
}
|
| 346 |
+
return shape_map.get(os.environ.get("MESH_DEVICE"), len(ttnn.get_device_ids()))
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
# Test configurations
|
| 350 |
+
TEST_CONFIGS = [
|
| 351 |
+
# Token accuracy test (like ci-token-matching)
|
| 352 |
+
pytest.param(
|
| 353 |
+
{
|
| 354 |
+
"name": "token-accuracy",
|
| 355 |
+
"batch_size": 1,
|
| 356 |
+
"max_generated_tokens": 500,
|
| 357 |
+
"measure_accuracy": True,
|
| 358 |
+
},
|
| 359 |
+
id="token-accuracy",
|
| 360 |
+
),
|
| 361 |
+
# Latency test (like batch-1)
|
| 362 |
+
pytest.param(
|
| 363 |
+
{
|
| 364 |
+
"name": "batch-1-latency",
|
| 365 |
+
"batch_size": 1,
|
| 366 |
+
"max_generated_tokens": 200,
|
| 367 |
+
"measure_accuracy": False,
|
| 368 |
+
},
|
| 369 |
+
id="batch-1",
|
| 370 |
+
),
|
| 371 |
+
# Throughput test (like batch-32)
|
| 372 |
+
pytest.param(
|
| 373 |
+
{
|
| 374 |
+
"name": "batch-32-throughput",
|
| 375 |
+
"batch_size": 32,
|
| 376 |
+
"max_generated_tokens": 200,
|
| 377 |
+
"measure_accuracy": False,
|
| 378 |
+
},
|
| 379 |
+
id="batch-32",
|
| 380 |
+
),
|
| 381 |
+
]
|
| 382 |
+
|
| 383 |
+
# Global cache for baseline metrics (collected before any test opens mesh_device)
|
| 384 |
+
_baseline_cache: dict[str, dict] = {}
|
| 385 |
+
_baseline_collected: bool = False
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def _get_baseline_cache_key(device_name: str, batch_size: int, opt_mode: str) -> str:
|
| 389 |
+
"""Generate cache key for baseline metrics."""
|
| 390 |
+
return f"{device_name}-batch{batch_size}-{opt_mode}"
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
def _collect_all_baselines():
|
| 394 |
+
"""
|
| 395 |
+
Collect all baseline metrics ONCE at module load time, before any tests run.
|
| 396 |
+
|
| 397 |
+
This ensures baselines are collected when no mesh_device is open.
|
| 398 |
+
"""
|
| 399 |
+
global _baseline_collected
|
| 400 |
+
if _baseline_collected:
|
| 401 |
+
return
|
| 402 |
+
|
| 403 |
+
device_name = os.environ.get("MESH_DEVICE")
|
| 404 |
+
assert device_name is not None, "MESH_DEVICE environment variable is not set"
|
| 405 |
+
logger.info(f"=== Collecting baselines from simple_text_demo.py for {device_name} ===")
|
| 406 |
+
|
| 407 |
+
# Collect baselines for batch_size x opt_mode combinations
|
| 408 |
+
# CI runs batch-32, so skip batch-1 when CI=true
|
| 409 |
+
is_ci = os.environ.get("CI", "").lower() in ("true", "1", "yes")
|
| 410 |
+
batch_sizes = [32] if is_ci else [1, 32]
|
| 411 |
+
|
| 412 |
+
for batch_size in batch_sizes:
|
| 413 |
+
for opt_mode in ["performance", "accuracy"]:
|
| 414 |
+
cache_key = _get_baseline_cache_key(device_name, batch_size, opt_mode)
|
| 415 |
+
logger.info(f"Collecting baseline for {cache_key}...")
|
| 416 |
+
baseline = collect_baseline_from_simple_text_demo(
|
| 417 |
+
device_name=device_name,
|
| 418 |
+
batch_size=batch_size,
|
| 419 |
+
opt_mode=opt_mode,
|
| 420 |
+
)
|
| 421 |
+
if baseline:
|
| 422 |
+
_baseline_cache[cache_key] = baseline
|
| 423 |
+
logger.info(f" ✓ {cache_key}: {baseline['tok_s_u']:.2f} tok/s/u, {baseline['ttft_ms']:.2f}ms TTFT")
|
| 424 |
+
else:
|
| 425 |
+
logger.warning(f" ✗ {cache_key}: baseline collection failed")
|
| 426 |
+
|
| 427 |
+
_baseline_collected = True
|
| 428 |
+
logger.info("=== Baseline collection complete ===")
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
def _get_cached_baseline(device_name: str, batch_size: int, opt_mode: str) -> dict | None:
|
| 432 |
+
"""Get cached baseline metrics. Must be called after baselines are collected."""
|
| 433 |
+
cache_key = _get_baseline_cache_key(device_name, batch_size, opt_mode)
|
| 434 |
+
return _baseline_cache.get(cache_key)
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
# Collect all baselines at module import time, before pytest sets up any fixtures.
|
| 438 |
+
# This ensures baselines are collected when no mesh_device is open.
|
| 439 |
+
_collect_all_baselines()
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
# =============================================================================
|
| 443 |
+
# Main Test Function
|
| 444 |
+
# =============================================================================
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
@pytest.mark.parametrize(
|
| 448 |
+
"device_params",
|
| 449 |
+
[{"fabric_config": ttnn.FabricConfig.FABRIC_1D, "trace_region_size": 50000000, "num_command_queues": 1}],
|
| 450 |
+
indirect=True,
|
| 451 |
+
)
|
| 452 |
+
@pytest.mark.parametrize(
|
| 453 |
+
"mesh_device",
|
| 454 |
+
[_get_mesh_shape()],
|
| 455 |
+
indirect=True,
|
| 456 |
+
)
|
| 457 |
+
@pytest.mark.parametrize(
|
| 458 |
+
"optimizations",
|
| 459 |
+
[
|
| 460 |
+
pytest.param(
|
| 461 |
+
lambda args: DecodersPrecision.performance(args.n_layers, args.model_name),
|
| 462 |
+
id="performance",
|
| 463 |
+
),
|
| 464 |
+
pytest.param(
|
| 465 |
+
lambda args: DecodersPrecision.accuracy(args.n_layers, args.model_name),
|
| 466 |
+
id="accuracy",
|
| 467 |
+
),
|
| 468 |
+
],
|
| 469 |
+
)
|
| 470 |
+
@pytest.mark.parametrize("test_config", TEST_CONFIGS)
|
| 471 |
+
def test_mlp1d_llama_demo(
|
| 472 |
+
mesh_device: ttnn.MeshDevice,
|
| 473 |
+
optimizations,
|
| 474 |
+
test_config: dict,
|
| 475 |
+
is_ci_env: bool,
|
| 476 |
+
request,
|
| 477 |
+
):
|
| 478 |
+
"""
|
| 479 |
+
Demo test for MLP1D replacement in Llama 3.1-8B-Instruct.
|
| 480 |
+
|
| 481 |
+
This test:
|
| 482 |
+
1. Creates the TT model with MLP layers replaced by MLP1D
|
| 483 |
+
2. Runs prefill and decode inference
|
| 484 |
+
3. Measures performance (TTFT, tok/s/u) and/or accuracy (Top-1, Top-5)
|
| 485 |
+
4. Validates results against centralized targets
|
| 486 |
+
"""
|
| 487 |
+
test_id = request.node.callspec.id
|
| 488 |
+
|
| 489 |
+
if is_ci_env and "batch-1" in test_id:
|
| 490 |
+
pytest.skip("CI only runs batch-32 and token-accuracy tests")
|
| 491 |
+
|
| 492 |
+
# Skip TG devices - MLP1D does not support Galaxy
|
| 493 |
+
mesh_shape = mesh_device.shape
|
| 494 |
+
if mesh_shape[0] > 1 or mesh_shape[1] > 8:
|
| 495 |
+
pytest.skip("MLP1D does not support TG/Galaxy devices")
|
| 496 |
+
|
| 497 |
+
# Get test parameters
|
| 498 |
+
test_name = test_config["name"]
|
| 499 |
+
batch_size = test_config["batch_size"]
|
| 500 |
+
max_generated_tokens = test_config["max_generated_tokens"]
|
| 501 |
+
measure_accuracy = test_config["measure_accuracy"]
|
| 502 |
+
|
| 503 |
+
# Get optimization mode from test ID
|
| 504 |
+
opt_mode = "performance" if "performance" in test_id else "accuracy"
|
| 505 |
+
|
| 506 |
+
# Get device name
|
| 507 |
+
device_name = get_device_name_from_mesh_shape(tuple(mesh_shape))
|
| 508 |
+
|
| 509 |
+
logger.info(f"=== MLP1D Demo: {test_name} ===")
|
| 510 |
+
logger.info(f"Device: {device_name}, Optimization: {opt_mode}, Batch: {batch_size}")
|
| 511 |
+
|
| 512 |
+
# Configuration
|
| 513 |
+
instruct = not measure_accuracy # Use non-instruct for token accuracy (matches ci-token-matching)
|
| 514 |
+
max_seq_len = 1024
|
| 515 |
+
paged_attention = True
|
| 516 |
+
page_params = {"page_block_size": 32, "page_max_num_blocks_per_dp": 1024}
|
| 517 |
+
dtype = ttnn.bfloat8_b
|
| 518 |
+
|
| 519 |
+
# Paged attention config
|
| 520 |
+
paged_attention_config = (
|
| 521 |
+
PagedAttentionConfig(
|
| 522 |
+
block_size=page_params["page_block_size"],
|
| 523 |
+
max_num_blocks=page_params["page_max_num_blocks_per_dp"],
|
| 524 |
+
)
|
| 525 |
+
if paged_attention
|
| 526 |
+
else None
|
| 527 |
+
)
|
| 528 |
+
|
| 529 |
+
# Create model with MLP1D replacement
|
| 530 |
+
logger.info("Creating TT model with MLP1D replacement...")
|
| 531 |
+
model_args, model, tt_kv_cache, _ = create_tt_model_with_mlp1d(
|
| 532 |
+
mesh_device=mesh_device,
|
| 533 |
+
instruct=instruct,
|
| 534 |
+
max_batch_size=batch_size,
|
| 535 |
+
optimizations=optimizations,
|
| 536 |
+
max_seq_len=max_seq_len,
|
| 537 |
+
paged_attention_config=paged_attention_config,
|
| 538 |
+
dtype=dtype,
|
| 539 |
+
)
|
| 540 |
+
|
| 541 |
+
# Validate model was created successfully
|
| 542 |
+
assert model is not None, "Failed to create model"
|
| 543 |
+
assert model_args is not None, "Failed to get model args"
|
| 544 |
+
|
| 545 |
+
# Wrap in lists for API compatibility (like prepare_generator_args does for data parallel)
|
| 546 |
+
model_args_list = [model_args]
|
| 547 |
+
model_list = [model]
|
| 548 |
+
tt_kv_cache_list = [tt_kv_cache]
|
| 549 |
+
|
| 550 |
+
tokenizer = model_args.tokenizer
|
| 551 |
+
|
| 552 |
+
# Initialize profiler
|
| 553 |
+
profiler = BenchmarkProfiler()
|
| 554 |
+
profiler.start("run")
|
| 555 |
+
|
| 556 |
+
# Setup token accuracy if needed
|
| 557 |
+
token_acc = None
|
| 558 |
+
if measure_accuracy:
|
| 559 |
+
try:
|
| 560 |
+
token_acc = TokenAccuracy(model_name=model_args.model_name)
|
| 561 |
+
except FileNotFoundError as e:
|
| 562 |
+
pytest.skip(f"Reference data not found: {e}")
|
| 563 |
+
|
| 564 |
+
# Prepare input prompts
|
| 565 |
+
if measure_accuracy and token_acc:
|
| 566 |
+
input_prompts = [token_acc.prepare_ref_tokens(tokenizer)]
|
| 567 |
+
else:
|
| 568 |
+
# Use the same 128-token prompts as simple_text_demo.py for fair perf comparison
|
| 569 |
+
prompt_file = "models/tt_transformers/demo/sample_prompts/input_data_questions_prefill_128.json"
|
| 570 |
+
with open(prompt_file, "r") as f:
|
| 571 |
+
prompts_data = json.load(f)
|
| 572 |
+
# Repeat/truncate prompts to match batch_size
|
| 573 |
+
input_prompts = [prompts_data[i % len(prompts_data)]["prompt"] for i in range(batch_size)]
|
| 574 |
+
|
| 575 |
+
# Create page table for paged attention
|
| 576 |
+
page_table = None
|
| 577 |
+
if paged_attention_config:
|
| 578 |
+
permutation = torch.randperm(paged_attention_config.max_num_blocks)
|
| 579 |
+
reverse_permutation = torch.argsort(permutation)
|
| 580 |
+
page_table = reverse_permutation.reshape(batch_size, paged_attention_config.max_num_blocks // batch_size)
|
| 581 |
+
|
| 582 |
+
# Preprocess inputs
|
| 583 |
+
logger.info("Preprocessing inputs...")
|
| 584 |
+
(
|
| 585 |
+
input_tokens_prefill_pt,
|
| 586 |
+
encoded_prompts,
|
| 587 |
+
decoding_pos,
|
| 588 |
+
prefill_lens,
|
| 589 |
+
) = preprocess_inputs_prefill(
|
| 590 |
+
input_prompts, tokenizer, model_args_list, instruct, max_generated_tokens, max_prefill_len=max_seq_len
|
| 591 |
+
)
|
| 592 |
+
input_tokens_prefill_pt = torch.stack(input_tokens_prefill_pt).view(batch_size, -1)
|
| 593 |
+
|
| 594 |
+
# Create generator
|
| 595 |
+
generator = Generator(model_list, model_args_list, mesh_device, tokenizer=tokenizer)
|
| 596 |
+
|
| 597 |
+
# --- Prefill Phase ---
|
| 598 |
+
logger.info("Starting prefill warmup...")
|
| 599 |
+
profiler.start("compile_prefill")
|
| 600 |
+
logits = generator.prefill_forward_text(
|
| 601 |
+
input_tokens_prefill_pt,
|
| 602 |
+
page_table=page_table,
|
| 603 |
+
kv_cache=tt_kv_cache_list,
|
| 604 |
+
prompt_lens=decoding_pos,
|
| 605 |
+
)
|
| 606 |
+
profiler.end("compile_prefill")
|
| 607 |
+
|
| 608 |
+
logger.info("Starting prefill inference...")
|
| 609 |
+
profiler.start("inference_prefill")
|
| 610 |
+
logits = generator.prefill_forward_text(
|
| 611 |
+
input_tokens_prefill_pt,
|
| 612 |
+
page_table=page_table,
|
| 613 |
+
kv_cache=tt_kv_cache_list,
|
| 614 |
+
prompt_lens=decoding_pos,
|
| 615 |
+
)
|
| 616 |
+
prefilled_token = torch.argmax(logits, dim=-1)
|
| 617 |
+
profiler.end("inference_prefill")
|
| 618 |
+
|
| 619 |
+
# --- Decode Phase ---
|
| 620 |
+
# Keep track of generated outputs to print out every iteration
|
| 621 |
+
all_outputs = [encoded_prompts[b][: prefill_lens[b]] for b in range(batch_size)]
|
| 622 |
+
for user in range(batch_size):
|
| 623 |
+
user_tok = int(prefilled_token[user].item())
|
| 624 |
+
all_outputs[user].append(user_tok)
|
| 625 |
+
|
| 626 |
+
logger.info("Starting decode loop...")
|
| 627 |
+
current_pos = torch.tensor([decoding_pos[b] for b in range(batch_size)])
|
| 628 |
+
out_tok = prefilled_token
|
| 629 |
+
user_done = [False] * batch_size
|
| 630 |
+
|
| 631 |
+
# Sampling params (argmax for deterministic results)
|
| 632 |
+
sampling_params = {"temperature": 0, "top_p": 0.08, "top_k": 32}
|
| 633 |
+
device_sampling_params = (
|
| 634 |
+
SamplingParams(
|
| 635 |
+
temperature=sampling_params["temperature"],
|
| 636 |
+
top_k=sampling_params["top_k"],
|
| 637 |
+
top_p=sampling_params["top_p"],
|
| 638 |
+
)
|
| 639 |
+
if model_list[0]._supports_on_device_sampling
|
| 640 |
+
else None
|
| 641 |
+
)
|
| 642 |
+
|
| 643 |
+
profiler.start("inference_decode")
|
| 644 |
+
|
| 645 |
+
for iteration in range(max_generated_tokens):
|
| 646 |
+
if iteration == 0:
|
| 647 |
+
profiler.start("compile_decode")
|
| 648 |
+
else:
|
| 649 |
+
profiler.start(f"inference_decode_time_{iteration}")
|
| 650 |
+
|
| 651 |
+
# Teacher forcing for token accuracy
|
| 652 |
+
if token_acc:
|
| 653 |
+
out_tok[0] = token_acc.collect_predicted_tokens(out_tok[0].item())
|
| 654 |
+
|
| 655 |
+
# Decode forward
|
| 656 |
+
logits, _ = generator.decode_forward(
|
| 657 |
+
out_tok,
|
| 658 |
+
current_pos,
|
| 659 |
+
enable_trace=not measure_accuracy, # Disable trace for accuracy (teacher forcing)
|
| 660 |
+
page_table=page_table,
|
| 661 |
+
kv_cache=tt_kv_cache_list,
|
| 662 |
+
sampling_params=device_sampling_params,
|
| 663 |
+
prompt_tokens=input_tokens_prefill_pt,
|
| 664 |
+
output_tokens=out_tok,
|
| 665 |
+
)
|
| 666 |
+
|
| 667 |
+
# Get next token
|
| 668 |
+
if device_sampling_params is not None:
|
| 669 |
+
out_tok = logits.unsqueeze(1)
|
| 670 |
+
else:
|
| 671 |
+
_, out_tok = sample_host(
|
| 672 |
+
logits,
|
| 673 |
+
temperature=sampling_params["temperature"],
|
| 674 |
+
top_p=sampling_params["top_p"],
|
| 675 |
+
on_host=True,
|
| 676 |
+
)
|
| 677 |
+
|
| 678 |
+
if iteration == 0:
|
| 679 |
+
profiler.end("compile_decode")
|
| 680 |
+
decode_iteration_time = profiler.get_duration("compile_decode")
|
| 681 |
+
else:
|
| 682 |
+
profiler.end(f"inference_decode_time_{iteration}")
|
| 683 |
+
decode_iteration_time = profiler.get_duration(f"inference_decode_time_{iteration}")
|
| 684 |
+
|
| 685 |
+
# Log per-iteration performance
|
| 686 |
+
tokens_per_second_per_user = 1 / decode_iteration_time if decode_iteration_time > 0 else 0
|
| 687 |
+
logger.debug(
|
| 688 |
+
f"Iteration {iteration}: {1000 * decode_iteration_time:.0f}ms @ {tokens_per_second_per_user:.1f} tok/s/user"
|
| 689 |
+
)
|
| 690 |
+
|
| 691 |
+
current_pos += 1
|
| 692 |
+
|
| 693 |
+
# Save output token to print out later and check for EOS
|
| 694 |
+
for user in range(batch_size):
|
| 695 |
+
user_tok = out_tok[user].item()
|
| 696 |
+
if user_tok not in tokenizer.stop_tokens and not user_done[user]:
|
| 697 |
+
all_outputs[user].append(user_tok)
|
| 698 |
+
else:
|
| 699 |
+
if not measure_accuracy: # For accuracy tests, keep decoding (teacher forcing)
|
| 700 |
+
user_done[user] = True
|
| 701 |
+
logger.trace(f"[User {user}] Finished decoding at iteration {iteration}")
|
| 702 |
+
else:
|
| 703 |
+
all_outputs[user].append(user_tok)
|
| 704 |
+
|
| 705 |
+
# Print out generated outputs for each user (debug level)
|
| 706 |
+
for user in range(batch_size):
|
| 707 |
+
text = "".join(tokenizer.decode(all_outputs[user]))
|
| 708 |
+
if len(text) > 100:
|
| 709 |
+
text = "..." + text[-97:]
|
| 710 |
+
text = text.replace("\n", " ")
|
| 711 |
+
logger.debug(f"[User {user}] {text}")
|
| 712 |
+
|
| 713 |
+
if all(user_done):
|
| 714 |
+
break
|
| 715 |
+
|
| 716 |
+
profiler.end("inference_decode")
|
| 717 |
+
|
| 718 |
+
# Final print of generated text
|
| 719 |
+
logger.info("Finished decoding, printing the final outputs...\n")
|
| 720 |
+
for user, output in enumerate(all_outputs):
|
| 721 |
+
text = tokenizer.decode(output)
|
| 722 |
+
# Find where prompt ends and generated text begins
|
| 723 |
+
prompt_text = input_prompts[user] if user < len(input_prompts) else ""
|
| 724 |
+
# Strip leading/trailing newlines from generated portion
|
| 725 |
+
text_after_prompt = text[len(prompt_text) :] if text.startswith(prompt_text) else text
|
| 726 |
+
short_prompt = (
|
| 727 |
+
(prompt_text[:100] + "\n<long prompt not printed in full>\n" + prompt_text[-100:])
|
| 728 |
+
if len(prompt_text) > 200
|
| 729 |
+
else prompt_text
|
| 730 |
+
)
|
| 731 |
+
logger.info(f"\n==USER {user} - PROMPT\n{short_prompt}\n==USER {user} - OUTPUT\n{text_after_prompt.strip()}\n")
|
| 732 |
+
profiler.end("run")
|
| 733 |
+
|
| 734 |
+
# --- Compute Metrics ---
|
| 735 |
+
compile_prefill_time = profiler.get_duration("compile_prefill")
|
| 736 |
+
compile_decode_time = profiler.get_duration("compile_decode")
|
| 737 |
+
total_inference_prefill_time = profiler.get_duration("inference_prefill")
|
| 738 |
+
|
| 739 |
+
# Sum decode times (skip compile iteration)
|
| 740 |
+
total_inference_decode_time = 0
|
| 741 |
+
num_decode_iterations = min(iteration + 1, max_generated_tokens)
|
| 742 |
+
for i in range(1, num_decode_iterations):
|
| 743 |
+
total_inference_decode_time += profiler.get_duration(f"inference_decode_time_{i}")
|
| 744 |
+
|
| 745 |
+
# Calculate performance metrics
|
| 746 |
+
avg_ttft_ms = (total_inference_prefill_time / batch_size) * 1000
|
| 747 |
+
tok_s_u = (num_decode_iterations - 1) / total_inference_decode_time if num_decode_iterations > 1 else 0
|
| 748 |
+
|
| 749 |
+
# Compute accuracy if applicable
|
| 750 |
+
top1_acc = None
|
| 751 |
+
top5_acc = None
|
| 752 |
+
if token_acc:
|
| 753 |
+
top1_acc, top5_acc = token_acc.compute_accuracy()
|
| 754 |
+
|
| 755 |
+
# --- Report Results ---
|
| 756 |
+
logger.info("")
|
| 757 |
+
logger.info("=== Performance Metrics ===")
|
| 758 |
+
logger.info(f"Device: {device_name}")
|
| 759 |
+
logger.info(f"Mode: {opt_mode}")
|
| 760 |
+
logger.info(f"Compile prefill: {compile_prefill_time:.2f}s")
|
| 761 |
+
logger.info(f"Compile decode: {compile_decode_time:.2f}s")
|
| 762 |
+
logger.info(f"TTFT: {avg_ttft_ms:.2f}ms")
|
| 763 |
+
logger.info(f"Decode speed: {tok_s_u:.2f} tok/s/u")
|
| 764 |
+
if top1_acc is not None:
|
| 765 |
+
logger.info(f"Top-1 accuracy: {top1_acc:.2f}%")
|
| 766 |
+
if top5_acc is not None:
|
| 767 |
+
logger.info(f"Top-5 accuracy: {top5_acc:.2f}%")
|
| 768 |
+
|
| 769 |
+
# Build measured metrics dict
|
| 770 |
+
measured = {
|
| 771 |
+
"ttft_ms": avg_ttft_ms,
|
| 772 |
+
"tok_s_u": tok_s_u,
|
| 773 |
+
}
|
| 774 |
+
if top1_acc is not None:
|
| 775 |
+
measured["top1"] = top1_acc
|
| 776 |
+
if top5_acc is not None:
|
| 777 |
+
measured["top5"] = top5_acc
|
| 778 |
+
|
| 779 |
+
# --- Validate Against Baseline ---
|
| 780 |
+
# Performance metrics (tok_s_u, ttft_ms): MUST come from simple_text_demo.py baseline.
|
| 781 |
+
# Accuracy metrics (top1, top5): centralized targets source for token-accuracy tests.
|
| 782 |
+
# Baselines are collected at module load time, before any test opens mesh_device.
|
| 783 |
+
baseline_metrics = _get_cached_baseline(device_name, batch_size, opt_mode)
|
| 784 |
+
|
| 785 |
+
if measure_accuracy:
|
| 786 |
+
# Token-accuracy tests: use centralized targets for top1/top5 only
|
| 787 |
+
baseline_source = "models/model_targets.yaml"
|
| 788 |
+
perf_md_metrics = EXPECTED_METRICS.get(opt_mode, {}).get(device_name, {})
|
| 789 |
+
expected_for_validation = {k: v for k, v in perf_md_metrics.items() if k in ("top1", "top5")}
|
| 790 |
+
logger.info("Token-accuracy test runs without trace - validating top1/top5 from model_targets.yaml only")
|
| 791 |
+
else:
|
| 792 |
+
# Performance tests: MUST use baseline from simple_text_demo.py
|
| 793 |
+
assert baseline_metrics is not None, (
|
| 794 |
+
f"Baseline collection from simple_text_demo.py failed for {device_name}/{opt_mode}/batch-{batch_size}. "
|
| 795 |
+
"Performance metrics (tok_s_u, ttft_ms) require a valid baseline measurement."
|
| 796 |
+
)
|
| 797 |
+
assert "tok_s_u" in baseline_metrics, f"Baseline missing 'tok_s_u': {baseline_metrics}"
|
| 798 |
+
assert "ttft_ms" in baseline_metrics, f"Baseline missing 'ttft_ms': {baseline_metrics}"
|
| 799 |
+
baseline_source = "simple_text_demo.py (TTTv1)"
|
| 800 |
+
expected_for_validation = baseline_metrics
|
| 801 |
+
|
| 802 |
+
if expected_for_validation:
|
| 803 |
+
logger.info(f"Comparing against baseline from: {baseline_source}")
|
| 804 |
+
if baseline_metrics:
|
| 805 |
+
logger.info(f" Baseline tok_s_u: {baseline_metrics.get('tok_s_u', 'N/A'):.2f}")
|
| 806 |
+
logger.info(f" Baseline ttft_ms: {baseline_metrics.get('ttft_ms', 'N/A'):.2f}")
|
| 807 |
+
logger.info(f" MLP1D tok_s_u: {tok_s_u:.2f}")
|
| 808 |
+
logger.info(f" MLP1D ttft_ms: {avg_ttft_ms:.2f}")
|
| 809 |
+
|
| 810 |
+
# Calculate relative performance
|
| 811 |
+
if baseline_metrics.get("tok_s_u"):
|
| 812 |
+
rel_perf = (tok_s_u / baseline_metrics["tok_s_u"]) * 100
|
| 813 |
+
logger.info(f" Relative performance: {rel_perf:.1f}% of baseline")
|
| 814 |
+
|
| 815 |
+
passed, message = validate_metrics(measured, expected_for_validation)
|
| 816 |
+
if passed:
|
| 817 |
+
logger.info(f"✓ All metrics within {PERF_TOLERANCE * 100:.0f}% of {baseline_source} targets")
|
| 818 |
+
else:
|
| 819 |
+
logger.warning(f"✗ Metrics outside tolerance: {message}")
|
| 820 |
+
# Don't fail the test on performance regression - just warn
|
| 821 |
+
# This allows the demo to run even if performance varies
|
| 822 |
+
|
| 823 |
+
logger.info(f"=== MLP1D Demo Complete ===")
|
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,487 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 21 |
+
import ttnn
|
| 22 |
+
|
| 23 |
+
from .auto_compose import to_torch_auto_compose
|
| 24 |
+
|
| 25 |
+
# ======================================================================================
|
| 26 |
+
# Public API
|
| 27 |
+
# ======================================================================================
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def compute_max_abs_error(impl, ref):
|
| 31 |
+
"""
|
| 32 |
+
Compute maximum absolute error between two tensors.
|
| 33 |
+
|
| 34 |
+
Supports both PyTorch and TTNN tensors. For TTNN tensors, computation
|
| 35 |
+
stays on device until the final scalar value.
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
impl: Implementation output (PyTorch or TTNN tensor)
|
| 39 |
+
ref: Reference output (PyTorch or TTNN tensor)
|
| 40 |
+
|
| 41 |
+
Returns:
|
| 42 |
+
float: Maximum absolute difference between tensors
|
| 43 |
+
|
| 44 |
+
Examples:
|
| 45 |
+
>>> a = torch.tensor([1.0, 2.0, 3.0])
|
| 46 |
+
>>> b = torch.tensor([1.1, 2.0, 2.9])
|
| 47 |
+
>>> _compute_max_abs_error(a, b)
|
| 48 |
+
0.10000002384185791
|
| 49 |
+
"""
|
| 50 |
+
try:
|
| 51 |
+
if _is_ttnn_tensor(impl) and _is_ttnn_tensor(ref):
|
| 52 |
+
# TTNN path - stay on device
|
| 53 |
+
diff = ttnn.subtract(impl, ref)
|
| 54 |
+
abs_diff = _ttnn_op_layout_invariant(diff, ttnn.abs)
|
| 55 |
+
return _ttnn_max_scalar_all_dtype(abs_diff)
|
| 56 |
+
elif _is_ttnn_tensor(impl):
|
| 57 |
+
return (to_torch_auto_compose(impl) - ref).abs().max().item()
|
| 58 |
+
elif _is_ttnn_tensor(ref):
|
| 59 |
+
return (impl - to_torch_auto_compose(ref)).abs().max().item()
|
| 60 |
+
else:
|
| 61 |
+
# PyTorch path - both must be torch tensors
|
| 62 |
+
return (impl - ref).abs().max().item()
|
| 63 |
+
except Exception as e:
|
| 64 |
+
return float("inf")
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def compute_mean_abs_error(impl, ref):
|
| 68 |
+
"""
|
| 69 |
+
Compute mean absolute error between two tensors.
|
| 70 |
+
|
| 71 |
+
Supports both PyTorch and TTNN tensors. For TTNN tensors, computation
|
| 72 |
+
stays on device until the final scalar value.
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
impl: Implementation output (PyTorch or TTNN tensor)
|
| 76 |
+
ref: Reference output (PyTorch or TTNN tensor)
|
| 77 |
+
|
| 78 |
+
Returns:
|
| 79 |
+
float: Mean absolute difference between tensors
|
| 80 |
+
|
| 81 |
+
Examples:
|
| 82 |
+
>>> a = torch.tensor([1.0, 2.0, 3.0])
|
| 83 |
+
>>> b = torch.tensor([1.1, 2.0, 2.9])
|
| 84 |
+
>>> _compute_mean_abs_error(a, b)
|
| 85 |
+
0.06666667014360428
|
| 86 |
+
"""
|
| 87 |
+
try:
|
| 88 |
+
if _is_ttnn_tensor(impl) and _is_ttnn_tensor(ref):
|
| 89 |
+
# TTNN path - stay on device
|
| 90 |
+
diff = ttnn.subtract(impl, ref)
|
| 91 |
+
abs_diff = _ttnn_op_layout_invariant(diff, ttnn.abs)
|
| 92 |
+
return _ttnn_mean_scalar_all_dtype(abs_diff)
|
| 93 |
+
elif _is_ttnn_tensor(impl):
|
| 94 |
+
return (to_torch_auto_compose(impl) - ref).abs().mean().item()
|
| 95 |
+
elif _is_ttnn_tensor(ref):
|
| 96 |
+
return (impl - to_torch_auto_compose(ref)).abs().mean().item()
|
| 97 |
+
else:
|
| 98 |
+
# PyTorch path - both must be torch tensors
|
| 99 |
+
return (impl - ref).abs().mean().item()
|
| 100 |
+
except Exception as e:
|
| 101 |
+
return float("inf")
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def compute_pcc(impl, ref):
|
| 105 |
+
"""
|
| 106 |
+
Compute Pearson Correlation Coefficient (PCC) between two tensors.
|
| 107 |
+
|
| 108 |
+
PCC measures the linear correlation between two tensors. Returns 1.0 for
|
| 109 |
+
perfect positive correlation, 0.0 for no correlation, and -1.0 for perfect
|
| 110 |
+
negative correlation. This is a common metric used in tt-metal for validating
|
| 111 |
+
tensor computations.
|
| 112 |
+
|
| 113 |
+
For TTNN tensors: Uses on-device computation (100-1000× faster). If edge cases
|
| 114 |
+
produce non-finite values (NaN/inf), raises ValueError with instructions to use
|
| 115 |
+
Pattern 2 (PyTorch reference with output_map for robust CPU-based PCC).
|
| 116 |
+
|
| 117 |
+
For PyTorch tensors: Uses robust numpy implementation (from tt-metal
|
| 118 |
+
comparison_funcs.py) that handles complex, NaN, inf, and constant tensors.
|
| 119 |
+
|
| 120 |
+
Args:
|
| 121 |
+
impl: Implementation output (PyTorch or TTNN tensor)
|
| 122 |
+
ref: Reference output (PyTorch or TTNN tensor)
|
| 123 |
+
|
| 124 |
+
Returns:
|
| 125 |
+
float: PCC value in range [-1.0, 1.0], or 0.0 on error
|
| 126 |
+
|
| 127 |
+
Raises:
|
| 128 |
+
ValueError: If TTNN-native computation produces non-finite values
|
| 129 |
+
|
| 130 |
+
Examples:
|
| 131 |
+
>>> a = torch.tensor([1.0, 2.0, 3.0])
|
| 132 |
+
>>> b = torch.tensor([1.0, 2.0, 3.0])
|
| 133 |
+
>>> _compute_pcc(a, b)
|
| 134 |
+
1.0
|
| 135 |
+
|
| 136 |
+
>>> a = torch.tensor([1.0, 2.0, 3.0])
|
| 137 |
+
>>> b = torch.tensor([3.0, 2.0, 1.0])
|
| 138 |
+
>>> _compute_pcc(a, b)
|
| 139 |
+
-1.0
|
| 140 |
+
"""
|
| 141 |
+
try:
|
| 142 |
+
# TTNN fast path - compute on device
|
| 143 |
+
if _is_ttnn_tensor(impl) and _is_ttnn_tensor(ref):
|
| 144 |
+
return compute_pcc_device(impl, ref)
|
| 145 |
+
elif _is_ttnn_tensor(impl):
|
| 146 |
+
return compute_pcc_host(to_torch_auto_compose(impl), ref)
|
| 147 |
+
elif _is_ttnn_tensor(ref):
|
| 148 |
+
return compute_pcc_host(impl, to_torch_auto_compose(ref))
|
| 149 |
+
else:
|
| 150 |
+
return compute_pcc_host(impl, ref)
|
| 151 |
+
except Exception:
|
| 152 |
+
return 0.0
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
# Default metrics dictionary for easy import
|
| 156 |
+
DEFAULT_METRICS = {
|
| 157 |
+
"max_abs_error": compute_max_abs_error,
|
| 158 |
+
"mean_abs_error": compute_mean_abs_error,
|
| 159 |
+
"pcc": compute_pcc,
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
# Allclose comparison with detailed delta string
|
| 164 |
+
def comp_allclose(impl, ref, rtol=1e-05, atol=1e-08):
|
| 165 |
+
"""
|
| 166 |
+
Compare two tensors using an allclose criterion and return (passing, details).
|
| 167 |
+
|
| 168 |
+
Provides both a TTNN-native on-device implementation and a PyTorch fallback.
|
| 169 |
+
Mirrors semantics of torch.allclose(..., equal_nan=True) and reports the
|
| 170 |
+
maximum absolute and relative deltas similar to comparison_funcs.py.
|
| 171 |
+
|
| 172 |
+
Args:
|
| 173 |
+
impl: Implementation output (PyTorch or TTNN tensor)
|
| 174 |
+
ref: Reference output (PyTorch or TTNN tensor)
|
| 175 |
+
rtol (float): Relative tolerance
|
| 176 |
+
atol (float): Absolute tolerance
|
| 177 |
+
|
| 178 |
+
Returns:
|
| 179 |
+
tuple[bool, str]: (passing, "Max ATOL Delta: x, Max RTOL Delta: y[,...]")
|
| 180 |
+
"""
|
| 181 |
+
try:
|
| 182 |
+
# TTNN-native path: compute deltas and allclose on device, then transfer final scalars
|
| 183 |
+
if _is_ttnn_tensor(impl) and _is_ttnn_tensor(ref):
|
| 184 |
+
# Compute deltas (device)
|
| 185 |
+
diff = _ttnn_op_layout_invariant(ttnn.subtract(impl, ref), ttnn.abs)
|
| 186 |
+
cal_atol = _ttnn_max_scalar_all_dtype(diff)
|
| 187 |
+
# For rtol delta, divide by abs(ref) (may produce inf for zeros; acceptable for reporting)
|
| 188 |
+
divided_by_ref = ttnn.divide(diff, _ttnn_op_layout_invariant(ref, ttnn.abs))
|
| 189 |
+
cal_rtol = _ttnn_max_scalar_all_dtype(divided_by_ref)
|
| 190 |
+
|
| 191 |
+
# equal_nan=True semantics and finite/infinite handling
|
| 192 |
+
isnan_impl = _ttnn_op_layout_invariant(impl, ttnn.isnan)
|
| 193 |
+
isnan_ref = _ttnn_op_layout_invariant(ref, ttnn.isnan)
|
| 194 |
+
both_nan = ttnn.logical_and(isnan_impl, isnan_ref)
|
| 195 |
+
|
| 196 |
+
isinf_impl = _ttnn_op_layout_invariant(impl, ttnn.isinf)
|
| 197 |
+
isinf_ref = _ttnn_op_layout_invariant(ref, ttnn.isinf)
|
| 198 |
+
impl_sign = _ttnn_op_layout_invariant(impl, ttnn.sign)
|
| 199 |
+
ref_sign = _ttnn_op_layout_invariant(ref, ttnn.sign)
|
| 200 |
+
same_sign_inf = ttnn.eq(impl_sign, ref_sign)
|
| 201 |
+
both_inf_same_sign = ttnn.logical_and(ttnn.logical_and(isinf_impl, isinf_ref), same_sign_inf)
|
| 202 |
+
|
| 203 |
+
# Finite elements where numeric closeness applies
|
| 204 |
+
any_nan = ttnn.logical_or(isnan_impl, isnan_ref)
|
| 205 |
+
any_inf = ttnn.logical_or(isinf_impl, isinf_ref)
|
| 206 |
+
finite_both = _ttnn_op_layout_invariant(ttnn.logical_or(any_nan, any_inf), ttnn.logical_not)
|
| 207 |
+
|
| 208 |
+
# |impl - ref| <= atol + rtol * |ref|
|
| 209 |
+
bound = ttnn.add(ttnn.mul(_ttnn_op_layout_invariant(ref, ttnn.abs), rtol, dtype=ttnn.bfloat16), atol)
|
| 210 |
+
close_numeric = ttnn.le(diff, bound)
|
| 211 |
+
finite_and_close = ttnn.logical_and(finite_both, close_numeric)
|
| 212 |
+
|
| 213 |
+
ok_mask = ttnn.logical_or(ttnn.logical_or(both_nan, both_inf_same_sign), finite_and_close)
|
| 214 |
+
fail_mask = _ttnn_op_layout_invariant(ok_mask, ttnn.logical_not)
|
| 215 |
+
|
| 216 |
+
# Reduce to scalar: any failure -> 1.0 else 0.0
|
| 217 |
+
fail_indicator = _ttnn_op_layout_invariant(fail_mask, ttnn.where, true_value=1.0, false_value=0.0)
|
| 218 |
+
any_fail = _ttnn_max_scalar_all_dtype(fail_indicator)
|
| 219 |
+
passing = any_fail == 0.0
|
| 220 |
+
|
| 221 |
+
output_str = f"Max ATOL Delta: {cal_atol}, Max RTOL Delta: {cal_rtol}"
|
| 222 |
+
if not passing:
|
| 223 |
+
output_str += ", Allclose check failed"
|
| 224 |
+
return passing, output_str
|
| 225 |
+
|
| 226 |
+
# Fallback: compute with PyTorch (handles mixed inputs by converting TTNN -> torch)
|
| 227 |
+
impl_torch = to_torch_auto_compose(impl) if _is_ttnn_tensor(impl) else impl
|
| 228 |
+
ref_torch = to_torch_auto_compose(ref) if _is_ttnn_tensor(ref) else ref
|
| 229 |
+
|
| 230 |
+
if torch.is_tensor(impl_torch) and torch.is_tensor(ref_torch):
|
| 231 |
+
# Match dtype for fair comparison
|
| 232 |
+
if impl_torch.dtype != ref_torch.dtype:
|
| 233 |
+
ref_torch = ref_torch.to(impl_torch.dtype)
|
| 234 |
+
|
| 235 |
+
atol_delta = torch.max(torch.abs(impl_torch - ref_torch)).item()
|
| 236 |
+
# May produce inf where ref == 0; this mirrors comparison_funcs.py behavior
|
| 237 |
+
rtol_delta = torch.max(torch.abs(impl_torch - ref_torch) / torch.abs(ref_torch)).item()
|
| 238 |
+
passing = torch.allclose(impl_torch, ref_torch, rtol, atol, True)
|
| 239 |
+
output_str = f"Max ATOL Delta: {atol_delta}, Max RTOL Delta: {rtol_delta}"
|
| 240 |
+
if not passing:
|
| 241 |
+
output_str += ", Allclose check failed"
|
| 242 |
+
return passing, output_str
|
| 243 |
+
|
| 244 |
+
# Unsupported types
|
| 245 |
+
return False, "Unsupported input types for comp_allclose"
|
| 246 |
+
except Exception as e:
|
| 247 |
+
return False, f"Error computing comp_allclose: {e}"
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def compute_pcc_device(impl, ref):
|
| 251 |
+
"""Fast on-device PCC for TTNN tensors only."""
|
| 252 |
+
try:
|
| 253 |
+
# Early edge-case handling to mirror CPU semantics
|
| 254 |
+
# - All NaNs → 1.0; mixed NaNs → 0.0
|
| 255 |
+
# - One tensor all zero and the other not → 0.0
|
| 256 |
+
# - Both constant → 1.0 if equal, else 0.0
|
| 257 |
+
|
| 258 |
+
# Any nonzero check (all-zero detection)
|
| 259 |
+
impl_abs_max = _ttnn_max_scalar_all_dtype(_ttnn_op_layout_invariant(impl, ttnn.abs))
|
| 260 |
+
ref_abs_max = _ttnn_max_scalar_all_dtype(_ttnn_op_layout_invariant(ref, ttnn.abs))
|
| 261 |
+
|
| 262 |
+
impl_has_any = impl_abs_max != 0.0
|
| 263 |
+
ref_has_any = ref_abs_max != 0.0
|
| 264 |
+
if impl_has_any != ref_has_any:
|
| 265 |
+
return 0.0
|
| 266 |
+
|
| 267 |
+
# Min/Max scalars for constant and NaN detection
|
| 268 |
+
impl_min = _ttnn_min_scalar_all_dtype(impl)
|
| 269 |
+
impl_max = _ttnn_max_scalar_all_dtype(impl)
|
| 270 |
+
ref_min = _ttnn_min_scalar_all_dtype(ref)
|
| 271 |
+
ref_max = _ttnn_max_scalar_all_dtype(ref)
|
| 272 |
+
|
| 273 |
+
impl_min_finite = np.isfinite(impl_min)
|
| 274 |
+
impl_max_finite = np.isfinite(impl_max)
|
| 275 |
+
ref_min_finite = np.isfinite(ref_min)
|
| 276 |
+
ref_max_finite = np.isfinite(ref_max)
|
| 277 |
+
|
| 278 |
+
impl_all_nan = (not impl_min_finite) and (not impl_max_finite)
|
| 279 |
+
ref_all_nan = (not ref_min_finite) and (not ref_max_finite)
|
| 280 |
+
if impl_all_nan and ref_all_nan:
|
| 281 |
+
return 1.0
|
| 282 |
+
if impl_all_nan != ref_all_nan:
|
| 283 |
+
return 0.0
|
| 284 |
+
|
| 285 |
+
# Constant tensors
|
| 286 |
+
if impl_min_finite and impl_max_finite and ref_min_finite and ref_max_finite:
|
| 287 |
+
if impl_min == impl_max and ref_min == ref_max:
|
| 288 |
+
return (
|
| 289 |
+
1.0
|
| 290 |
+
if torch.isclose(
|
| 291 |
+
torch.tensor(impl_max, dtype=torch.float32), torch.tensor(ref_max, dtype=torch.float32)
|
| 292 |
+
)
|
| 293 |
+
else 0.0
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
# Standard PCC formula on device
|
| 297 |
+
mean_impl = _ttnn_mean_scalar_all_dtype(impl)
|
| 298 |
+
mean_ref = _ttnn_mean_scalar_all_dtype(ref)
|
| 299 |
+
|
| 300 |
+
impl_centered = ttnn.subtract(impl, mean_impl)
|
| 301 |
+
ref_centered = ttnn.subtract(ref, mean_ref)
|
| 302 |
+
|
| 303 |
+
# todo)) ttnn.sum() does local reduction only; need CCL reduction for global sum when adding support for multiple-devices
|
| 304 |
+
# [INFO] we cast to float32 to avoid overflow when impl and ref are in bfloat8_b or bfloat4_b
|
| 305 |
+
numerator = ttnn.sum(ttnn.mul(impl_centered, ref_centered, dtype=ttnn.float32))
|
| 306 |
+
impl_sq_sum = ttnn.sum(ttnn.mul(impl_centered, impl_centered, dtype=ttnn.float32))
|
| 307 |
+
ref_sq_sum = ttnn.sum(ttnn.mul(ref_centered, ref_centered, dtype=ttnn.float32))
|
| 308 |
+
denominator = ttnn.sqrt(ttnn.mul(impl_sq_sum, ref_sq_sum, dtype=ttnn.float32))
|
| 309 |
+
|
| 310 |
+
# Safe divide
|
| 311 |
+
denom_scalar = denominator.item()
|
| 312 |
+
if denom_scalar == 0.0 or not np.isfinite(denom_scalar):
|
| 313 |
+
return 0.0
|
| 314 |
+
|
| 315 |
+
pcc = numerator.item() / denom_scalar
|
| 316 |
+
if not np.isfinite(pcc):
|
| 317 |
+
return 0.0
|
| 318 |
+
return pcc
|
| 319 |
+
except Exception as e:
|
| 320 |
+
# todo)) maybe return a string for logging: f"Error computing PCC on device: {e}, impl: {impl}, ref: {ref}"
|
| 321 |
+
return 0.0
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
# code stolen from tests/tt_eager/python_api_testing/sweep_tests/comparison_funcs.py
|
| 325 |
+
# and models/common/utility_functions.py
|
| 326 |
+
def compute_pcc_host(impl, ref):
|
| 327 |
+
"""Robust CPU PCC for PyTorch tensors only."""
|
| 328 |
+
try:
|
| 329 |
+
calculated = impl
|
| 330 |
+
golden = ref
|
| 331 |
+
if golden.dtype != calculated.dtype:
|
| 332 |
+
calculated = calculated.type(golden.dtype)
|
| 333 |
+
|
| 334 |
+
# Handle complex tensors
|
| 335 |
+
if golden.is_complex() and calculated.is_complex():
|
| 336 |
+
golden = torch.view_as_real(golden.clone())
|
| 337 |
+
calculated = torch.view_as_real(calculated.clone())
|
| 338 |
+
|
| 339 |
+
# Convert to float if needed
|
| 340 |
+
if not (golden.is_floating_point() or calculated.is_floating_point()):
|
| 341 |
+
golden = golden.to(torch.float)
|
| 342 |
+
calculated = calculated.to(torch.float)
|
| 343 |
+
|
| 344 |
+
# Both tensors are nan
|
| 345 |
+
if torch.all(torch.isnan(golden)) and torch.all(torch.isnan(calculated)):
|
| 346 |
+
return 1.0
|
| 347 |
+
|
| 348 |
+
# One tensor is all nan, the other is not
|
| 349 |
+
if torch.all(torch.isnan(golden)) or torch.all(torch.isnan(calculated)):
|
| 350 |
+
return 0.0
|
| 351 |
+
|
| 352 |
+
# One tensor is all zero, the other is not
|
| 353 |
+
if torch.any(golden.bool()) != torch.any(calculated.bool()):
|
| 354 |
+
return 0.0
|
| 355 |
+
|
| 356 |
+
# Mask all infs and nans
|
| 357 |
+
golden = golden.clone()
|
| 358 |
+
golden[
|
| 359 |
+
torch.logical_or(
|
| 360 |
+
torch.isnan(golden),
|
| 361 |
+
torch.logical_or(torch.isinf(golden), torch.isneginf(golden)),
|
| 362 |
+
)
|
| 363 |
+
] = 0
|
| 364 |
+
calculated = calculated.clone()
|
| 365 |
+
calculated[
|
| 366 |
+
torch.logical_or(
|
| 367 |
+
torch.isnan(calculated),
|
| 368 |
+
torch.logical_or(torch.isinf(calculated), torch.isneginf(calculated)),
|
| 369 |
+
)
|
| 370 |
+
] = 0
|
| 371 |
+
|
| 372 |
+
if torch.equal(golden, calculated):
|
| 373 |
+
return 1.0
|
| 374 |
+
|
| 375 |
+
if golden.dtype == torch.bfloat16:
|
| 376 |
+
golden = golden.type(torch.float32)
|
| 377 |
+
calculated = calculated.type(torch.float32)
|
| 378 |
+
|
| 379 |
+
# Single element case
|
| 380 |
+
if golden.numel() == 1:
|
| 381 |
+
return float(torch.equal(golden, calculated))
|
| 382 |
+
|
| 383 |
+
# If both tensors are constant
|
| 384 |
+
if torch.max(golden) == torch.min(golden) and torch.max(calculated) == torch.min(calculated):
|
| 385 |
+
return torch.isclose(torch.max(golden), torch.max(calculated)).item()
|
| 386 |
+
|
| 387 |
+
# Compute PCC using numpy's corrcoef
|
| 388 |
+
cal_pcc = np.ma.corrcoef(
|
| 389 |
+
np.ma.masked_invalid(torch.squeeze(golden).detach().numpy()).flatten(),
|
| 390 |
+
np.ma.masked_invalid(torch.squeeze(calculated).detach().numpy()).flatten(),
|
| 391 |
+
)
|
| 392 |
+
# Remove correlation coefficient with self (typically always 1.0)
|
| 393 |
+
mask = np.ones(cal_pcc.shape, dtype=bool)
|
| 394 |
+
np.fill_diagonal(mask, 0)
|
| 395 |
+
cal_pcc = np.min(cal_pcc[mask])
|
| 396 |
+
|
| 397 |
+
if isinstance(cal_pcc, np.ma.core.MaskedConstant):
|
| 398 |
+
return 1.0
|
| 399 |
+
|
| 400 |
+
return float(cal_pcc)
|
| 401 |
+
except Exception:
|
| 402 |
+
return 0.0
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
# ======================================================================================
|
| 406 |
+
# Private Implementation
|
| 407 |
+
# ======================================================================================
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
def _is_ttnn_tensor(x):
|
| 411 |
+
"""Safely detect TTNN tensors even if ttnn.Tensor is not defined in this environment."""
|
| 412 |
+
return isinstance(x, ttnn.Tensor)
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
def _ttnn_op_layout_invariant(x, op_func, **kwargs):
|
| 416 |
+
"""
|
| 417 |
+
Generic helper for TTNN operations that require TILE layout for non-sharded tensors.
|
| 418 |
+
|
| 419 |
+
Args:
|
| 420 |
+
x: TTNN tensor
|
| 421 |
+
op_func: TTNN operation function to call (e.g., ttnn.sign, ttnn.abs, ttnn.typecast)
|
| 422 |
+
**kwargs: Additional keyword arguments to pass to op_func (e.g., dtype=ttnn.bfloat16 for typecast)
|
| 423 |
+
|
| 424 |
+
Returns:
|
| 425 |
+
Result of op_func applied to x, with layout preserved.
|
| 426 |
+
"""
|
| 427 |
+
# [ttnn contract] all TTNN operations require tensors to be in TILE layout when working with non-sharded tensors:
|
| 428 |
+
# ttnn.sign(), ttnn.abs(), ttnn.isinf(), ttnn.isnan(), ttnn.typecast(), ttnn.where(), ttnn.logical_not()
|
| 429 |
+
layout = x.get_layout()
|
| 430 |
+
if layout == ttnn.TILE_LAYOUT or x.is_sharded(): # sharded tensors can use either layout
|
| 431 |
+
return op_func(x, **kwargs)
|
| 432 |
+
else:
|
| 433 |
+
return ttnn.to_layout(op_func(ttnn.to_layout(x, ttnn.TILE_LAYOUT), **kwargs), layout)
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def _ttnn_max_scalar_all_dtype(x):
|
| 437 |
+
x_bf16 = _ttnn_op_layout_invariant(x, ttnn.typecast, dtype=ttnn.bfloat16)
|
| 438 |
+
|
| 439 |
+
# [ttnn contract] ttnn.max() internally calls a FillPad operation, which only supports the following dtypes:
|
| 440 |
+
# BFLOAT16
|
| 441 |
+
# FLOAT32
|
| 442 |
+
# UINT16, UINT32, INT32
|
| 443 |
+
# UINT8
|
| 444 |
+
# see ttnn/cpp/ttnn/operations/data_movement/fill_pad/device/fill_pad_program_factory.hpp for more details
|
| 445 |
+
# [INFO] we cast to bfloat16 to avoid the limitation of the FillPad operation when dealing with e.g., bfloat8_b and bfloat4_b dtypes
|
| 446 |
+
|
| 447 |
+
# [ttnn contract] When called without a dim parameter, ttnn.max() returns the maximum value across the entire tensor as a scalar.
|
| 448 |
+
max_val_tensor = ttnn.max(x_bf16)
|
| 449 |
+
|
| 450 |
+
# [ttnn contract] When called without a dim parameter, ttnn.max() returns the maximum value across the entire tensor as a scalar.
|
| 451 |
+
# The method supports multiple data types:
|
| 452 |
+
# FLOAT32 → Python float
|
| 453 |
+
# BFLOAT16 → Python float (cast from bfloat16)
|
| 454 |
+
# BFLOAT8_B and BFLOAT4_B → Python float
|
| 455 |
+
return max_val_tensor.item()
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
def _ttnn_mean_scalar_all_dtype(x):
|
| 459 |
+
x_bf16 = _ttnn_op_layout_invariant(x, ttnn.typecast, dtype=ttnn.bfloat16)
|
| 460 |
+
|
| 461 |
+
# [ttnn contract] ttnn.mean() internally calls a FillPad operation, which only supports the following dtypes:
|
| 462 |
+
# BFLOAT16
|
| 463 |
+
# FLOAT32
|
| 464 |
+
# UINT16, UINT32, INT32
|
| 465 |
+
# UINT8
|
| 466 |
+
# see ttnn/cpp/ttnn/operations/data_movement/fill_pad/device/fill_pad_program_factory.hpp for more details
|
| 467 |
+
# [INFO] we cast to bfloat16 to avoid the limitation of the FillPad operation when dealing with e.g., bfloat8_b and bfloat4_b dtypes
|
| 468 |
+
|
| 469 |
+
# [ttnn contract] When called without a dim parameter, ttnn.mean() returns the mean value across the entire tensor as a scalar.
|
| 470 |
+
mean_val_tensor = ttnn.mean(x_bf16)
|
| 471 |
+
return mean_val_tensor.item()
|
| 472 |
+
|
| 473 |
+
|
| 474 |
+
def _ttnn_min_scalar_all_dtype(x):
|
| 475 |
+
x_bf16 = _ttnn_op_layout_invariant(x, ttnn.typecast, dtype=ttnn.bfloat16)
|
| 476 |
+
|
| 477 |
+
# [ttnn contract] ttnn.mean() internally calls a FillPad operation, which only supports the following dtypes:
|
| 478 |
+
# BFLOAT16
|
| 479 |
+
# FLOAT32
|
| 480 |
+
# UINT16, UINT32, INT32
|
| 481 |
+
# UINT8
|
| 482 |
+
# see ttnn/cpp/ttnn/operations/data_movement/fill_pad/device/fill_pad_program_factory.hpp for more details
|
| 483 |
+
# [INFO] we cast to bfloat16 to avoid the limitation of the FillPad operation when dealing with e.g., bfloat8_b and bfloat4_b dtypes
|
| 484 |
+
|
| 485 |
+
# [ttnn contract] When called without a dim parameter, ttnn.mean() returns the mean value across the entire tensor as a scalar.
|
| 486 |
+
min_val_tensor = ttnn.min(x_bf16)
|
| 487 |
+
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/readiness_check/check_degenerate_output.py
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
Machine check for degenerate generated text in readiness artifacts.
|
| 6 |
+
|
| 7 |
+
This is a runner-side verification, not a quality judgement. It flags
|
| 8 |
+
mechanically broken generation — the kind produced by decode-loop bugs
|
| 9 |
+
(stale token/position feedback, stale trace inputs) — while staying
|
| 10 |
+
agnostic to model style, checkpoint type (base vs instruct), and content.
|
| 11 |
+
|
| 12 |
+
The key signal is adjacent-token duplication: a healthy model, base or
|
| 13 |
+
instruct, does not emit nearly every word twice while the text continues
|
| 14 |
+
to advance ("the the difference difference between between ..."). Phrase
|
| 15 |
+
-level looping, by contrast, is common in base checkpoints under greedy
|
| 16 |
+
decoding and is reported only as advisory.
|
| 17 |
+
|
| 18 |
+
Checked artifacts (discovered under one or more roots):
|
| 19 |
+
|
| 20 |
+
- ``readiness_vllm/vllm_qualitative_outputs.json`` written by
|
| 21 |
+
``run_vllm_server`` (list of {prompt, greedy_completion,
|
| 22 |
+
sampled_completion}).
|
| 23 |
+
- ``autoregressive_meta.json`` written by ``run_autoregressive``
|
| 24 |
+
({hf: {token_ids}, tt: {token_ids}, ...}) plus the sibling
|
| 25 |
+
``tt_completion.txt`` when present.
|
| 26 |
+
|
| 27 |
+
Exit codes: 0 = clean, 1 = advisory findings only, 2 = critical findings,
|
| 28 |
+
3 = checker-internal error (never a model verdict).
|
| 29 |
+
|
| 30 |
+
Invoke by file path rather than ``python -m`` so the package ``__init__``
|
| 31 |
+
(which imports torch) is not pulled in:
|
| 32 |
+
|
| 33 |
+
python models/common/readiness_check/check_degenerate_output.py \\
|
| 34 |
+
[--model-dir models/autoports/<model>] [--hf-model <hf-model-id>] \\
|
| 35 |
+
[--root models/autoports] [--scope all|vllm|autoregressive] \\
|
| 36 |
+
[--missing-artifacts advisory|critical] [--json report.json]
|
| 37 |
+
|
| 38 |
+
Scoping precedence: explicit paths, then ``--model-dir``, then ``--hf-model``
|
| 39 |
+
fuzzy resolution against autoport directory names, then the unscoped
|
| 40 |
+
``--root``. Scoped runs ensure stale artifacts from another model on the
|
| 41 |
+
same machine can neither pass nor fail this check. Unreadable artifacts are
|
| 42 |
+
critical findings (the stage owns its evidence); exit 3 is reserved for
|
| 43 |
+
checker/environment faults.
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
from __future__ import annotations
|
| 47 |
+
|
| 48 |
+
import argparse
|
| 49 |
+
import json
|
| 50 |
+
import re
|
| 51 |
+
import sys
|
| 52 |
+
from dataclasses import asdict, dataclass, field
|
| 53 |
+
from pathlib import Path
|
| 54 |
+
from typing import Any, Iterable, Sequence
|
| 55 |
+
|
| 56 |
+
# A completion shorter than this many words is too small for a stable
|
| 57 |
+
# duplication rate; it is skipped rather than judged.
|
| 58 |
+
MIN_WORDS_FOR_DUPLICATION = 20
|
| 59 |
+
# Critical: fraction of consecutive word pairs that are identical.
|
| 60 |
+
# Calibration on experiment archives: healthy serving outputs measure
|
| 61 |
+
# <= 0.8% (max over completions); the known token-feedback bug measures
|
| 62 |
+
# 54% mean / 94% max. Anything above 10% is mechanical, not stylistic.
|
| 63 |
+
ADJACENT_DUPLICATION_CRITICAL = 0.10
|
| 64 |
+
# Advisory: fraction of the completion covered by repeats of its most
|
| 65 |
+
# common trigram. Base checkpoints legitimately loop phrases under greedy
|
| 66 |
+
# decoding, so this never fails the check on its own.
|
| 67 |
+
TRIGRAM_LOOP_ADVISORY = 0.50
|
| 68 |
+
MIN_WORDS_FOR_LOOP = 50
|
| 69 |
+
# Advisory: a near-empty completion when many tokens were requested.
|
| 70 |
+
NEAR_EMPTY_CHARS = 5
|
| 71 |
+
|
| 72 |
+
_WORD_RE = re.compile(r"\w+", re.UNICODE)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
@dataclass
|
| 76 |
+
class Finding:
|
| 77 |
+
severity: str # "critical" | "advisory"
|
| 78 |
+
artifact: str
|
| 79 |
+
label: str
|
| 80 |
+
metric: str
|
| 81 |
+
value: float
|
| 82 |
+
threshold: float
|
| 83 |
+
detail: str
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@dataclass
|
| 87 |
+
class Report:
|
| 88 |
+
findings: list[Finding] = field(default_factory=list)
|
| 89 |
+
measured: list[dict[str, Any]] = field(default_factory=list)
|
| 90 |
+
|
| 91 |
+
@property
|
| 92 |
+
def exit_code(self) -> int:
|
| 93 |
+
if any(f.severity == "critical" for f in self.findings):
|
| 94 |
+
return 2
|
| 95 |
+
if self.findings:
|
| 96 |
+
return 1
|
| 97 |
+
return 0
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def words_of(text: str) -> list[str]:
|
| 101 |
+
return [w.lower() for w in _WORD_RE.findall(text)]
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def adjacent_duplication(tokens: Sequence[Any]) -> float:
|
| 105 |
+
"""Fraction of consecutive token pairs that are identical."""
|
| 106 |
+
if len(tokens) < 2:
|
| 107 |
+
return 0.0
|
| 108 |
+
dup = sum(1 for a, b in zip(tokens, tokens[1:]) if a == b)
|
| 109 |
+
return dup / (len(tokens) - 1)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def trigram_loop_fraction(tokens: Sequence[Any]) -> float:
|
| 113 |
+
"""Fraction of the sequence covered by non-overlapping repeats of its most common trigram."""
|
| 114 |
+
if len(tokens) < 3:
|
| 115 |
+
return 0.0
|
| 116 |
+
counts: dict[tuple[Any, ...], int] = {}
|
| 117 |
+
for i in range(len(tokens) - 2):
|
| 118 |
+
gram = tuple(tokens[i : i + 3])
|
| 119 |
+
counts[gram] = counts.get(gram, 0) + 1
|
| 120 |
+
top_gram = max(counts, key=counts.get) # type: ignore[arg-type]
|
| 121 |
+
covered = 0
|
| 122 |
+
i = 0
|
| 123 |
+
while i <= len(tokens) - 3:
|
| 124 |
+
if tuple(tokens[i : i + 3]) == top_gram:
|
| 125 |
+
covered += 3
|
| 126 |
+
i += 3
|
| 127 |
+
else:
|
| 128 |
+
i += 1
|
| 129 |
+
return covered / len(tokens)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def check_completion(
|
| 133 |
+
report: Report,
|
| 134 |
+
*,
|
| 135 |
+
artifact: Path,
|
| 136 |
+
label: str,
|
| 137 |
+
text: str | None,
|
| 138 |
+
token_ids: Sequence[int] | None = None,
|
| 139 |
+
) -> None:
|
| 140 |
+
"""Apply degeneracy metrics to one completion (text and/or token ids)."""
|
| 141 |
+
tokens: Sequence[Any] | None = None
|
| 142 |
+
source = None
|
| 143 |
+
if text is not None and text.strip():
|
| 144 |
+
tokens = words_of(text)
|
| 145 |
+
source = "words"
|
| 146 |
+
elif token_ids:
|
| 147 |
+
# No text available: token-id duplication is a weaker but still
|
| 148 |
+
# meaningful signal (repeated layout tokens make it noisier).
|
| 149 |
+
tokens = list(token_ids)
|
| 150 |
+
source = "token_ids"
|
| 151 |
+
|
| 152 |
+
measured: dict[str, Any] = {"artifact": str(artifact), "label": label, "source": source}
|
| 153 |
+
|
| 154 |
+
if text is not None and len(text.strip()) < NEAR_EMPTY_CHARS:
|
| 155 |
+
report.findings.append(
|
| 156 |
+
Finding(
|
| 157 |
+
severity="advisory",
|
| 158 |
+
artifact=str(artifact),
|
| 159 |
+
label=label,
|
| 160 |
+
metric="near_empty_completion",
|
| 161 |
+
value=float(len(text.strip())),
|
| 162 |
+
threshold=float(NEAR_EMPTY_CHARS),
|
| 163 |
+
detail="Completion is empty or whitespace; verify EOS handling and sampler output.",
|
| 164 |
+
)
|
| 165 |
+
)
|
| 166 |
+
measured["near_empty"] = True
|
| 167 |
+
report.measured.append(measured)
|
| 168 |
+
return
|
| 169 |
+
|
| 170 |
+
if tokens is None:
|
| 171 |
+
report.measured.append(measured)
|
| 172 |
+
return
|
| 173 |
+
|
| 174 |
+
dup = adjacent_duplication(tokens)
|
| 175 |
+
loop = trigram_loop_fraction(tokens)
|
| 176 |
+
measured.update(
|
| 177 |
+
{"num_tokens": len(tokens), "adjacent_duplication": round(dup, 4), "trigram_loop_fraction": round(loop, 4)}
|
| 178 |
+
)
|
| 179 |
+
report.measured.append(measured)
|
| 180 |
+
|
| 181 |
+
if len(tokens) >= MIN_WORDS_FOR_DUPLICATION and dup > ADJACENT_DUPLICATION_CRITICAL:
|
| 182 |
+
report.findings.append(
|
| 183 |
+
Finding(
|
| 184 |
+
severity="critical",
|
| 185 |
+
artifact=str(artifact),
|
| 186 |
+
label=label,
|
| 187 |
+
metric="adjacent_duplication",
|
| 188 |
+
value=round(dup, 4),
|
| 189 |
+
threshold=ADJACENT_DUPLICATION_CRITICAL,
|
| 190 |
+
detail=(
|
| 191 |
+
"Adjacent-token duplication this high while the text advances is a "
|
| 192 |
+
"decode-loop input bug signature (stale token/position/trace feedback), "
|
| 193 |
+
"not a model-quality property. Compare the same prompt against the HF "
|
| 194 |
+
"reference before classifying this as a model limitation."
|
| 195 |
+
),
|
| 196 |
+
)
|
| 197 |
+
)
|
| 198 |
+
elif len(tokens) >= MIN_WORDS_FOR_LOOP and loop > TRIGRAM_LOOP_ADVISORY:
|
| 199 |
+
report.findings.append(
|
| 200 |
+
Finding(
|
| 201 |
+
severity="advisory",
|
| 202 |
+
artifact=str(artifact),
|
| 203 |
+
label=label,
|
| 204 |
+
metric="trigram_loop_fraction",
|
| 205 |
+
value=round(loop, 4),
|
| 206 |
+
threshold=TRIGRAM_LOOP_ADVISORY,
|
| 207 |
+
detail=(
|
| 208 |
+
"Completion is dominated by one repeating phrase. This can be normal for "
|
| 209 |
+
"base checkpoints under greedy decoding; verify against the HF reference "
|
| 210 |
+
"on the same prompt."
|
| 211 |
+
),
|
| 212 |
+
)
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def _load_artifact(report: Report, path: Path) -> Any | None:
|
| 217 |
+
"""Parse an artifact, recording unreadable files as critical findings.
|
| 218 |
+
|
| 219 |
+
A required artifact that cannot be read is failed stage evidence, not a
|
| 220 |
+
checker fault, so it goes through the normal finding path rather than
|
| 221 |
+
exiting 3.
|
| 222 |
+
"""
|
| 223 |
+
try:
|
| 224 |
+
return json.loads(path.read_text(encoding="utf-8"))
|
| 225 |
+
except Exception as exc: # noqa: BLE001
|
| 226 |
+
report.findings.append(
|
| 227 |
+
Finding(
|
| 228 |
+
severity="critical",
|
| 229 |
+
artifact=str(path),
|
| 230 |
+
label="artifact parse",
|
| 231 |
+
metric="unreadable_artifact",
|
| 232 |
+
value=0.0,
|
| 233 |
+
threshold=1.0,
|
| 234 |
+
detail=f"Artifact could not be read or parsed ({exc}). Regenerate it from the documented runner.",
|
| 235 |
+
)
|
| 236 |
+
)
|
| 237 |
+
return None
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def check_vllm_qualitative(report: Report, path: Path) -> None:
|
| 241 |
+
items = _load_artifact(report, path)
|
| 242 |
+
if items is None:
|
| 243 |
+
return
|
| 244 |
+
for i, item in enumerate(items):
|
| 245 |
+
prompt = str(item.get("prompt", ""))[:60]
|
| 246 |
+
for key in ("greedy_completion", "sampled_completion"):
|
| 247 |
+
if key in item:
|
| 248 |
+
check_completion(
|
| 249 |
+
report,
|
| 250 |
+
artifact=path,
|
| 251 |
+
label=f"prompt[{i}] {key} ({prompt!r})",
|
| 252 |
+
text=item.get(key) or "",
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def check_autoregressive_meta(report: Report, path: Path) -> None:
|
| 257 |
+
meta = _load_artifact(report, path)
|
| 258 |
+
if meta is None:
|
| 259 |
+
return
|
| 260 |
+
tt = meta.get("tt", {})
|
| 261 |
+
text_path = path.parent / "tt_completion.txt"
|
| 262 |
+
text = text_path.read_text(encoding="utf-8") if text_path.exists() else None
|
| 263 |
+
check_completion(
|
| 264 |
+
report,
|
| 265 |
+
artifact=path,
|
| 266 |
+
label="tt free-running completion",
|
| 267 |
+
text=text,
|
| 268 |
+
token_ids=tt.get("token_ids"),
|
| 269 |
+
)
|
| 270 |
+
hf_ids = meta.get("hf", {}).get("token_ids")
|
| 271 |
+
tt_ids = tt.get("token_ids")
|
| 272 |
+
if hf_ids and tt_ids:
|
| 273 |
+
match = sum(1 for a, b in zip(hf_ids, tt_ids) if a == b)
|
| 274 |
+
report.measured.append(
|
| 275 |
+
{
|
| 276 |
+
"artifact": str(path),
|
| 277 |
+
"label": "hf/tt token agreement (informational)",
|
| 278 |
+
"matching_tokens": match,
|
| 279 |
+
"compared_tokens": min(len(hf_ids), len(tt_ids)),
|
| 280 |
+
}
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def discover(roots: Iterable[Path], scope: str) -> tuple[list[Path], list[Path]]:
|
| 285 |
+
vllm_files: list[Path] = []
|
| 286 |
+
meta_files: list[Path] = []
|
| 287 |
+
for root in roots:
|
| 288 |
+
if root.is_file():
|
| 289 |
+
if root.name == "autoregressive_meta.json":
|
| 290 |
+
meta_files.append(root)
|
| 291 |
+
else:
|
| 292 |
+
vllm_files.append(root)
|
| 293 |
+
continue
|
| 294 |
+
if scope in ("all", "vllm"):
|
| 295 |
+
vllm_files.extend(sorted(root.rglob("vllm_qualitative_outputs.json")))
|
| 296 |
+
if scope in ("all", "autoregressive"):
|
| 297 |
+
meta_files.extend(sorted(root.rglob("autoregressive_meta.json")))
|
| 298 |
+
return vllm_files, meta_files
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def _squash(text: str) -> str:
|
| 302 |
+
return re.sub(r"[^a-z0-9]+", "", text.lower())
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def resolve_model_dirs(root: Path, hf_model: str) -> tuple[list[Path], str]:
|
| 306 |
+
"""Find the autoport directory (or directories) belonging to hf_model.
|
| 307 |
+
|
| 308 |
+
Agents choose their own autoport directory names (observed variants for
|
| 309 |
+
one model include `llama31_8b_instruct` and `meta_llama_Llama_3_1_8B_Instruct`),
|
| 310 |
+
so match on alphanumeric-squashed containment between the directory path
|
| 311 |
+
relative to root and the HF model id, in either direction.
|
| 312 |
+
"""
|
| 313 |
+
if not root.is_dir():
|
| 314 |
+
return [], f"{root} does not exist"
|
| 315 |
+
target = _squash(hf_model)
|
| 316 |
+
markers = ("tt", "doc", "readiness_vllm")
|
| 317 |
+
candidates = [
|
| 318 |
+
path
|
| 319 |
+
for path in sorted(root.rglob("*"))
|
| 320 |
+
if path.is_dir()
|
| 321 |
+
and len(path.relative_to(root).parts) <= 3
|
| 322 |
+
and any((path / marker).is_dir() for marker in markers)
|
| 323 |
+
]
|
| 324 |
+
matches = []
|
| 325 |
+
for path in candidates:
|
| 326 |
+
squashed = _squash(str(path.relative_to(root)))
|
| 327 |
+
if squashed and (squashed in target or target in squashed):
|
| 328 |
+
matches.append(path)
|
| 329 |
+
# Prefer the deepest matching directories; a parent that only matched
|
| 330 |
+
# because its child did adds nothing but duplicate scanning.
|
| 331 |
+
matches = [m for m in matches if not any(other != m and other.is_relative_to(m) for other in matches)]
|
| 332 |
+
if matches:
|
| 333 |
+
return matches, ""
|
| 334 |
+
if candidates:
|
| 335 |
+
listing = ", ".join(str(c.relative_to(root)) for c in candidates)
|
| 336 |
+
return [], f"no autoport directory under {root} matches {hf_model!r} (found: {listing})"
|
| 337 |
+
return [], f"no autoport directories found under {root}"
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
class _Parser(argparse.ArgumentParser):
|
| 341 |
+
def error(self, message: str) -> None: # noqa: D102 - argparse override
|
| 342 |
+
# argparse exits 2 on misuse, which stage gates would misread as a
|
| 343 |
+
# critical model verdict. CLI misuse is a checker/environment fault.
|
| 344 |
+
self.print_usage(sys.stderr)
|
| 345 |
+
print(f"{self.prog}: error: {message}", file=sys.stderr)
|
| 346 |
+
raise SystemExit(3)
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
def main(argv: Sequence[str] | None = None) -> int:
|
| 350 |
+
parser = _Parser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 351 |
+
parser.add_argument(
|
| 352 |
+
"paths",
|
| 353 |
+
nargs="*",
|
| 354 |
+
type=Path,
|
| 355 |
+
help="Artifact files or directories to scan. Default: --model-dir, --hf-model resolution, or --root.",
|
| 356 |
+
)
|
| 357 |
+
parser.add_argument(
|
| 358 |
+
"--root",
|
| 359 |
+
type=Path,
|
| 360 |
+
default=Path("models/autoports"),
|
| 361 |
+
help="Directory scanned when no paths are given (default: models/autoports).",
|
| 362 |
+
)
|
| 363 |
+
parser.add_argument(
|
| 364 |
+
"--model-dir",
|
| 365 |
+
type=Path,
|
| 366 |
+
help="Explicit autoport directory to scope the scan to. Takes precedence over --hf-model.",
|
| 367 |
+
)
|
| 368 |
+
parser.add_argument(
|
| 369 |
+
"--hf-model",
|
| 370 |
+
help="Scope the scan to the autoport directory matching this HF model id (fuzzy fallback when --model-dir is not given).",
|
| 371 |
+
)
|
| 372 |
+
parser.add_argument("--scope", choices=("all", "vllm", "autoregressive"), default="all")
|
| 373 |
+
parser.add_argument(
|
| 374 |
+
"--missing-artifacts",
|
| 375 |
+
choices=("advisory", "critical"),
|
| 376 |
+
default="advisory",
|
| 377 |
+
help=(
|
| 378 |
+
"Severity when no generation artifacts (or no matching model "
|
| 379 |
+
"directory) are found. Stage gates should use 'critical': by "
|
| 380 |
+
"stages 5+, missing generation evidence is a failed completion "
|
| 381 |
+
"requirement, not a soft warning."
|
| 382 |
+
),
|
| 383 |
+
)
|
| 384 |
+
parser.add_argument("--json", type=Path, help="Write the machine-readable report here.")
|
| 385 |
+
args = parser.parse_args(argv)
|
| 386 |
+
|
| 387 |
+
report = Report()
|
| 388 |
+
|
| 389 |
+
if args.paths:
|
| 390 |
+
roots: list[Path] = args.paths
|
| 391 |
+
elif args.model_dir:
|
| 392 |
+
if args.model_dir.is_dir():
|
| 393 |
+
roots = [args.model_dir]
|
| 394 |
+
else:
|
| 395 |
+
roots = []
|
| 396 |
+
report.findings.append(
|
| 397 |
+
Finding(
|
| 398 |
+
severity=args.missing_artifacts,
|
| 399 |
+
artifact=str(args.model_dir),
|
| 400 |
+
label="model directory resolution",
|
| 401 |
+
metric="missing_model_dir",
|
| 402 |
+
value=0.0,
|
| 403 |
+
threshold=1.0,
|
| 404 |
+
detail=(
|
| 405 |
+
f"--model-dir {args.model_dir} does not exist. The stage requires "
|
| 406 |
+
"generation evidence under the model's autoport directory."
|
| 407 |
+
),
|
| 408 |
+
)
|
| 409 |
+
)
|
| 410 |
+
elif args.hf_model:
|
| 411 |
+
roots, why_empty = resolve_model_dirs(args.root, args.hf_model)
|
| 412 |
+
if not roots:
|
| 413 |
+
report.findings.append(
|
| 414 |
+
Finding(
|
| 415 |
+
severity=args.missing_artifacts,
|
| 416 |
+
artifact=str(args.root),
|
| 417 |
+
label="model directory resolution",
|
| 418 |
+
metric="missing_model_dir",
|
| 419 |
+
value=0.0,
|
| 420 |
+
threshold=1.0,
|
| 421 |
+
detail=(
|
| 422 |
+
f"{why_empty}. The stage requires generation evidence under the "
|
| 423 |
+
"model's autoport directory; create it at the documented location."
|
| 424 |
+
),
|
| 425 |
+
)
|
| 426 |
+
)
|
| 427 |
+
else:
|
| 428 |
+
print(f"scoped to: {', '.join(str(r) for r in roots)}")
|
| 429 |
+
else:
|
| 430 |
+
roots = [args.root]
|
| 431 |
+
|
| 432 |
+
vllm_files, meta_files = discover(roots, args.scope)
|
| 433 |
+
|
| 434 |
+
if roots and not vllm_files and not meta_files and not report.findings:
|
| 435 |
+
report.findings.append(
|
| 436 |
+
Finding(
|
| 437 |
+
severity=args.missing_artifacts,
|
| 438 |
+
artifact=", ".join(str(r) for r in roots),
|
| 439 |
+
label="artifact discovery",
|
| 440 |
+
metric="missing_artifacts",
|
| 441 |
+
value=0.0,
|
| 442 |
+
threshold=1.0,
|
| 443 |
+
detail=(
|
| 444 |
+
f"No generation artifacts found (scope={args.scope}). If this stage "
|
| 445 |
+
"requires generation evidence, produce it at the documented location "
|
| 446 |
+
"before reporting complete."
|
| 447 |
+
),
|
| 448 |
+
)
|
| 449 |
+
)
|
| 450 |
+
for path in vllm_files:
|
| 451 |
+
check_vllm_qualitative(report, path)
|
| 452 |
+
for path in meta_files:
|
| 453 |
+
check_autoregressive_meta(report, path)
|
| 454 |
+
|
| 455 |
+
for m in report.measured:
|
| 456 |
+
compact = {k: v for k, v in m.items() if k not in ("artifact", "label")}
|
| 457 |
+
print(f"measured: {m.get('label')} [{m.get('artifact')}] {compact}")
|
| 458 |
+
|
| 459 |
+
if report.findings:
|
| 460 |
+
print(f"\n{len(report.findings)} finding(s):")
|
| 461 |
+
for f in report.findings:
|
| 462 |
+
print(f"\n[{f.severity.upper()}] {f.metric}={f.value} (threshold {f.threshold})")
|
| 463 |
+
print(f" artifact: {f.artifact}")
|
| 464 |
+
print(f" where: {f.label}")
|
| 465 |
+
print(f" {f.detail}")
|
| 466 |
+
else:
|
| 467 |
+
print("\nNo degenerate output detected.")
|
| 468 |
+
|
| 469 |
+
if args.json:
|
| 470 |
+
args.json.parent.mkdir(parents=True, exist_ok=True)
|
| 471 |
+
args.json.write_text(
|
| 472 |
+
json.dumps(
|
| 473 |
+
{
|
| 474 |
+
"findings": [asdict(f) for f in report.findings],
|
| 475 |
+
"measured": report.measured,
|
| 476 |
+
"exit_code": report.exit_code,
|
| 477 |
+
},
|
| 478 |
+
indent=2,
|
| 479 |
+
),
|
| 480 |
+
encoding="utf-8",
|
| 481 |
+
)
|
| 482 |
+
|
| 483 |
+
return report.exit_code
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
if __name__ == "__main__":
|
| 487 |
+
try:
|
| 488 |
+
sys.exit(main())
|
| 489 |
+
except SystemExit:
|
| 490 |
+
raise
|
| 491 |
+
except Exception: # noqa: BLE001 - exit 3 marks checker-internal errors, never a model verdict
|
| 492 |
+
import traceback
|
| 493 |
+
|
| 494 |
+
traceback.print_exc()
|
| 495 |
+
sys.exit(3)
|
code/models/common/readiness_check/run_prefill_check.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
Run prefill-based readiness check against a reference file.
|
| 6 |
+
|
| 7 |
+
Validates TT model prefill accuracy by:
|
| 8 |
+
1. Loading the generator from `<model_dir>/tt/generator.py`
|
| 9 |
+
2. Running `prefill_forward(return_all_logits=True)` on full sequences
|
| 10 |
+
3. Comparing predictions against reference top-K at each position
|
| 11 |
+
4. Reporting top-1, top-5, and top-K accuracy
|
| 12 |
+
|
| 13 |
+
Use alongside `run_teacher_forcing.py` to validate both prefill and decode paths.
|
| 14 |
+
|
| 15 |
+
CLI:
|
| 16 |
+
python -m models.common.readiness_check.run_prefill_check \\
|
| 17 |
+
--model-dir models/autoports/<model_name> \\
|
| 18 |
+
--reference references/<model>.refpt \\
|
| 19 |
+
--mesh-device N150
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import argparse
|
| 25 |
+
import importlib.util
|
| 26 |
+
import sys
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
from typing import Any, Dict, List
|
| 29 |
+
|
| 30 |
+
import torch
|
| 31 |
+
|
| 32 |
+
from models.common.readiness_check.contract import (
|
| 33 |
+
BUILD_GENERATOR_FUNCTION_NAME,
|
| 34 |
+
GENERATOR_MODULE_RELPATH,
|
| 35 |
+
BuildGeneratorFn,
|
| 36 |
+
Generator,
|
| 37 |
+
)
|
| 38 |
+
from models.common.readiness_check.mesh_device import (
|
| 39 |
+
add_mesh_device_args,
|
| 40 |
+
close_readiness_mesh_device,
|
| 41 |
+
open_readiness_mesh_device,
|
| 42 |
+
)
|
| 43 |
+
from models.common.readiness_check.schema import Reference, ReferenceEntry, load_reference
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _import_build_generator(model_dir: Path) -> BuildGeneratorFn:
|
| 47 |
+
"""
|
| 48 |
+
Load `<model_dir>/tt/generator.py` and return its `build_generator` function.
|
| 49 |
+
"""
|
| 50 |
+
generator_path = model_dir / GENERATOR_MODULE_RELPATH
|
| 51 |
+
if not generator_path.exists():
|
| 52 |
+
raise FileNotFoundError(
|
| 53 |
+
f"Expected generator at {generator_path}. The readiness check requires "
|
| 54 |
+
f"<model_dir>/{GENERATOR_MODULE_RELPATH} to exist and expose `{BUILD_GENERATOR_FUNCTION_NAME}`."
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
module_name = f"_readiness_generator_{model_dir.resolve().name}"
|
| 58 |
+
spec = importlib.util.spec_from_file_location(module_name, generator_path)
|
| 59 |
+
if spec is None or spec.loader is None:
|
| 60 |
+
raise ImportError(f"Could not load spec for {generator_path}")
|
| 61 |
+
module = importlib.util.module_from_spec(spec)
|
| 62 |
+
sys.modules[module_name] = module
|
| 63 |
+
spec.loader.exec_module(module)
|
| 64 |
+
|
| 65 |
+
fn = getattr(module, BUILD_GENERATOR_FUNCTION_NAME, None)
|
| 66 |
+
if fn is None or not callable(fn):
|
| 67 |
+
raise AttributeError(
|
| 68 |
+
f"{generator_path} does not export a callable `{BUILD_GENERATOR_FUNCTION_NAME}`. "
|
| 69 |
+
f"See models/common/readiness_check/contract.py."
|
| 70 |
+
)
|
| 71 |
+
return fn # type: ignore[return-value]
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _run_one_entry_prefill(
|
| 75 |
+
*,
|
| 76 |
+
generator: Generator,
|
| 77 |
+
entry: ReferenceEntry,
|
| 78 |
+
reference: Reference,
|
| 79 |
+
) -> Dict[str, Any]:
|
| 80 |
+
"""
|
| 81 |
+
Run batch prefill check for one entry.
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
generator: TT generator instance
|
| 85 |
+
entry: Reference entry with prompt, generated tokens, and top-K predictions
|
| 86 |
+
reference: Reference object (for metadata)
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
Accuracy dict with top1, top5, top100, matches, and total
|
| 90 |
+
"""
|
| 91 |
+
prompt_tokens = entry.prompt_tokens[0].tolist() # [P]
|
| 92 |
+
gen_tokens = entry.generated_tokens[0].tolist() # [G]
|
| 93 |
+
topk_reference = entry.topk_tokens # [G, K]
|
| 94 |
+
|
| 95 |
+
# Concatenate prompt + generated for full sequence
|
| 96 |
+
full_sequence = prompt_tokens + gen_tokens
|
| 97 |
+
prompt_len = len(prompt_tokens)
|
| 98 |
+
gen_len = len(gen_tokens)
|
| 99 |
+
full_len = prompt_len + gen_len
|
| 100 |
+
|
| 101 |
+
# Convert to tensor [1, full_len]
|
| 102 |
+
tokens_tensor = torch.tensor([full_sequence], dtype=torch.long)
|
| 103 |
+
|
| 104 |
+
# Prepare KV cache and page table (implementation-specific)
|
| 105 |
+
# This is a simplified version - real implementations need proper setup
|
| 106 |
+
# For now, we'll call prefill_forward with minimal setup and let the
|
| 107 |
+
# generator handle defaults through **kwargs
|
| 108 |
+
|
| 109 |
+
# Call prefill with return_all_logits=True to get logits at all positions
|
| 110 |
+
# Note: This requires the generator implementation to support return_all_logits
|
| 111 |
+
try:
|
| 112 |
+
# Most generators will need proper page_table and kv_cache setup
|
| 113 |
+
# This is a placeholder - real usage requires model-specific initialization
|
| 114 |
+
import ttnn
|
| 115 |
+
|
| 116 |
+
# Allocate dummy page table and kv cache
|
| 117 |
+
# Real implementations should use generator's initialization methods
|
| 118 |
+
batch_size = 1
|
| 119 |
+
max_blocks = 1024
|
| 120 |
+
page_table = torch.arange(max_blocks).reshape(batch_size, max_blocks)
|
| 121 |
+
page_table_tt = ttnn.from_torch(
|
| 122 |
+
page_table,
|
| 123 |
+
device=generator.mesh_device if hasattr(generator, "mesh_device") else None,
|
| 124 |
+
dtype=ttnn.int32,
|
| 125 |
+
layout=ttnn.ROW_MAJOR_LAYOUT,
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
# Placeholder kv_cache - real implementation needs proper initialization
|
| 129 |
+
kv_cache = None # Generator should handle None gracefully or we need to init properly
|
| 130 |
+
|
| 131 |
+
logits = generator.prefill_forward(
|
| 132 |
+
tokens=tokens_tensor,
|
| 133 |
+
page_table=page_table_tt,
|
| 134 |
+
kv_cache=kv_cache,
|
| 135 |
+
prompt_lens=[full_len],
|
| 136 |
+
return_all_logits=True,
|
| 137 |
+
)
|
| 138 |
+
except TypeError as e:
|
| 139 |
+
if "return_all_logits" in str(e):
|
| 140 |
+
raise NotImplementedError(
|
| 141 |
+
f"Generator {type(generator).__name__} does not support return_all_logits parameter. "
|
| 142 |
+
"Please update the generator to support the new contract."
|
| 143 |
+
) from e
|
| 144 |
+
raise
|
| 145 |
+
|
| 146 |
+
# logits should be [1, full_len, vocab_size]
|
| 147 |
+
if logits.dim() == 2:
|
| 148 |
+
# [batch, vocab] - only got last position, not all positions
|
| 149 |
+
raise RuntimeError(
|
| 150 |
+
f"Generator returned logits with shape {logits.shape}, but return_all_logits=True "
|
| 151 |
+
"should return shape [batch, seq_len, vocab]. Generator may not support return_all_logits."
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
# Extract logits at positions that predict gen_tokens
|
| 155 |
+
# logits[0, i] predicts token at position i+1
|
| 156 |
+
# So logits[0, prompt_len-1:prompt_len+gen_len-1] predicts gen_tokens
|
| 157 |
+
prediction_logits = logits[0, prompt_len - 1 : prompt_len + gen_len - 1, :] # [gen_len, vocab]
|
| 158 |
+
|
| 159 |
+
# Get top-1 predictions (argmax)
|
| 160 |
+
tt_predictions = torch.argmax(prediction_logits, dim=-1).cpu() # [gen_len]
|
| 161 |
+
|
| 162 |
+
# Compare against reference
|
| 163 |
+
matches_top1 = 0
|
| 164 |
+
matches_top5 = 0
|
| 165 |
+
matches_topk = 0
|
| 166 |
+
k_cols = min(5, topk_reference.shape[1])
|
| 167 |
+
|
| 168 |
+
for i in range(gen_len):
|
| 169 |
+
tt_pred = int(tt_predictions[i].item())
|
| 170 |
+
ref_topk = topk_reference[i]
|
| 171 |
+
|
| 172 |
+
if tt_pred == int(ref_topk[0].item()):
|
| 173 |
+
matches_top1 += 1
|
| 174 |
+
if tt_pred in ref_topk[:k_cols].tolist():
|
| 175 |
+
matches_top5 += 1
|
| 176 |
+
if tt_pred in ref_topk.tolist():
|
| 177 |
+
matches_topk += 1
|
| 178 |
+
|
| 179 |
+
return {
|
| 180 |
+
"top1": matches_top1 / gen_len,
|
| 181 |
+
"top5": matches_top5 / gen_len,
|
| 182 |
+
"top100": matches_topk / gen_len,
|
| 183 |
+
"matches_top1": matches_top1,
|
| 184 |
+
"matches_top5": matches_top5,
|
| 185 |
+
"matches_top100": matches_topk,
|
| 186 |
+
"total": gen_len,
|
| 187 |
+
"k": reference.k,
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def _format_row(label: str, stats: Dict[str, Any]) -> str:
|
| 192 |
+
return (
|
| 193 |
+
f"{label:<20} "
|
| 194 |
+
f"top1={stats['top1']:.3f} ({stats['matches_top1']}/{stats['total']}) "
|
| 195 |
+
f"top5={stats['top5']:.3f} ({stats['matches_top5']}/{stats['total']}) "
|
| 196 |
+
f"top{stats['k']}={stats['top100']:.3f} ({stats['matches_top100']}/{stats['total']})"
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def run_prefill_check(
|
| 201 |
+
*,
|
| 202 |
+
model_dir: Path,
|
| 203 |
+
reference_path: Path,
|
| 204 |
+
mesh_device,
|
| 205 |
+
build_kwargs: Dict[str, Any] | None = None,
|
| 206 |
+
) -> List[Dict[str, Any]]:
|
| 207 |
+
"""
|
| 208 |
+
Programmatic entry point. Builds the generator, runs batch prefill
|
| 209 |
+
over all reference entries, and returns the per-entry accuracy dicts.
|
| 210 |
+
"""
|
| 211 |
+
build_kwargs = build_kwargs or {}
|
| 212 |
+
build_generator = _import_build_generator(model_dir)
|
| 213 |
+
generator: Generator = build_generator(model_dir=model_dir, mesh_device=mesh_device, **build_kwargs)
|
| 214 |
+
|
| 215 |
+
reference = load_reference(reference_path)
|
| 216 |
+
per_entry: List[Dict[str, Any]] = []
|
| 217 |
+
|
| 218 |
+
try:
|
| 219 |
+
for entry_idx, entry in enumerate(reference.entries):
|
| 220 |
+
if entry_idx > 0:
|
| 221 |
+
generator.reset()
|
| 222 |
+
stats = _run_one_entry_prefill(generator=generator, entry=entry, reference=reference)
|
| 223 |
+
per_entry.append(stats)
|
| 224 |
+
print(_format_row(f"entry[{entry_idx}]", stats))
|
| 225 |
+
finally:
|
| 226 |
+
teardown = getattr(generator, "teardown", None)
|
| 227 |
+
if callable(teardown):
|
| 228 |
+
teardown()
|
| 229 |
+
|
| 230 |
+
total = sum(s["total"] for s in per_entry)
|
| 231 |
+
if total:
|
| 232 |
+
agg = {
|
| 233 |
+
"top1": sum(s["matches_top1"] for s in per_entry) / total,
|
| 234 |
+
"top5": sum(s["matches_top5"] for s in per_entry) / total,
|
| 235 |
+
"top100": sum(s["matches_top100"] for s in per_entry) / total,
|
| 236 |
+
"matches_top1": sum(s["matches_top1"] for s in per_entry),
|
| 237 |
+
"matches_top5": sum(s["matches_top5"] for s in per_entry),
|
| 238 |
+
"matches_top100": sum(s["matches_top100"] for s in per_entry),
|
| 239 |
+
"total": total,
|
| 240 |
+
"k": per_entry[0]["k"],
|
| 241 |
+
}
|
| 242 |
+
print(_format_row("AGGREGATE", agg))
|
| 243 |
+
|
| 244 |
+
return per_entry
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def _main() -> None:
|
| 248 |
+
parser = argparse.ArgumentParser(description="Run the batch prefill readiness check against a reference file.")
|
| 249 |
+
parser.add_argument("--model-dir", type=Path, required=True, help="Path to the model directory.")
|
| 250 |
+
parser.add_argument("--reference", type=Path, required=True, help="Path to the .refpt reference file.")
|
| 251 |
+
add_mesh_device_args(parser)
|
| 252 |
+
args = parser.parse_args()
|
| 253 |
+
|
| 254 |
+
mesh_device = open_readiness_mesh_device(args.mesh_device, args.fabric_config)
|
| 255 |
+
try:
|
| 256 |
+
run_prefill_check(
|
| 257 |
+
model_dir=args.model_dir.resolve(),
|
| 258 |
+
reference_path=args.reference.resolve(),
|
| 259 |
+
mesh_device=mesh_device,
|
| 260 |
+
)
|
| 261 |
+
finally:
|
| 262 |
+
close_readiness_mesh_device(mesh_device, args.fabric_config)
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
if __name__ == "__main__":
|
| 266 |
+
_main()
|
code/models/common/readiness_check/schema.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
Reference file schema for the model-readiness check.
|
| 6 |
+
|
| 7 |
+
A reference file pins, for one or more prompts, the HuggingFace teacher's
|
| 8 |
+
top-K next-token predictions at every generated position. The TT model
|
| 9 |
+
under test is then run with teacher forcing against this file and scored
|
| 10 |
+
on top-1 / top-5 / top-K hit rate.
|
| 11 |
+
|
| 12 |
+
On-disk format (single torch.save'd dict):
|
| 13 |
+
|
| 14 |
+
{
|
| 15 |
+
"format_version": "readiness_v1",
|
| 16 |
+
"k": int,
|
| 17 |
+
"hf_model_id": str,
|
| 18 |
+
"token_ids_meta": {"bos_id": int|None, "eos_id": int, "pad_id": int|None},
|
| 19 |
+
"entries": [
|
| 20 |
+
{
|
| 21 |
+
"prompt_text": str,
|
| 22 |
+
"prompt_tokens": Tensor [1, P] int64,
|
| 23 |
+
"generated_tokens": Tensor [1, G] int64,
|
| 24 |
+
"topk_tokens": Tensor [G, K] int32,
|
| 25 |
+
"tf_prompt_len": int, # == P, stored for clarity
|
| 26 |
+
},
|
| 27 |
+
...
|
| 28 |
+
],
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
`topk_tokens[i]` is the teacher's top-K prediction for the token at
|
| 32 |
+
position `tf_prompt_len + i`, conditioned on positions `[0 .. tf_prompt_len + i - 1]`.
|
| 33 |
+
Top-1 is column 0; top-5 is columns 0..4; top-K is the full row.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
from __future__ import annotations
|
| 37 |
+
|
| 38 |
+
from dataclasses import dataclass, field
|
| 39 |
+
from pathlib import Path
|
| 40 |
+
from typing import Any, Dict, List, Optional
|
| 41 |
+
|
| 42 |
+
import torch
|
| 43 |
+
|
| 44 |
+
FORMAT_VERSION = "readiness_v1"
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@dataclass
|
| 48 |
+
class ReferenceEntry:
|
| 49 |
+
prompt_text: str
|
| 50 |
+
prompt_tokens: torch.Tensor # [1, P] int64
|
| 51 |
+
generated_tokens: torch.Tensor # [1, G] int64
|
| 52 |
+
topk_tokens: torch.Tensor # [G, K] int32
|
| 53 |
+
tf_prompt_len: int
|
| 54 |
+
|
| 55 |
+
def __post_init__(self) -> None:
|
| 56 |
+
if self.prompt_tokens.dim() != 2 or self.prompt_tokens.shape[0] != 1:
|
| 57 |
+
raise ValueError(f"prompt_tokens must be [1, P], got {tuple(self.prompt_tokens.shape)}")
|
| 58 |
+
if self.generated_tokens.dim() != 2 or self.generated_tokens.shape[0] != 1:
|
| 59 |
+
raise ValueError(f"generated_tokens must be [1, G], got {tuple(self.generated_tokens.shape)}")
|
| 60 |
+
if self.topk_tokens.dim() != 2:
|
| 61 |
+
raise ValueError(f"topk_tokens must be [G, K], got {tuple(self.topk_tokens.shape)}")
|
| 62 |
+
if self.topk_tokens.shape[0] != self.generated_tokens.shape[1]:
|
| 63 |
+
raise ValueError(
|
| 64 |
+
f"topk_tokens rows ({self.topk_tokens.shape[0]}) must equal "
|
| 65 |
+
f"generated_tokens length ({self.generated_tokens.shape[1]})"
|
| 66 |
+
)
|
| 67 |
+
if self.tf_prompt_len != self.prompt_tokens.shape[1]:
|
| 68 |
+
raise ValueError(
|
| 69 |
+
f"tf_prompt_len ({self.tf_prompt_len}) must equal prompt length " f"({self.prompt_tokens.shape[1]})"
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
@property
|
| 73 |
+
def num_generated(self) -> int:
|
| 74 |
+
return int(self.generated_tokens.shape[1])
|
| 75 |
+
|
| 76 |
+
@property
|
| 77 |
+
def k(self) -> int:
|
| 78 |
+
return int(self.topk_tokens.shape[1])
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
@dataclass
|
| 82 |
+
class Reference:
|
| 83 |
+
k: int
|
| 84 |
+
hf_model_id: str
|
| 85 |
+
entries: List[ReferenceEntry]
|
| 86 |
+
token_ids_meta: Dict[str, Optional[int]] = field(default_factory=dict)
|
| 87 |
+
|
| 88 |
+
def __post_init__(self) -> None:
|
| 89 |
+
if not self.entries:
|
| 90 |
+
raise ValueError("Reference must contain at least one entry")
|
| 91 |
+
for idx, entry in enumerate(self.entries):
|
| 92 |
+
if entry.k != self.k:
|
| 93 |
+
raise ValueError(f"Entry {idx} has k={entry.k}, expected {self.k} from Reference.k")
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def save_reference(reference: Reference, path: Path | str) -> Path:
|
| 97 |
+
path = Path(path)
|
| 98 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 99 |
+
payload: Dict[str, Any] = {
|
| 100 |
+
"format_version": FORMAT_VERSION,
|
| 101 |
+
"k": reference.k,
|
| 102 |
+
"hf_model_id": reference.hf_model_id,
|
| 103 |
+
"token_ids_meta": reference.token_ids_meta,
|
| 104 |
+
"entries": [
|
| 105 |
+
{
|
| 106 |
+
"prompt_text": e.prompt_text,
|
| 107 |
+
"prompt_tokens": e.prompt_tokens.to(torch.int64).cpu().contiguous(),
|
| 108 |
+
"generated_tokens": e.generated_tokens.to(torch.int64).cpu().contiguous(),
|
| 109 |
+
"topk_tokens": e.topk_tokens.to(torch.int32).cpu().contiguous(),
|
| 110 |
+
"tf_prompt_len": int(e.tf_prompt_len),
|
| 111 |
+
}
|
| 112 |
+
for e in reference.entries
|
| 113 |
+
],
|
| 114 |
+
}
|
| 115 |
+
torch.save(payload, path)
|
| 116 |
+
return path
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def load_reference(path: Path | str) -> Reference:
|
| 120 |
+
path = Path(path)
|
| 121 |
+
payload = torch.load(path, weights_only=False)
|
| 122 |
+
fmt = payload.get("format_version")
|
| 123 |
+
if fmt != FORMAT_VERSION:
|
| 124 |
+
raise ValueError(f"Unsupported reference format_version={fmt!r} in {path}; " f"expected {FORMAT_VERSION!r}")
|
| 125 |
+
raw_entries = payload.get("entries")
|
| 126 |
+
if not isinstance(raw_entries, list) or not raw_entries:
|
| 127 |
+
raise ValueError(f"Reference {path} missing non-empty 'entries' list")
|
| 128 |
+
entries = [
|
| 129 |
+
ReferenceEntry(
|
| 130 |
+
prompt_text=str(e["prompt_text"]),
|
| 131 |
+
prompt_tokens=e["prompt_tokens"],
|
| 132 |
+
generated_tokens=e["generated_tokens"],
|
| 133 |
+
topk_tokens=e["topk_tokens"],
|
| 134 |
+
tf_prompt_len=int(e["tf_prompt_len"]),
|
| 135 |
+
)
|
| 136 |
+
for e in raw_entries
|
| 137 |
+
]
|
| 138 |
+
return Reference(
|
| 139 |
+
k=int(payload["k"]),
|
| 140 |
+
hf_model_id=str(payload["hf_model_id"]),
|
| 141 |
+
entries=entries,
|
| 142 |
+
token_ids_meta=dict(payload.get("token_ids_meta") or {}),
|
| 143 |
+
)
|
code/models/common/readiness_check/teacher_forcing.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
Teacher-forcing accuracy harness driven by a `readiness_v1` reference file.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
|
| 9 |
+
acc = TokenAccuracy("llama31_8b.refpt")
|
| 10 |
+
prompt_ids = acc.get_prompt_token_ids(user_idx=0)
|
| 11 |
+
|
| 12 |
+
# Prefill the TT model with prompt_ids, then decode one token at a time:
|
| 13 |
+
for _ in range(acc.num_gt_tokens(user_idx=0)):
|
| 14 |
+
tt_pred = run_tt_decode(...) # int token id
|
| 15 |
+
forced_next = acc.collect_predicted_tokens(tt_pred, user_idx=0)
|
| 16 |
+
feed_token_to_tt(forced_next) # teacher forcing
|
| 17 |
+
|
| 18 |
+
print(acc.compute_accuracy(user_idx=0))
|
| 19 |
+
# {'top1': ..., 'top5': ..., 'top100': ..., 'matches_top1': ..., ...}
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
from typing import Dict, List, Optional
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
|
| 29 |
+
from models.common.readiness_check.schema import Reference, load_reference
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class TokenAccuracy:
|
| 33 |
+
"""
|
| 34 |
+
Per-user teacher-forcing helper backed by a `readiness_v1` reference file.
|
| 35 |
+
|
| 36 |
+
Each `entry` in the reference corresponds to one prompt and is exposed as
|
| 37 |
+
a separate "user." For multi-user vLLM-style decode, callers pass
|
| 38 |
+
`user_idx` to route per-user state.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
def __init__(self, reference: str | Path | Reference) -> None:
|
| 42 |
+
if isinstance(reference, Reference):
|
| 43 |
+
self.reference = reference
|
| 44 |
+
else:
|
| 45 |
+
self.reference = load_reference(reference)
|
| 46 |
+
|
| 47 |
+
self.entry_count = len(self.reference.entries)
|
| 48 |
+
self.k = self.reference.k
|
| 49 |
+
|
| 50 |
+
# Flattened per-user views.
|
| 51 |
+
self.prompt_1d: List[torch.Tensor] = [
|
| 52 |
+
e.prompt_tokens[0].to(torch.long).contiguous() for e in self.reference.entries
|
| 53 |
+
]
|
| 54 |
+
self.gt_gen_1d: List[torch.Tensor] = [
|
| 55 |
+
e.generated_tokens[0].to(torch.long).contiguous() for e in self.reference.entries
|
| 56 |
+
]
|
| 57 |
+
self.topk: List[torch.Tensor] = [e.topk_tokens.to(torch.long).contiguous() for e in self.reference.entries]
|
| 58 |
+
self.tf_prompt_len: List[int] = [int(e.tf_prompt_len) for e in self.reference.entries]
|
| 59 |
+
|
| 60 |
+
meta = self.reference.token_ids_meta or {}
|
| 61 |
+
self.eos_id: Optional[int] = int(meta["eos_id"]) if meta.get("eos_id") is not None else None
|
| 62 |
+
|
| 63 |
+
# Per-user runtime state.
|
| 64 |
+
self.pred_tokens: List[List[int]] = [[] for _ in range(self.entry_count)]
|
| 65 |
+
self.cursor: List[int] = [0] * self.entry_count
|
| 66 |
+
|
| 67 |
+
# Bounds + reset.
|
| 68 |
+
|
| 69 |
+
@property
|
| 70 |
+
def num_entries(self) -> int:
|
| 71 |
+
return self.entry_count
|
| 72 |
+
|
| 73 |
+
def _check_idx(self, user_idx: int) -> int:
|
| 74 |
+
idx = int(user_idx)
|
| 75 |
+
if idx < 0 or idx >= self.entry_count:
|
| 76 |
+
raise IndexError(f"user_idx={idx} out of range for {self.entry_count} entry(ies)")
|
| 77 |
+
return idx
|
| 78 |
+
|
| 79 |
+
def reset(self) -> None:
|
| 80 |
+
for buf in self.pred_tokens:
|
| 81 |
+
buf.clear()
|
| 82 |
+
for i in range(self.entry_count):
|
| 83 |
+
self.cursor[i] = 0
|
| 84 |
+
|
| 85 |
+
# Accessors.
|
| 86 |
+
|
| 87 |
+
def get_prompt_token_ids(self, user_idx: int = 0) -> List[int]:
|
| 88 |
+
return self.prompt_1d[self._check_idx(user_idx)].tolist()
|
| 89 |
+
|
| 90 |
+
def num_gt_tokens(self, user_idx: int = 0) -> int:
|
| 91 |
+
return int(self.gt_gen_1d[self._check_idx(user_idx)].numel())
|
| 92 |
+
|
| 93 |
+
def num_pred_tokens(self, user_idx: int = 0) -> int:
|
| 94 |
+
return len(self.pred_tokens[self._check_idx(user_idx)])
|
| 95 |
+
|
| 96 |
+
def get_predicted_tokens(self, user_idx: int = 0) -> List[int]:
|
| 97 |
+
return list(self.pred_tokens[self._check_idx(user_idx)])
|
| 98 |
+
|
| 99 |
+
# Core teacher-forcing interface.
|
| 100 |
+
|
| 101 |
+
def collect_predicted_tokens(self, tt_pred_token: int, *, user_idx: int = 0) -> int:
|
| 102 |
+
"""
|
| 103 |
+
Record TT's predicted token for the next generated position of
|
| 104 |
+
`user_idx` and return the ground-truth token to force into the
|
| 105 |
+
next TT decode step.
|
| 106 |
+
|
| 107 |
+
Once the ground-truth sequence is exhausted, returns `eos_id` if
|
| 108 |
+
known, else the last ground-truth token.
|
| 109 |
+
"""
|
| 110 |
+
idx = self._check_idx(user_idx)
|
| 111 |
+
gt_gen = self.gt_gen_1d[idx]
|
| 112 |
+
cursor = self.cursor[idx]
|
| 113 |
+
|
| 114 |
+
self.pred_tokens[idx].append(int(tt_pred_token))
|
| 115 |
+
|
| 116 |
+
if cursor >= int(gt_gen.numel()):
|
| 117 |
+
if self.eos_id is not None:
|
| 118 |
+
return int(self.eos_id)
|
| 119 |
+
return int(gt_gen[-1].item())
|
| 120 |
+
|
| 121 |
+
forced = int(gt_gen[cursor].item())
|
| 122 |
+
self.cursor[idx] = cursor + 1
|
| 123 |
+
return forced
|
| 124 |
+
|
| 125 |
+
def compute_accuracy(self, user_idx: int = 0) -> Dict[str, float]:
|
| 126 |
+
"""
|
| 127 |
+
Top-1 / top-5 / top-K hit rate for `user_idx`:
|
| 128 |
+
|
| 129 |
+
For generated step i:
|
| 130 |
+
- top-1 = topk_tokens[i, 0]
|
| 131 |
+
- top-5 = topk_tokens[i, 0:5]
|
| 132 |
+
- top-K = topk_tokens[i, :]
|
| 133 |
+
|
| 134 |
+
Returns a dict with float ratios and integer counts:
|
| 135 |
+
{top1, top5, top100, matches_top1, matches_top5, matches_top100, total}
|
| 136 |
+
(`top100` is named for the K=100 default; if the reference has a
|
| 137 |
+
different K, it is still keyed `top100` for consistency. `k` is
|
| 138 |
+
also returned so callers can verify.)
|
| 139 |
+
"""
|
| 140 |
+
idx = self._check_idx(user_idx)
|
| 141 |
+
preds = self.pred_tokens[idx]
|
| 142 |
+
gt_gen = self.gt_gen_1d[idx]
|
| 143 |
+
topk = self.topk[idx]
|
| 144 |
+
|
| 145 |
+
total = min(len(preds), int(gt_gen.numel()))
|
| 146 |
+
if total == 0:
|
| 147 |
+
return {
|
| 148 |
+
"top1": 0.0,
|
| 149 |
+
"top5": 0.0,
|
| 150 |
+
"top100": 0.0,
|
| 151 |
+
"matches_top1": 0,
|
| 152 |
+
"matches_top5": 0,
|
| 153 |
+
"matches_top100": 0,
|
| 154 |
+
"total": 0,
|
| 155 |
+
"k": self.k,
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
matches_top1 = 0
|
| 159 |
+
matches_top5 = 0
|
| 160 |
+
matches_topk = 0
|
| 161 |
+
k_cols = min(5, topk.shape[1])
|
| 162 |
+
|
| 163 |
+
for i in range(total):
|
| 164 |
+
row = topk[i]
|
| 165 |
+
tt_pred = int(preds[i])
|
| 166 |
+
if tt_pred == int(row[0].item()):
|
| 167 |
+
matches_top1 += 1
|
| 168 |
+
if tt_pred in row[:k_cols].tolist():
|
| 169 |
+
matches_top5 += 1
|
| 170 |
+
if tt_pred in row.tolist():
|
| 171 |
+
matches_topk += 1
|
| 172 |
+
|
| 173 |
+
return {
|
| 174 |
+
"top1": matches_top1 / total,
|
| 175 |
+
"top5": matches_top5 / total,
|
| 176 |
+
"top100": matches_topk / total,
|
| 177 |
+
"matches_top1": matches_top1,
|
| 178 |
+
"matches_top5": matches_top5,
|
| 179 |
+
"matches_top100": matches_topk,
|
| 180 |
+
"total": total,
|
| 181 |
+
"k": self.k,
|
| 182 |
+
}
|
code/models/common/readiness_check/vllm_prompts.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Write a haiku about machine learning.
|
| 2 |
+
|
| 3 |
+
Explain the difference between supervised and unsupervised learning in simple terms.
|
| 4 |
+
|
| 5 |
+
Complete this story: Once upon a time, in a faraway kingdom, there lived a curious young inventor who discovered
|
| 6 |
+
|
| 7 |
+
What are the three laws of thermodynamics?
|
| 8 |
+
|
| 9 |
+
Translate the following to French: "Hello, how are you today?"
|
| 10 |
+
|
| 11 |
+
Write a Python function to calculate the Fibonacci sequence.
|
code/models/common/rmsnorm.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.tt_transformers.tt.common import Mode
|
| 7 |
+
|
| 8 |
+
TILE = 32
|
| 9 |
+
SHARD_HEIGHT = TILE # Current ttnn.rms_norm implementation requires shard height to be a single tile
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class RMSNorm(LightweightModule):
|
| 13 |
+
"""
|
| 14 |
+
RMSNorm supporting replication over a MeshDevice and sharding within devices.
|
| 15 |
+
|
| 16 |
+
This class implements a Root Mean Square Normalization (RMSNorm) that can be
|
| 17 |
+
distributed across multiple devices and cores. If the `device` parameter is a
|
| 18 |
+
MeshDevice, the weights and computations are replicated across all devices in
|
| 19 |
+
the mesh. Expects an interleaved input tensor, can optionally output a sharded tensor.
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
device: The device or MeshDevice on which to perform the computations.
|
| 23 |
+
state_dict: The state dictionary containing the model parameters.
|
| 24 |
+
dim: Input dimension (e.g. model hidden dimension size).
|
| 25 |
+
layer_num: The layer number to determine the weight key in the state dictionary.
|
| 26 |
+
weight_key: The key for retrieving the weight from the state dictionary.
|
| 27 |
+
weight_cache_path: Optional path for caching the tilized weights.
|
| 28 |
+
weight_memory_config: Configuration for the weight memory, default is DRAM_MEMORY_CONFIG.
|
| 29 |
+
weight_dtype: The data type for the tensors, bfp8_b hits >0.999 PCC in the models we tested.
|
| 30 |
+
model_config: Optional configuration dictionary for the model.
|
| 31 |
+
eps (float): Small value to avoid division by zero in normalization, default is 1e-05.
|
| 32 |
+
|
| 33 |
+
If model_config is provided, it must specify SHARDED_NORM_INPUT_MEMCFG, SHARDED_NORM_PRGM_CFG
|
| 34 |
+
and SHARDED_NORM_OUTPUT_MEMCFG. If not provided, default configurations will be generated.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
def __init__(
|
| 38 |
+
self,
|
| 39 |
+
device,
|
| 40 |
+
dim,
|
| 41 |
+
state_dict,
|
| 42 |
+
weight_key,
|
| 43 |
+
layer_num=None,
|
| 44 |
+
state_dict_prefix=None,
|
| 45 |
+
weight_cache_path=None,
|
| 46 |
+
weight_memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 47 |
+
weight_dtype=ttnn.bfloat16,
|
| 48 |
+
is_distributed=None,
|
| 49 |
+
eps: float = 1e-05,
|
| 50 |
+
add_unit_offset=False,
|
| 51 |
+
sharded_program_config=None,
|
| 52 |
+
sharded_output_config=None,
|
| 53 |
+
output_mem_config=None,
|
| 54 |
+
ccl_topology=ttnn.Topology.Ring,
|
| 55 |
+
tt_ccl=None,
|
| 56 |
+
fp32_dest_acc_en=True,
|
| 57 |
+
):
|
| 58 |
+
super().__init__()
|
| 59 |
+
self.device = device
|
| 60 |
+
self.eps = eps
|
| 61 |
+
self.is_distributed = is_distributed
|
| 62 |
+
self.ccl_topology = ccl_topology
|
| 63 |
+
self.tt_ccl = tt_ccl
|
| 64 |
+
|
| 65 |
+
if state_dict_prefix:
|
| 66 |
+
weight_name = f"{state_dict_prefix}{weight_key}.weight"
|
| 67 |
+
else:
|
| 68 |
+
if layer_num is None:
|
| 69 |
+
weight_name = f"{weight_key}.weight"
|
| 70 |
+
else:
|
| 71 |
+
weight_name = f"layers.{layer_num}.{weight_key}.weight"
|
| 72 |
+
|
| 73 |
+
torch_weight = (
|
| 74 |
+
state_dict[weight_name].unsqueeze(0).view(1, 1, dim).reshape([1, 1, dim // SHARD_HEIGHT, SHARD_HEIGHT])
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
# Add offset before caching
|
| 78 |
+
if add_unit_offset:
|
| 79 |
+
torch_weight = torch_weight + 1.0
|
| 80 |
+
|
| 81 |
+
# Compatibility with models that don't use mesh devices (e.g. single-chip Mistral-7b)
|
| 82 |
+
is_mesh_device = device.__class__.__name__ == "MeshDevice"
|
| 83 |
+
|
| 84 |
+
self.weight = ttnn.as_tensor(
|
| 85 |
+
torch_weight,
|
| 86 |
+
device=device,
|
| 87 |
+
dtype=weight_dtype,
|
| 88 |
+
layout=ttnn.ROW_MAJOR_LAYOUT,
|
| 89 |
+
memory_config=weight_memory_config,
|
| 90 |
+
cache_file_name=None if weight_cache_path is None else weight_cache_path / weight_name,
|
| 91 |
+
mesh_mapper=ttnn.ReplicateTensorToMesh(device) if is_mesh_device else None,
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
if self.is_distributed:
|
| 95 |
+
self.weight_distributed = ttnn.as_tensor(
|
| 96 |
+
torch_weight,
|
| 97 |
+
device=device,
|
| 98 |
+
dtype=weight_dtype,
|
| 99 |
+
layout=ttnn.ROW_MAJOR_LAYOUT,
|
| 100 |
+
memory_config=weight_memory_config,
|
| 101 |
+
cache_file_name=(
|
| 102 |
+
None if weight_cache_path is None else weight_cache_path / (weight_name + "_distributed")
|
| 103 |
+
),
|
| 104 |
+
mesh_mapper=(
|
| 105 |
+
ttnn.ShardTensor2dMesh(device, dims=(None, 2), mesh_shape=list(device.shape))
|
| 106 |
+
if is_mesh_device
|
| 107 |
+
else None
|
| 108 |
+
),
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
self.sharded_output_config = sharded_output_config
|
| 112 |
+
self.sharded_program_config = sharded_program_config
|
| 113 |
+
self.output_mem_config = output_mem_config
|
| 114 |
+
|
| 115 |
+
self.compute_kernel_config_hifi2 = ttnn.WormholeComputeKernelConfig(
|
| 116 |
+
math_fidelity=ttnn.MathFidelity.HiFi2,
|
| 117 |
+
math_approx_mode=False,
|
| 118 |
+
fp32_dest_acc_en=fp32_dest_acc_en,
|
| 119 |
+
packer_l1_acc=True,
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
def forward(
|
| 123 |
+
self,
|
| 124 |
+
x: ttnn.Tensor,
|
| 125 |
+
mode: Mode | str,
|
| 126 |
+
in_sharded=False,
|
| 127 |
+
out_sharded=False,
|
| 128 |
+
norm_config=None,
|
| 129 |
+
) -> ttnn.Tensor:
|
| 130 |
+
if isinstance(mode, str):
|
| 131 |
+
try:
|
| 132 |
+
mode = Mode(mode)
|
| 133 |
+
except ValueError:
|
| 134 |
+
raise ValueError(f"Invalid mode: {mode}")
|
| 135 |
+
elif not isinstance(mode, Mode):
|
| 136 |
+
raise ValueError(f"Invalid mode: {mode}")
|
| 137 |
+
|
| 138 |
+
sharded_program_config = norm_config.get("sharded_program_config") if norm_config else None
|
| 139 |
+
sharded_output_config = norm_config.get("sharded_output_config") if norm_config else None
|
| 140 |
+
output_mem_config = norm_config.get("output_mem_config") if norm_config else None
|
| 141 |
+
|
| 142 |
+
# If input is sharded do sharded RMSNorm and optionally return sharded output
|
| 143 |
+
program_config = sharded_program_config if in_sharded else None
|
| 144 |
+
memory_config = sharded_output_config if out_sharded else None
|
| 145 |
+
distributed = self.is_distributed and self.is_distributed(mode)
|
| 146 |
+
norm = self._distributed_rmsnorm if distributed else ttnn.rms_norm
|
| 147 |
+
weight = self.weight_distributed if distributed else self.weight
|
| 148 |
+
|
| 149 |
+
if in_sharded:
|
| 150 |
+
assert not distributed, "Distributed RMSNorm does not support sharded inputs"
|
| 151 |
+
else:
|
| 152 |
+
assert not out_sharded, "Non-sharded version of RMSNorm cannot output a sharded tensor"
|
| 153 |
+
|
| 154 |
+
x = norm(
|
| 155 |
+
x,
|
| 156 |
+
epsilon=self.eps,
|
| 157 |
+
weight=weight,
|
| 158 |
+
program_config=program_config,
|
| 159 |
+
memory_config=memory_config,
|
| 160 |
+
compute_kernel_config=self.compute_kernel_config_hifi2,
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
if in_sharded and not out_sharded:
|
| 164 |
+
return ttnn.sharded_to_interleaved(x)
|
| 165 |
+
else:
|
| 166 |
+
if output_mem_config is not None:
|
| 167 |
+
x = ttnn.to_memory_config(x, output_mem_config)
|
| 168 |
+
return x
|
| 169 |
+
|
| 170 |
+
def _distributed_rmsnorm(
|
| 171 |
+
self, inp, epsilon=None, weight=None, program_config=None, memory_config=None, compute_kernel_config=None
|
| 172 |
+
):
|
| 173 |
+
assert program_config is None, "Distributed RMSNorm does not support sharded inputs"
|
| 174 |
+
assert memory_config is None, "Distributed RMSNorm does not support sharded outputs"
|
| 175 |
+
assert self.tt_ccl is not None, "Distributed RMSNorm requires tt_ccl"
|
| 176 |
+
|
| 177 |
+
# Run distributed rmsnorm part 1
|
| 178 |
+
tt_stats = ttnn.rms_norm_pre_all_gather(inp, compute_kernel_config=compute_kernel_config, dtype=ttnn.bfloat16)
|
| 179 |
+
# AllGather stats
|
| 180 |
+
tt_stats = ttnn.experimental.all_gather_async(
|
| 181 |
+
tt_stats,
|
| 182 |
+
persistent_output_buffer=None,
|
| 183 |
+
dim=3,
|
| 184 |
+
multi_device_global_semaphore=self.tt_ccl.get_and_cycle_ag_semaphore_handles(),
|
| 185 |
+
num_links=1,
|
| 186 |
+
topology=self.ccl_topology,
|
| 187 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 188 |
+
barrier_semaphore=self.tt_ccl.get_and_cycle_barrier_semaphore_handle(),
|
| 189 |
+
chunks_per_sync=10,
|
| 190 |
+
num_workers_per_link=2,
|
| 191 |
+
num_buffers_per_channel=2,
|
| 192 |
+
)
|
| 193 |
+
# Run distributed rmsnorm part 2
|
| 194 |
+
tt_out = ttnn.rms_norm_post_all_gather(
|
| 195 |
+
inp,
|
| 196 |
+
tt_stats,
|
| 197 |
+
epsilon=epsilon,
|
| 198 |
+
weight=weight,
|
| 199 |
+
compute_kernel_config=compute_kernel_config,
|
| 200 |
+
)
|
| 201 |
+
tt_stats.deallocate(True)
|
| 202 |
+
|
| 203 |
+
return tt_out
|
code/models/common/sampling/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Sampling Module Overview
|
| 2 |
+
|
| 3 |
+
The `models.common.sampling` package bundles everything needed to run on-device
|
| 4 |
+
sampling (top-k / top-p / temperature/ seed) plus presence/frequency/repetition
|
| 5 |
+
penalties with optional trace capture.
|
| 6 |
+
|
| 7 |
+
## Key Components
|
| 8 |
+
- `SamplingGenerator`: high-level class that owns both `TTSampling` and
|
| 9 |
+
`TTPenalties`, exposes helper methods to reset sampling parameters, penalties,
|
| 10 |
+
prompt/output state, and to run sampling with or without trace capture.
|
| 11 |
+
- `format_sampling_params`: utility that pads/clamps sampling parameters to the
|
| 12 |
+
hardware-friendly layout expected by `TTSampling`.
|
| 13 |
+
- `LogProbsCalculator`: computes per-token log-probabilities across a sharded
|
| 14 |
+
vocabulary using numerically stable log-softmax (global max / sum-exp
|
| 15 |
+
reduction across devices).
|
| 16 |
+
|
| 17 |
+
## Quick Start
|
| 18 |
+
```python
|
| 19 |
+
from models.common.sampling import SamplingGenerator, format_sampling_params
|
| 20 |
+
|
| 21 |
+
sampling = SamplingGenerator(args=args, mesh_device=mesh_device, tt_ccl=tt_ccl)
|
| 22 |
+
|
| 23 |
+
params = format_sampling_params(user_params, max_batch_size=32)
|
| 24 |
+
sampling.reset_sampling_params(params)
|
| 25 |
+
|
| 26 |
+
sampling.reset_seed(seed)
|
| 27 |
+
|
| 28 |
+
sampling.reset_prompt_tokens(prompt_tokens) # torch tensor shaped [B, S]
|
| 29 |
+
sampling.reset_output_state(output_tokens)
|
| 30 |
+
|
| 31 |
+
tt_tokens = sampling.sample(
|
| 32 |
+
tt_logits,
|
| 33 |
+
tt_out_tok=tt_out_buffer,
|
| 34 |
+
)
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
`SamplingGenerator.sample()` accepts `enable_trace=True` to record/replay
|
| 38 |
+
sampling traces.
|
| 39 |
+
|
| 40 |
+
## File Map
|
| 41 |
+
|
| 42 |
+
| File | Purpose |
|
| 43 |
+
|---|---|
|
| 44 |
+
| `generator.py` | `SamplingGenerator` orchestrator; `SamplingParams`; `format_sampling_params`; `broadcast_sampling_params`; `chunk_sampling_params`; `SeedManager` |
|
| 45 |
+
| `tt_sampling.py` | `TTSampling` — on-device top-k/top-p/temp with multi-device all-gather |
|
| 46 |
+
| `tt_penalties.py` | `TTPenalties` — presence / frequency / repetition penalties |
|
| 47 |
+
| `tt_log_probs.py` | `LogProbsCalculator` — log-softmax across sharded vocabulary |
|
| 48 |
+
| `_utils.py` | Shared helpers: `clamp`, `is_default_value`, `filter_none`, `split_list` |
|
| 49 |
+
|
| 50 |
+
## Required `args` Attributes
|
| 51 |
+
|
| 52 |
+
```python
|
| 53 |
+
vocab_size: int # actual vocabulary size (unpadded)
|
| 54 |
+
cluster_shape: tuple # (rows, cols) of the device mesh, e.g. (4, 8)
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
Optional (with defaults):
|
| 58 |
+
|
| 59 |
+
```python
|
| 60 |
+
padded_vocab_size: int # tile-aligned total vocab; defaults to vocab_size
|
| 61 |
+
max_batch_size: int # per sampling row; default 32
|
| 62 |
+
max_top_k: int # default 32
|
| 63 |
+
sampling_dp: int # >1 for multi-row DP; default 1
|
| 64 |
+
sub_core_grids # CoreRangeSet or None
|
| 65 |
+
model_config: dict # keys: GALAXY_NUM_LINKS, DECODE_SAMPLING_INPUT_MEMCFG, SAMPLING_AG_CONFIG
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
## `data_parallel` vs `sampling_dp`
|
| 69 |
+
|
| 70 |
+
These are different concepts and should not be mixed:
|
| 71 |
+
|
| 72 |
+
- **`data_parallel`** lives above this package. It means multiple TT model
|
| 73 |
+
instances / submeshes process different requests in parallel.
|
| 74 |
+
- **`sampling_dp`** lives inside this package. It means one TT model instance
|
| 75 |
+
has multiple independent sampling groups, usually one per mesh row.
|
| 76 |
+
|
| 77 |
+
For `sampling_dp > 1`:
|
| 78 |
+
- logits are still computed per sampling group
|
| 79 |
+
- but sampling params, seeds, and penalty state are flattened to
|
| 80 |
+
`max_batch_size * sampling_dp`
|
| 81 |
+
- those flattened host tensors are then row-sharded onto the device
|
| 82 |
+
|
| 83 |
+
Decode already follows this contract by using `chunk_sampling_params(...)`
|
| 84 |
+
plus `apply_decode_state(...)`.
|
| 85 |
+
|
| 86 |
+
## Param Distribution API
|
| 87 |
+
|
| 88 |
+
**`SamplingParams`**: Canonical dataclass for sampling parameters (temp, top_k, top_p, penalties, seed, log_probs). Import from `models.common.sampling`. vLLM has its own duck-type-compatible `TTSamplingParams`.
|
| 89 |
+
|
| 90 |
+
**`broadcast_sampling_params(params, idx, slot_len=32)`**: Expand a single user's params to fill `slot_len` slots. Used during prefill.
|
| 91 |
+
|
| 92 |
+
**`chunk_sampling_params(params, sampling_dp)`**: Split a SamplingParams into `sampling_dp` pieces. List fields split evenly; scalars replicated. Works with duck-typed objects (vLLM).
|
| 93 |
+
|
| 94 |
+
**`SamplingGenerator.apply_prefill_state(...)`**: Reset params, seeds, prompt tokens, and output state for a prefill request.
|
| 95 |
+
|
| 96 |
+
**`SamplingGenerator.apply_decode_state(chunks, ...)`**: Format/merge params and apply for one model instance. Handles both simple (1 chunk) and row-sharded (multiple chunks) cases. Does NOT advance seeds — callers manage `seed_manager.get_new_values()` separately.
|
| 97 |
+
|
| 98 |
+
## Pitfalls
|
| 99 |
+
|
| 100 |
+
**`padded_vocab_size` vs `vocab_size`**: TTSampling device offsets for global token IDs must use the padded vocab size to match how the LM head shards logits across devices. Using unpadded `vocab_size` for offsets shifts token IDs from devices 1+ and produces garbled output.
|
| 101 |
+
|
| 102 |
+
**`sampling_dp`**: When >1, k/p/temp tensors must have length `max_batch_size * sampling_dp` and are row-sharded via `ShardTensor2dMesh(dims=(0, None))`. Use `chunk_sampling_params` + `apply_decode_state` to distribute params across mesh rows.
|
| 103 |
+
|
| 104 |
+
**Batched prefill + on-device sampling**: This path is only valid when the
|
| 105 |
+
runtime prefill compute layout matches the sampling-group layout. If a model
|
| 106 |
+
uses `sampling_dp > 1` but does not expose a row-sharded batched-prefill input
|
| 107 |
+
contract, batched prefill must fall back to sequential prefill for correctness.
|
| 108 |
+
|
| 109 |
+
**Trace invalidation**: Changing `force_argmax_sampling` state invalidates captured traces. Force-argmax is triggered when callers pass k=1, p=1.0, temp=1.0 (note: p=1.0 means "no top-p filtering", distinct from the internal initialization default of p=0). `SamplingGenerator.reset_sampling_params` handles this.
|
| 110 |
+
|
| 111 |
+
## Future Work
|
| 112 |
+
|
| 113 |
+
- Consolidate DeepSeek's minimal `SamplingParams` (in `models/demos/deepseek_v3/tt/generator.py`) to use the common one
|
code/models/common/sampling/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
|
| 2 |
+
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
|
| 5 |
+
from .tt_sampling import TTSampling
|
| 6 |
+
from .tt_penalties import TTPenalties, apply_penalties
|
| 7 |
+
from .tt_log_probs import LogProbsCalculator, LogProbsResult
|
| 8 |
+
from .generator import (
|
| 9 |
+
SamplingGenerator,
|
| 10 |
+
SamplingParams,
|
| 11 |
+
SAMPLING_PARAM_FIELDS,
|
| 12 |
+
format_sampling_params,
|
| 13 |
+
broadcast_sampling_params,
|
| 14 |
+
chunk_sampling_params,
|
| 15 |
+
SeedManager,
|
| 16 |
+
)
|
| 17 |
+
from ._utils import split_list
|
| 18 |
+
|
| 19 |
+
__all__ = [
|
| 20 |
+
"TTSampling",
|
| 21 |
+
"TTPenalties",
|
| 22 |
+
"apply_penalties",
|
| 23 |
+
"LogProbsCalculator",
|
| 24 |
+
"LogProbsResult",
|
| 25 |
+
"SamplingGenerator",
|
| 26 |
+
"SamplingParams",
|
| 27 |
+
"SAMPLING_PARAM_FIELDS",
|
| 28 |
+
"format_sampling_params",
|
| 29 |
+
"broadcast_sampling_params",
|
| 30 |
+
"chunk_sampling_params",
|
| 31 |
+
"SeedManager",
|
| 32 |
+
"split_list",
|
| 33 |
+
]
|
code/models/common/sampling/_utils.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
|
| 2 |
+
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
|
| 5 |
+
from loguru import logger
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def clamp(value, min_value, max_value):
|
| 9 |
+
if value < min_value:
|
| 10 |
+
return min_value
|
| 11 |
+
elif value > max_value:
|
| 12 |
+
return max_value
|
| 13 |
+
return value
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def is_default_value(values, default):
|
| 17 |
+
"""Check if values match a default, handling None, scalar, and iterable inputs."""
|
| 18 |
+
if values is None:
|
| 19 |
+
return True
|
| 20 |
+
if isinstance(values, (int, float)):
|
| 21 |
+
return values == default
|
| 22 |
+
return all(value == default for value in values)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def filter_none(kwargs: dict) -> dict:
|
| 26 |
+
return {k: v for k, v in kwargs.items() if v is not None}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def split_list(lst, n):
|
| 30 |
+
"""Split list into n equal parts."""
|
| 31 |
+
chunk_size = len(lst) // n
|
| 32 |
+
return [list(lst[i * chunk_size : (i + 1) * chunk_size]) for i in range(n)]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def compact_debug_list(values, max_items=12):
|
| 36 |
+
if values is None:
|
| 37 |
+
return None
|
| 38 |
+
if hasattr(values, "reshape") and hasattr(values, "tolist"):
|
| 39 |
+
values = values.reshape(-1).tolist()
|
| 40 |
+
elif isinstance(values, tuple):
|
| 41 |
+
values = list(values)
|
| 42 |
+
elif not isinstance(values, list):
|
| 43 |
+
values = list(values) if isinstance(values, range) else [values]
|
| 44 |
+
if len(values) <= max_items:
|
| 45 |
+
return values
|
| 46 |
+
half = max(1, max_items // 2)
|
| 47 |
+
return {"len": len(values), "head": values[:half], "tail": values[-half:]}
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def is_llama33_70b_model(args) -> bool:
|
| 51 |
+
if isinstance(args, list):
|
| 52 |
+
args = args[0] if args else None
|
| 53 |
+
if args is None:
|
| 54 |
+
return False
|
| 55 |
+
|
| 56 |
+
fields_to_check = (
|
| 57 |
+
"model_name",
|
| 58 |
+
"base_model_name",
|
| 59 |
+
"model_base_path",
|
| 60 |
+
"model_cache_path",
|
| 61 |
+
"tokenizer_path",
|
| 62 |
+
"CKPT_DIR",
|
| 63 |
+
"LLAMA_DIR",
|
| 64 |
+
"hf_model",
|
| 65 |
+
"HF_MODEL",
|
| 66 |
+
)
|
| 67 |
+
for field in fields_to_check:
|
| 68 |
+
value = getattr(args, field, None)
|
| 69 |
+
if value is None:
|
| 70 |
+
continue
|
| 71 |
+
normalized = str(value).lower().replace("_", "-")
|
| 72 |
+
if "llama" in normalized and "3.3-70b" in normalized:
|
| 73 |
+
return True
|
| 74 |
+
return False
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def log_sampling_debug(enabled, message, **kwargs):
|
| 78 |
+
if not enabled:
|
| 79 |
+
return
|
| 80 |
+
compact = {key: value for key, value in kwargs.items() if value is not None}
|
| 81 |
+
logger.info(f"SamplingDBG {message}: {compact}")
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def is_power_of_2(n):
|
| 85 |
+
return n > 0 and (n & (n - 1)) == 0
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def upper_power_of_2(n: int) -> int:
|
| 89 |
+
if n <= 1:
|
| 90 |
+
return 1
|
| 91 |
+
return 1 << (n - 1).bit_length()
|
code/models/common/sampling/generator.py
ADDED
|
@@ -0,0 +1,932 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
|
| 2 |
+
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
|
| 5 |
+
import copy
|
| 6 |
+
import random
|
| 7 |
+
import secrets
|
| 8 |
+
from dataclasses import dataclass, fields, replace
|
| 9 |
+
from typing import List, Optional
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
from loguru import logger
|
| 13 |
+
|
| 14 |
+
import ttnn
|
| 15 |
+
|
| 16 |
+
from ._utils import clamp
|
| 17 |
+
from ._utils import compact_debug_list as _compact_debug_list
|
| 18 |
+
from ._utils import is_default_value, is_llama33_70b_model
|
| 19 |
+
from ._utils import log_sampling_debug as _log_sampling_debug
|
| 20 |
+
from ._utils import split_list
|
| 21 |
+
from .tt_penalties import TTPenalties
|
| 22 |
+
from .tt_sampling import TTSampling
|
| 23 |
+
|
| 24 |
+
MAX_UINT32 = 2**32 - 1
|
| 25 |
+
# MAX_UINT32 is reserved as the device skip sentinel; keep real seeds in a bounded positive range.
|
| 26 |
+
DEVICE_SEED_MAX = 1_000_000
|
| 27 |
+
_UINT64_MASK = (1 << 64) - 1
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _hash_request_seed_to_device_seed(seed: int, counter: int) -> int:
|
| 31 |
+
"""Derive a stable per-token device seed from a request seed.
|
| 32 |
+
|
| 33 |
+
The device sampling op accepts bounded positive seeds, while vLLM
|
| 34 |
+
request seeds can be any integer and must be reproducible regardless
|
| 35 |
+
of batch slot. Hashing (request seed, token counter) gives each token
|
| 36 |
+
a deterministic but well-mixed device seed without relying on mutable
|
| 37 |
+
per-slot RNG state. The constants below are the SplitMix64 finalizer.
|
| 38 |
+
"""
|
| 39 |
+
value = (int(seed) & _UINT64_MASK) ^ ((int(counter) + 0x9E3779B97F4A7C15) & _UINT64_MASK)
|
| 40 |
+
value = ((value ^ (value >> 30)) * 0xBF58476D1CE4E5B9) & _UINT64_MASK
|
| 41 |
+
value = ((value ^ (value >> 27)) * 0x94D049BB133111EB) & _UINT64_MASK
|
| 42 |
+
value = (value ^ (value >> 31)) & _UINT64_MASK
|
| 43 |
+
return (value % DEVICE_SEED_MAX) + 1
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@dataclass(frozen=True)
|
| 47 |
+
class SamplingParams:
|
| 48 |
+
"""
|
| 49 |
+
Sampling parameters for on-device greedy decoding / sampling.
|
| 50 |
+
|
| 51 |
+
Used by Generator decode/prefill functions. vLLM has its own duck-type-compatible
|
| 52 |
+
TTSamplingParams (in vllm/worker/tt_model_runner.py) that works with the same
|
| 53 |
+
format_sampling_params / chunk_sampling_params functions.
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
temperature: float | list[float]
|
| 57 |
+
top_k: int | list[int]
|
| 58 |
+
top_p: float | list[float]
|
| 59 |
+
presence_penalty: float | list[float] = 0.0
|
| 60 |
+
frequency_penalty: float | list[float] = 0.0
|
| 61 |
+
repetition_penalty: float | list[float] = 1.0
|
| 62 |
+
seed: int | list[int] | None = None
|
| 63 |
+
enable_log_probs: bool | list[bool] = False
|
| 64 |
+
num_logprobs: int | list[int] = 0
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
SAMPLING_PARAM_FIELDS = tuple(f.name for f in fields(SamplingParams))
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@dataclass(frozen=True)
|
| 71 |
+
class _TraceKey:
|
| 72 |
+
penalties_on: bool
|
| 73 |
+
log_probs_on: bool
|
| 74 |
+
force_argmax: bool
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class SamplingGenerator:
|
| 78 |
+
"""
|
| 79 |
+
High-level sampling helper that owns both `TTSampling` and `TTPenalties`
|
| 80 |
+
modules and optionally manages TTNN trace capture/execution for sampling.
|
| 81 |
+
|
| 82 |
+
Typical usage:
|
| 83 |
+
generator = SamplingGenerator(args=args, mesh_device=mesh_device, tt_ccl=tt_ccl)
|
| 84 |
+
generator.reset_sampling_params(k=..., p=..., temp=...)
|
| 85 |
+
tokens = generator.sample(logits, enable_trace=True)
|
| 86 |
+
"""
|
| 87 |
+
|
| 88 |
+
_DEFAULT_PENALTIES = {
|
| 89 |
+
"presence": 0.0,
|
| 90 |
+
"frequency": 0.0,
|
| 91 |
+
"repetition": 1.0,
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
def __init__(
|
| 95 |
+
self,
|
| 96 |
+
*,
|
| 97 |
+
args,
|
| 98 |
+
mesh_device,
|
| 99 |
+
tt_ccl,
|
| 100 |
+
cq_id: int = 0,
|
| 101 |
+
):
|
| 102 |
+
self.mesh_device = mesh_device
|
| 103 |
+
self.cq_id = cq_id
|
| 104 |
+
self.args = args
|
| 105 |
+
self._sampling_debug_enabled = is_llama33_70b_model(args)
|
| 106 |
+
self.sub_core_grids = getattr(args, "sub_core_grids", None)
|
| 107 |
+
self.tt_sampling = TTSampling(mesh_device=mesh_device, tt_ccl=tt_ccl, args=args)
|
| 108 |
+
self.tt_penalties = TTPenalties(mesh_device=mesh_device, args=args)
|
| 109 |
+
|
| 110 |
+
self._penalties_active = False
|
| 111 |
+
|
| 112 |
+
self._trace_states: dict[_TraceKey, dict] = {}
|
| 113 |
+
seed_batch_size = self.tt_sampling.max_batch_size * self.tt_sampling._sampling_dp
|
| 114 |
+
self.seed_manager = SeedManager(
|
| 115 |
+
self.tt_sampling,
|
| 116 |
+
max_batch_size=seed_batch_size,
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
def _new_trace_state(self):
|
| 120 |
+
return {"id": None, "input": None, "output": None, "kwargs": {}}
|
| 121 |
+
|
| 122 |
+
def _trace_slot(self, penalties_on: bool, log_probs_on: bool, force_argmax: bool):
|
| 123 |
+
key = _TraceKey(penalties_on=penalties_on, log_probs_on=log_probs_on, force_argmax=force_argmax)
|
| 124 |
+
slot = self._trace_states.get(key)
|
| 125 |
+
if slot is None:
|
| 126 |
+
slot = self._new_trace_state()
|
| 127 |
+
self._trace_states[key] = slot
|
| 128 |
+
return key, slot
|
| 129 |
+
|
| 130 |
+
def reset_trace(self):
|
| 131 |
+
"""
|
| 132 |
+
Drop any cached trace metadata for both penalties/no-penalties and log-probs/no-log-probs paths.
|
| 133 |
+
"""
|
| 134 |
+
for key, slot in self._trace_states.items():
|
| 135 |
+
if slot["id"] is None:
|
| 136 |
+
continue
|
| 137 |
+
logger.debug(
|
| 138 |
+
f"Resetting sampling trace (penalties={key.penalties_on}, log_probs={key.log_probs_on}, force_argmax={key.force_argmax}, trace_id={slot['id']})"
|
| 139 |
+
)
|
| 140 |
+
try:
|
| 141 |
+
ttnn.release_trace(self.mesh_device, slot["id"])
|
| 142 |
+
except Exception as e:
|
| 143 |
+
logger.warning(f"Failed to release trace {slot['id']} : {e}")
|
| 144 |
+
continue
|
| 145 |
+
self._trace_states.clear()
|
| 146 |
+
|
| 147 |
+
def reset_prompt_tokens(self, prompt_tokens):
|
| 148 |
+
if not self._penalties_active:
|
| 149 |
+
return
|
| 150 |
+
self.tt_penalties.reset_prompt_tokens(prompt_tokens)
|
| 151 |
+
|
| 152 |
+
def reset_output_state(self, tokens=None):
|
| 153 |
+
if not self._penalties_active:
|
| 154 |
+
return
|
| 155 |
+
self.tt_penalties.reset_output_tokens(tokens)
|
| 156 |
+
|
| 157 |
+
# ---------------------------------------------------------------------
|
| 158 |
+
# Prefill / decode state helpers
|
| 159 |
+
# ---------------------------------------------------------------------
|
| 160 |
+
def apply_prefill_state(
|
| 161 |
+
self,
|
| 162 |
+
*,
|
| 163 |
+
sampling_params,
|
| 164 |
+
prompt_tokens: torch.Tensor | None,
|
| 165 |
+
empty_slots: list[int],
|
| 166 |
+
replicate_seeds: bool = True,
|
| 167 |
+
):
|
| 168 |
+
"""Prepare sampling state for a prefill request.
|
| 169 |
+
|
| 170 |
+
Resets params, seeds, prompt tokens, and output state in the correct order.
|
| 171 |
+
"""
|
| 172 |
+
self.reset_sampling_params(sampling_params, empty_slots=empty_slots)
|
| 173 |
+
seed = getattr(sampling_params, "seed", None)
|
| 174 |
+
# assert on condition that seed is not None
|
| 175 |
+
assert seed is not None, "sampling_params must be formatted (seed should be a list, not None)"
|
| 176 |
+
self.seed_manager.reset_seed(seed, empty_slots)
|
| 177 |
+
self.seed_manager.get_new_values(empty_slots, replicate_seeds=replicate_seeds)
|
| 178 |
+
if prompt_tokens is not None:
|
| 179 |
+
self.reset_prompt_tokens(prompt_tokens)
|
| 180 |
+
self.reset_output_state()
|
| 181 |
+
|
| 182 |
+
def apply_decode_state(
|
| 183 |
+
self,
|
| 184 |
+
sampling_params_chunks: list,
|
| 185 |
+
*,
|
| 186 |
+
reset_batch: bool = False,
|
| 187 |
+
prompt_tokens: torch.Tensor | None = None,
|
| 188 |
+
output_tokens: torch.Tensor | None = None,
|
| 189 |
+
):
|
| 190 |
+
"""Format, merge (if row-sharded), and apply sampling params for one model instance.
|
| 191 |
+
|
| 192 |
+
Args:
|
| 193 |
+
sampling_params_chunks: List of SamplingParams assigned to this instance.
|
| 194 |
+
Length-1 for simple cases; >1 for row-sharded (sampling_dp > data_parallel).
|
| 195 |
+
reset_batch: Also reset prompt tokens and output state (first decode step).
|
| 196 |
+
prompt_tokens: Prompt tokens for penalty tracking.
|
| 197 |
+
output_tokens: Output tokens for penalty tracking.
|
| 198 |
+
|
| 199 |
+
Does NOT call ``seed_manager.get_new_values()`` — callers manage seed
|
| 200 |
+
advancement separately since generators call it at different points.
|
| 201 |
+
"""
|
| 202 |
+
chunks_per_model = len(sampling_params_chunks)
|
| 203 |
+
|
| 204 |
+
max_batch_size = self.tt_sampling.max_batch_size
|
| 205 |
+
|
| 206 |
+
if chunks_per_model == 1:
|
| 207 |
+
formatted_params = format_sampling_params(sampling_params_chunks[0], max_batch_size)
|
| 208 |
+
self.reset_sampling_params(formatted_params)
|
| 209 |
+
else:
|
| 210 |
+
# Row-sharded case: format each chunk to max_batch_size, concatenate.
|
| 211 |
+
# After (0, None) sharding each row gets its own chunk of max_batch_size entries.
|
| 212 |
+
# Both TTSampling and TTPenalties use the same concatenated params.
|
| 213 |
+
formatted_chunks = [format_sampling_params(chunk, max_batch_size) for chunk in sampling_params_chunks]
|
| 214 |
+
concat_fields = {}
|
| 215 |
+
for field in SAMPLING_PARAM_FIELDS:
|
| 216 |
+
lists = [getattr(fc, field) for fc in formatted_chunks]
|
| 217 |
+
if all(v is None for v in lists):
|
| 218 |
+
concat_fields[field] = None
|
| 219 |
+
else:
|
| 220 |
+
concat_fields[field] = sum((v if isinstance(v, list) else [v] for v in lists), [])
|
| 221 |
+
formatted_params = SamplingParams(**concat_fields)
|
| 222 |
+
self.reset_sampling_params(formatted_params)
|
| 223 |
+
|
| 224 |
+
if reset_batch:
|
| 225 |
+
self.reset_prompt_tokens(prompt_tokens)
|
| 226 |
+
self.reset_output_state(output_tokens)
|
| 227 |
+
|
| 228 |
+
# ---------------------------------------------------------------------
|
| 229 |
+
# Sampling helpers
|
| 230 |
+
# ---------------------------------------------------------------------
|
| 231 |
+
def reset_sampling_params(self, sampling_params, empty_slots: list[int] | None = None):
|
| 232 |
+
old_force_argmax_sampling = self.tt_sampling.force_argmax_sampling
|
| 233 |
+
num_logprobs = getattr(sampling_params, "num_logprobs", None)
|
| 234 |
+
self.tt_sampling.reset_params(
|
| 235 |
+
k=sampling_params.top_k,
|
| 236 |
+
p=sampling_params.top_p,
|
| 237 |
+
temp=sampling_params.temperature,
|
| 238 |
+
enable_log_probs=sampling_params.enable_log_probs,
|
| 239 |
+
num_logprobs=num_logprobs,
|
| 240 |
+
empty_slots=empty_slots,
|
| 241 |
+
)
|
| 242 |
+
if self.tt_sampling.force_argmax_sampling != old_force_argmax_sampling:
|
| 243 |
+
self.reset_trace()
|
| 244 |
+
|
| 245 |
+
old_penalties_active = self._penalties_active
|
| 246 |
+
self._penalties_active = not (
|
| 247 |
+
is_default_value(sampling_params.presence_penalty, self._DEFAULT_PENALTIES["presence"])
|
| 248 |
+
and is_default_value(sampling_params.frequency_penalty, self._DEFAULT_PENALTIES["frequency"])
|
| 249 |
+
and is_default_value(sampling_params.repetition_penalty, self._DEFAULT_PENALTIES["repetition"])
|
| 250 |
+
)
|
| 251 |
+
if (
|
| 252 |
+
not self.tt_sampling.force_argmax_sampling
|
| 253 |
+
or self._penalties_active
|
| 254 |
+
or self._penalties_active != old_penalties_active
|
| 255 |
+
):
|
| 256 |
+
self.tt_penalties.reset_params(
|
| 257 |
+
sampling_params.presence_penalty, sampling_params.frequency_penalty, sampling_params.repetition_penalty
|
| 258 |
+
)
|
| 259 |
+
self._log_probs_active = self.tt_sampling.log_probs_calculator.enable_log_probs
|
| 260 |
+
_log_sampling_debug(
|
| 261 |
+
self._sampling_debug_enabled,
|
| 262 |
+
"SamplingGenerator reset params",
|
| 263 |
+
empty_slots=_compact_debug_list(empty_slots),
|
| 264 |
+
force_argmax=self.tt_sampling.force_argmax_sampling,
|
| 265 |
+
force_argmax_changed=self.tt_sampling.force_argmax_sampling != old_force_argmax_sampling,
|
| 266 |
+
penalties_active=self._penalties_active,
|
| 267 |
+
log_probs_active=self._log_probs_active,
|
| 268 |
+
temperature=_compact_debug_list(sampling_params.temperature),
|
| 269 |
+
top_k=_compact_debug_list(sampling_params.top_k),
|
| 270 |
+
top_p=_compact_debug_list(sampling_params.top_p),
|
| 271 |
+
presence_penalty=_compact_debug_list(sampling_params.presence_penalty),
|
| 272 |
+
frequency_penalty=_compact_debug_list(sampling_params.frequency_penalty),
|
| 273 |
+
repetition_penalty=_compact_debug_list(sampling_params.repetition_penalty),
|
| 274 |
+
seed=_compact_debug_list(getattr(sampling_params, "seed", None)),
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
def _validate_trace_inputs(self, slot, logits: ttnn.Tensor, tt_out_tok: Optional[ttnn.Tensor]):
|
| 278 |
+
if slot["input"] is None or slot["output"] is None:
|
| 279 |
+
raise RuntimeError("Trace metadata missing. Call capture_trace first.")
|
| 280 |
+
|
| 281 |
+
if logits is not slot["input"]:
|
| 282 |
+
raise ValueError(
|
| 283 |
+
"The provided logits tensor does not match the tensor used during trace capture. "
|
| 284 |
+
"Call `reset_trace()` before tracing with new tensors."
|
| 285 |
+
)
|
| 286 |
+
if isinstance(slot["output"], tuple):
|
| 287 |
+
if tt_out_tok is not None and tt_out_tok is not slot["output"][0]:
|
| 288 |
+
raise ValueError(
|
| 289 |
+
"The provided output tensor does not match the tensor used during trace capture. "
|
| 290 |
+
"Call `reset_trace()` before tracing with new tensors."
|
| 291 |
+
)
|
| 292 |
+
else:
|
| 293 |
+
if tt_out_tok is not None and tt_out_tok is not slot["output"]:
|
| 294 |
+
raise ValueError(
|
| 295 |
+
"The provided output tensor does not match the tensor used during trace capture. "
|
| 296 |
+
"Call `reset_trace()` before tracing with new tensors."
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
def _run_sampling(
|
| 300 |
+
self,
|
| 301 |
+
logits,
|
| 302 |
+
*,
|
| 303 |
+
penalties_on: bool,
|
| 304 |
+
tt_out_tok: Optional[ttnn.Tensor],
|
| 305 |
+
):
|
| 306 |
+
if penalties_on:
|
| 307 |
+
logits = self.tt_penalties.apply(logits)
|
| 308 |
+
tt_tokens, tt_log_probs = self.tt_sampling(logits, tt_out_tok=tt_out_tok)
|
| 309 |
+
return tt_tokens, tt_log_probs
|
| 310 |
+
|
| 311 |
+
def capture_trace(
|
| 312 |
+
self,
|
| 313 |
+
logits: ttnn.Tensor,
|
| 314 |
+
*,
|
| 315 |
+
tt_out_tok: Optional[ttnn.Tensor] = None,
|
| 316 |
+
skip_precompile: bool = False,
|
| 317 |
+
) -> ttnn.Tensor:
|
| 318 |
+
"""
|
| 319 |
+
Capture a trace of the sampling pipeline for the given configuration.
|
| 320 |
+
"""
|
| 321 |
+
penalties_on = self._penalties_active
|
| 322 |
+
log_probs_on = getattr(self, "_log_probs_active", False)
|
| 323 |
+
force_argmax = self.tt_sampling.force_argmax_sampling
|
| 324 |
+
|
| 325 |
+
key, slot = self._trace_slot(penalties_on, log_probs_on, force_argmax)
|
| 326 |
+
|
| 327 |
+
if not skip_precompile:
|
| 328 |
+
logger.debug(
|
| 329 |
+
f"Pre-compiling sampling path before trace capture (penalties={penalties_on},log_probs_on={log_probs_on},force_argmax={force_argmax})"
|
| 330 |
+
)
|
| 331 |
+
self._run_sampling(
|
| 332 |
+
logits,
|
| 333 |
+
penalties_on=penalties_on,
|
| 334 |
+
tt_out_tok=tt_out_tok,
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
trace_id = ttnn.begin_trace_capture(self.mesh_device, cq_id=self.cq_id)
|
| 338 |
+
sampled = self._run_sampling(
|
| 339 |
+
logits,
|
| 340 |
+
penalties_on=penalties_on,
|
| 341 |
+
tt_out_tok=tt_out_tok,
|
| 342 |
+
)
|
| 343 |
+
ttnn.end_trace_capture(self.mesh_device, trace_id, cq_id=self.cq_id)
|
| 344 |
+
ttnn.synchronize_device(self.mesh_device)
|
| 345 |
+
|
| 346 |
+
if tt_out_tok is not None:
|
| 347 |
+
if isinstance(sampled, tuple):
|
| 348 |
+
output = (tt_out_tok, sampled[-1])
|
| 349 |
+
else:
|
| 350 |
+
output = (tt_out_tok, sampled)
|
| 351 |
+
else:
|
| 352 |
+
output = sampled
|
| 353 |
+
|
| 354 |
+
slot["id"] = trace_id
|
| 355 |
+
slot["input"] = logits
|
| 356 |
+
slot["output"] = output
|
| 357 |
+
slot["kwargs"] = {"tt_out_tok": tt_out_tok}
|
| 358 |
+
|
| 359 |
+
return slot["output"]
|
| 360 |
+
|
| 361 |
+
def _execute_trace(self, key: _TraceKey) -> ttnn.Tensor:
|
| 362 |
+
slot = self._trace_states.get(key)
|
| 363 |
+
if slot is None:
|
| 364 |
+
raise RuntimeError("Trace has not been captured yet.")
|
| 365 |
+
if slot["id"] is None or slot["output"] is None:
|
| 366 |
+
raise RuntimeError("Trace has not been captured yet.")
|
| 367 |
+
|
| 368 |
+
ttnn.execute_trace(self.mesh_device, slot["id"], cq_id=self.cq_id, blocking=False)
|
| 369 |
+
return slot["output"]
|
| 370 |
+
|
| 371 |
+
def sample(
|
| 372 |
+
self,
|
| 373 |
+
logits: ttnn.Tensor,
|
| 374 |
+
*,
|
| 375 |
+
enable_trace: bool = True,
|
| 376 |
+
tt_out_tok: Optional[ttnn.Tensor] = None,
|
| 377 |
+
skip_precompile: bool = False,
|
| 378 |
+
) -> ttnn.Tensor:
|
| 379 |
+
"""
|
| 380 |
+
Convenience wrapper that either runs the sampling module directly or
|
| 381 |
+
replays a captured trace.
|
| 382 |
+
"""
|
| 383 |
+
|
| 384 |
+
penalties_on = self._penalties_active
|
| 385 |
+
log_probs_on = getattr(self, "_log_probs_active", False)
|
| 386 |
+
force_argmax = self.tt_sampling.force_argmax_sampling
|
| 387 |
+
# Explicit request seeds update a persistent seed tensor every token;
|
| 388 |
+
# run them directly so trace replay cannot observe stale seed state.
|
| 389 |
+
use_internal_trace = enable_trace and not self.seed_manager.has_active_request_seed()
|
| 390 |
+
_log_sampling_debug(
|
| 391 |
+
self._sampling_debug_enabled,
|
| 392 |
+
"SamplingGenerator sample",
|
| 393 |
+
enable_trace=enable_trace,
|
| 394 |
+
use_internal_trace=use_internal_trace,
|
| 395 |
+
penalties_on=penalties_on,
|
| 396 |
+
log_probs_on=log_probs_on,
|
| 397 |
+
force_argmax=force_argmax,
|
| 398 |
+
logits_shape=list(logits.shape),
|
| 399 |
+
tt_out_tok_shape=list(tt_out_tok.shape) if tt_out_tok is not None else None,
|
| 400 |
+
)
|
| 401 |
+
|
| 402 |
+
if not use_internal_trace:
|
| 403 |
+
tt_out = self._run_sampling(
|
| 404 |
+
logits,
|
| 405 |
+
penalties_on=penalties_on,
|
| 406 |
+
tt_out_tok=tt_out_tok,
|
| 407 |
+
)
|
| 408 |
+
else:
|
| 409 |
+
key, slot = self._trace_slot(penalties_on, log_probs_on, force_argmax)
|
| 410 |
+
if slot["id"] is None:
|
| 411 |
+
return self.capture_trace(
|
| 412 |
+
logits,
|
| 413 |
+
tt_out_tok=tt_out_tok,
|
| 414 |
+
skip_precompile=skip_precompile,
|
| 415 |
+
)
|
| 416 |
+
|
| 417 |
+
self._validate_trace_inputs(slot, logits, tt_out_tok)
|
| 418 |
+
tt_out = self._execute_trace(key)
|
| 419 |
+
|
| 420 |
+
if penalties_on and tt_out is not None:
|
| 421 |
+
if isinstance(tt_out, tuple):
|
| 422 |
+
self.tt_penalties.update_output_tokens(tt_out[0])
|
| 423 |
+
else:
|
| 424 |
+
self.tt_penalties.update_output_tokens(tt_out)
|
| 425 |
+
return tt_out
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
def format_sampling_params(sampling_params, max_batch_size):
|
| 429 |
+
"""
|
| 430 |
+
Format sampling parameters for on-device use.
|
| 431 |
+
|
| 432 |
+
Converts scalar fields to lists, pads all lists to ``max_batch_size``,
|
| 433 |
+
inverts temperature, clamps top-p/top-k, and normalises penalties.
|
| 434 |
+
|
| 435 |
+
Returns a **new** SamplingParams — the input is never mutated.
|
| 436 |
+
"""
|
| 437 |
+
if not isinstance(sampling_params.temperature, List):
|
| 438 |
+
update_dict = {field.name: [getattr(sampling_params, field.name)] for field in fields(sampling_params)}
|
| 439 |
+
sampling_params = replace(sampling_params, **update_dict)
|
| 440 |
+
|
| 441 |
+
target_len = max_batch_size
|
| 442 |
+
assert target_len % 32 == 0, f"Sampling batch size must be a multiple of 32, got {target_len}"
|
| 443 |
+
|
| 444 |
+
# Defaults used when padding short lists to target_len
|
| 445 |
+
defaults = {
|
| 446 |
+
"temperature": 0.0,
|
| 447 |
+
"top_p": 1.0,
|
| 448 |
+
"top_k": 1,
|
| 449 |
+
"presence_penalty": 0.0,
|
| 450 |
+
"frequency_penalty": 0.0,
|
| 451 |
+
"repetition_penalty": 1.0,
|
| 452 |
+
"seed": None,
|
| 453 |
+
"num_logprobs": 0,
|
| 454 |
+
"enable_log_probs": False,
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
def _pad(lst, name):
|
| 458 |
+
"""Return a new list padded to target_len with the default for *name*."""
|
| 459 |
+
if len(lst) >= target_len:
|
| 460 |
+
return list(lst)
|
| 461 |
+
return list(lst) + [defaults[name]] * (target_len - len(lst))
|
| 462 |
+
|
| 463 |
+
# Pad core sampling fields (scalar→list already done above)
|
| 464 |
+
temperature = _pad(sampling_params.temperature, "temperature")
|
| 465 |
+
top_p = _pad(sampling_params.top_p, "top_p")
|
| 466 |
+
top_k = _pad(sampling_params.top_k, "top_k")
|
| 467 |
+
|
| 468 |
+
# enable_log_probs / num_logprobs: scalar → broadcast to all users.
|
| 469 |
+
# Multi-element list → pad with default (False/0) for inactive slots.
|
| 470 |
+
# Single-element list (from scalar→list conversion) → broadcast to all.
|
| 471 |
+
def _broadcast_pad(lst, name):
|
| 472 |
+
if not isinstance(lst, list):
|
| 473 |
+
return [lst] * target_len
|
| 474 |
+
if len(lst) == 1:
|
| 475 |
+
return lst * target_len
|
| 476 |
+
return _pad(lst, name)
|
| 477 |
+
|
| 478 |
+
enable_log_probs = _broadcast_pad(sampling_params.enable_log_probs, "enable_log_probs")
|
| 479 |
+
if getattr(sampling_params, "num_logprobs", None) is not None:
|
| 480 |
+
num_logprobs = _broadcast_pad(sampling_params.num_logprobs, "num_logprobs")
|
| 481 |
+
else:
|
| 482 |
+
num_logprobs = None
|
| 483 |
+
|
| 484 |
+
# Normalise and pad penalty / seed fields (may still be None/scalar)
|
| 485 |
+
def _normalise_and_pad(name):
|
| 486 |
+
value = getattr(sampling_params, name, None)
|
| 487 |
+
if value is None:
|
| 488 |
+
lst = [defaults[name]]
|
| 489 |
+
elif isinstance(value, List):
|
| 490 |
+
lst = list(value)
|
| 491 |
+
else:
|
| 492 |
+
lst = [value]
|
| 493 |
+
return _pad(lst, name)
|
| 494 |
+
|
| 495 |
+
presence_penalty = _normalise_and_pad("presence_penalty")
|
| 496 |
+
frequency_penalty = _normalise_and_pad("frequency_penalty")
|
| 497 |
+
repetition_penalty = _normalise_and_pad("repetition_penalty")
|
| 498 |
+
seed = _normalise_and_pad("seed")
|
| 499 |
+
|
| 500 |
+
# Clamp / transform values in the new lists (no mutation of the input)
|
| 501 |
+
TOP_P_MIN = 0.0
|
| 502 |
+
TOP_P_MAX = 1.0
|
| 503 |
+
|
| 504 |
+
for i in range(len(temperature)):
|
| 505 |
+
top_p[i] = clamp(top_p[i], TOP_P_MIN, TOP_P_MAX)
|
| 506 |
+
|
| 507 |
+
if temperature[i] == 0:
|
| 508 |
+
temperature[i] = 1.0
|
| 509 |
+
top_k[i] = 1
|
| 510 |
+
# Device sampling treats p=0 as a first-token cutoff; with k=1
|
| 511 |
+
# this is the compact argmax representation for greedy rows.
|
| 512 |
+
top_p[i] = 0.0
|
| 513 |
+
else:
|
| 514 |
+
temperature[i] = 1 / temperature[i]
|
| 515 |
+
|
| 516 |
+
# top_k contract: TT sampling supports up to 32 today.
|
| 517 |
+
# k < 1 means "no restriction" → max (32); k > 32 → capped to 32.
|
| 518 |
+
if top_k[i] < 1:
|
| 519 |
+
top_k[i] = 32
|
| 520 |
+
if top_k[i] > 32:
|
| 521 |
+
top_k[i] = 32
|
| 522 |
+
|
| 523 |
+
if repetition_penalty[i] == 0:
|
| 524 |
+
repetition_penalty[i] = defaults["repetition_penalty"]
|
| 525 |
+
|
| 526 |
+
kwargs = dict(
|
| 527 |
+
temperature=temperature,
|
| 528 |
+
top_p=top_p,
|
| 529 |
+
top_k=top_k,
|
| 530 |
+
presence_penalty=presence_penalty,
|
| 531 |
+
frequency_penalty=frequency_penalty,
|
| 532 |
+
repetition_penalty=repetition_penalty,
|
| 533 |
+
seed=seed,
|
| 534 |
+
)
|
| 535 |
+
# Only include logprobs fields if the input dataclass has them
|
| 536 |
+
# (vLLM's TTSamplingParams may not have these fields)
|
| 537 |
+
input_fields = {f.name for f in fields(sampling_params)}
|
| 538 |
+
if "num_logprobs" in input_fields:
|
| 539 |
+
kwargs["num_logprobs"] = num_logprobs
|
| 540 |
+
if "enable_log_probs" in input_fields:
|
| 541 |
+
kwargs["enable_log_probs"] = enable_log_probs
|
| 542 |
+
|
| 543 |
+
return replace(sampling_params, **kwargs)
|
| 544 |
+
|
| 545 |
+
|
| 546 |
+
def broadcast_sampling_params(
|
| 547 |
+
formatted_sampling_params,
|
| 548 |
+
idx: int,
|
| 549 |
+
slot_len: int = 32,
|
| 550 |
+
):
|
| 551 |
+
"""
|
| 552 |
+
Create a new SamplingParams where each list field is broadcast to a full list of length
|
| 553 |
+
``slot_len``, taking the value from ``idx``. Does not mutate the input.
|
| 554 |
+
"""
|
| 555 |
+
kwargs = {}
|
| 556 |
+
for f in fields(formatted_sampling_params):
|
| 557 |
+
value = getattr(formatted_sampling_params, f.name)
|
| 558 |
+
value_is_list = isinstance(value, List)
|
| 559 |
+
if value_is_list:
|
| 560 |
+
chosen = value[idx] if idx < len(value) else value[0]
|
| 561 |
+
else:
|
| 562 |
+
chosen = value
|
| 563 |
+
if value_is_list:
|
| 564 |
+
# Preserve list fields as lists even when the selected value is None.
|
| 565 |
+
kwargs[f.name] = [chosen] * slot_len
|
| 566 |
+
elif chosen is None:
|
| 567 |
+
kwargs[f.name] = None
|
| 568 |
+
else:
|
| 569 |
+
kwargs[f.name] = [chosen] * slot_len
|
| 570 |
+
return SamplingParams(**kwargs)
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
def chunk_sampling_params(sampling_params, sampling_dp: int) -> list:
|
| 574 |
+
"""
|
| 575 |
+
Chunk a SamplingParams (or duck-type-compatible object) into ``sampling_dp`` pieces.
|
| 576 |
+
|
| 577 |
+
List fields are split evenly (length must be divisible by ``sampling_dp``).
|
| 578 |
+
Scalar fields are replicated to all chunks. Falls back to dataclass defaults
|
| 579 |
+
for missing attributes so that vLLM's TTSamplingParams works transparently.
|
| 580 |
+
|
| 581 |
+
Returns a list of SamplingParams.
|
| 582 |
+
"""
|
| 583 |
+
if sampling_dp == 1:
|
| 584 |
+
return [sampling_params]
|
| 585 |
+
|
| 586 |
+
chunked_fields = {}
|
| 587 |
+
for field_name in SAMPLING_PARAM_FIELDS:
|
| 588 |
+
try:
|
| 589 |
+
val = getattr(sampling_params, field_name)
|
| 590 |
+
except AttributeError:
|
| 591 |
+
if hasattr(SamplingParams, field_name):
|
| 592 |
+
val = getattr(SamplingParams, field_name)
|
| 593 |
+
else:
|
| 594 |
+
raise
|
| 595 |
+
if isinstance(val, list):
|
| 596 |
+
assert (
|
| 597 |
+
len(val) % sampling_dp == 0
|
| 598 |
+
), f"Sampling param '{field_name}' length {len(val)} not divisible by sampling_dp {sampling_dp}"
|
| 599 |
+
chunked_fields[field_name] = split_list(val, sampling_dp)
|
| 600 |
+
else:
|
| 601 |
+
chunked_fields[field_name] = [val] * sampling_dp
|
| 602 |
+
|
| 603 |
+
return [
|
| 604 |
+
SamplingParams(**{field: chunked_fields[field][i] for field in SAMPLING_PARAM_FIELDS})
|
| 605 |
+
for i in range(sampling_dp)
|
| 606 |
+
]
|
| 607 |
+
|
| 608 |
+
|
| 609 |
+
class SeedManager:
|
| 610 |
+
"""Manages per-user RNG seeds for on-device sampling.
|
| 611 |
+
|
| 612 |
+
Tracks which users have explicit seeds set (``_seed_active``) and avoids
|
| 613 |
+
unnecessary host-to-device copies during decode when no seeds are active.
|
| 614 |
+
"""
|
| 615 |
+
|
| 616 |
+
def __init__(self, tt_sampling, max_batch_size=32):
|
| 617 |
+
self.max_batch_size = max_batch_size
|
| 618 |
+
self.seeds = [None for _ in range(max_batch_size)]
|
| 619 |
+
self.seed_counters = [0 for _ in range(max_batch_size)]
|
| 620 |
+
# Pre-allocate RNG objects; actual request seeds are set via reset_seed().
|
| 621 |
+
self.rngs = [random.Random(secrets.randbits(64)) for _ in range(max_batch_size)]
|
| 622 |
+
self.tt_sampling = tt_sampling
|
| 623 |
+
self._sampling_debug_enabled = getattr(tt_sampling, "_sampling_debug_enabled", False)
|
| 624 |
+
# True when at least one user slot has a non-None request seed.
|
| 625 |
+
self._seed_active = False
|
| 626 |
+
# Set to True by reset_seed() so the next get_new_values() pushes
|
| 627 |
+
# fresh values to the device. When _seed_active is True this pushes
|
| 628 |
+
# per-user seeds; when False it pushes varied per-user
|
| 629 |
+
# values to diversify the device RNG state. Cleared after the push.
|
| 630 |
+
self._reseted = False
|
| 631 |
+
# When True, the next get_new_values() must push MAX_UINT32 (SKIP) so
|
| 632 |
+
# the device transitions from rand_tile_init to rand_tile advance.
|
| 633 |
+
self._needs_skip = False
|
| 634 |
+
# True only for the most recent get_new_values() call when at least
|
| 635 |
+
# one active slot used an explicit request seed.
|
| 636 |
+
self._active_request_seed = False
|
| 637 |
+
# Mesh mapper for sharding seeds across rows when sampling_dp > 1.
|
| 638 |
+
if tt_sampling._sampling_dp > 1:
|
| 639 |
+
self._seed_mapper = ttnn.ShardTensor2dMesh(
|
| 640 |
+
tt_sampling.mesh_device, dims=tt_sampling._param_dims, mesh_shape=tt_sampling.cluster_shape
|
| 641 |
+
)
|
| 642 |
+
else:
|
| 643 |
+
self._seed_mapper = None
|
| 644 |
+
|
| 645 |
+
def _next_unseeded_rng_seed(self) -> int:
|
| 646 |
+
return secrets.randbits(64)
|
| 647 |
+
|
| 648 |
+
def _next_unseeded_device_seed(self) -> int:
|
| 649 |
+
return secrets.randbelow(DEVICE_SEED_MAX) + 1
|
| 650 |
+
|
| 651 |
+
def _next_device_seed_from_rng(self, rng: random.Random) -> int:
|
| 652 |
+
return rng.randint(1, DEVICE_SEED_MAX)
|
| 653 |
+
|
| 654 |
+
def _next_device_seed_for_slot(self, slot: int) -> int:
|
| 655 |
+
request_seed = self.seeds[slot]
|
| 656 |
+
if request_seed is None:
|
| 657 |
+
return self._next_device_seed_from_rng(self.rngs[slot])
|
| 658 |
+
device_seed = _hash_request_seed_to_device_seed(int(request_seed), self.seed_counters[slot])
|
| 659 |
+
self.seed_counters[slot] += 1
|
| 660 |
+
return device_seed
|
| 661 |
+
|
| 662 |
+
def _seed_from_slot_params(self, seeds, slot: int):
|
| 663 |
+
if seeds is None:
|
| 664 |
+
return None
|
| 665 |
+
if isinstance(seeds, torch.Tensor):
|
| 666 |
+
flat = seeds.reshape(-1)
|
| 667 |
+
if slot < 0 or slot >= flat.numel():
|
| 668 |
+
return None
|
| 669 |
+
seed = flat[slot]
|
| 670 |
+
elif isinstance(seeds, list):
|
| 671 |
+
if slot < 0 or slot >= len(seeds):
|
| 672 |
+
return None
|
| 673 |
+
seed = seeds[slot]
|
| 674 |
+
else:
|
| 675 |
+
seed = seeds
|
| 676 |
+
|
| 677 |
+
if seed is None:
|
| 678 |
+
return None
|
| 679 |
+
if isinstance(seed, torch.Tensor):
|
| 680 |
+
if seed.numel() == 0:
|
| 681 |
+
return None
|
| 682 |
+
seed = seed.reshape(-1)[0].item()
|
| 683 |
+
return int(seed)
|
| 684 |
+
|
| 685 |
+
def reset_seed_from_slots(self, seeds, user_ids):
|
| 686 |
+
"""Reset decode seed state from slot-indexed sampling params."""
|
| 687 |
+
if user_ids is None:
|
| 688 |
+
user_ids = range(self.max_batch_size)
|
| 689 |
+
for user in user_ids:
|
| 690 |
+
slot = int(user)
|
| 691 |
+
seed = self._seed_from_slot_params(seeds, slot)
|
| 692 |
+
self.seeds[slot] = seed
|
| 693 |
+
self.seed_counters[slot] = 0
|
| 694 |
+
if seed is None:
|
| 695 |
+
self.rngs[slot].seed(self._next_unseeded_rng_seed())
|
| 696 |
+
else:
|
| 697 |
+
self.rngs[slot].seed(int(seed))
|
| 698 |
+
self._seed_active = any(s is not None for s in self.seeds)
|
| 699 |
+
self._reseted = True
|
| 700 |
+
|
| 701 |
+
def reset_seed_from_slots_if_needed(self, seeds, user_ids) -> bool:
|
| 702 |
+
"""Reset only active slots whose slot-indexed seed changed."""
|
| 703 |
+
if user_ids is None:
|
| 704 |
+
user_ids = range(self.max_batch_size)
|
| 705 |
+
reset_slots = []
|
| 706 |
+
for user in user_ids:
|
| 707 |
+
slot = int(user)
|
| 708 |
+
if self._seed_from_slot_params(seeds, slot) != self.seeds[slot]:
|
| 709 |
+
reset_slots.append(slot)
|
| 710 |
+
if not reset_slots:
|
| 711 |
+
return False
|
| 712 |
+
self.reset_seed_from_slots(seeds, reset_slots)
|
| 713 |
+
return True
|
| 714 |
+
|
| 715 |
+
def align_seed_counters_to_positions(self, seeds, user_ids, positions, offset: int = 1):
|
| 716 |
+
"""Make explicit-seed decode independent of persistent slot lifetime.
|
| 717 |
+
|
| 718 |
+
vLLM can temporarily remove running requests from the persistent batch
|
| 719 |
+
while admitting another prefill batch, then re-add them in different
|
| 720 |
+
slots. For explicit request seeds, deriving the per-token device seed
|
| 721 |
+
from the absolute decode position keeps the stream reproducible even
|
| 722 |
+
when the Python-side slot counter was reset or moved.
|
| 723 |
+
"""
|
| 724 |
+
if positions is None:
|
| 725 |
+
return
|
| 726 |
+
if user_ids is None:
|
| 727 |
+
user_ids = range(self.max_batch_size)
|
| 728 |
+
|
| 729 |
+
if isinstance(positions, torch.Tensor):
|
| 730 |
+
flat_positions = positions.reshape(-1)
|
| 731 |
+
|
| 732 |
+
def _position(slot):
|
| 733 |
+
if slot < 0 or slot >= flat_positions.numel():
|
| 734 |
+
return None
|
| 735 |
+
pos = flat_positions[slot]
|
| 736 |
+
return int(pos.item())
|
| 737 |
+
|
| 738 |
+
elif isinstance(positions, list):
|
| 739 |
+
|
| 740 |
+
def _position(slot):
|
| 741 |
+
if slot < 0 or slot >= len(positions):
|
| 742 |
+
return None
|
| 743 |
+
return int(positions[slot])
|
| 744 |
+
|
| 745 |
+
else:
|
| 746 |
+
|
| 747 |
+
def _position(_slot):
|
| 748 |
+
return int(positions)
|
| 749 |
+
|
| 750 |
+
for user in user_ids:
|
| 751 |
+
slot = int(user)
|
| 752 |
+
seed = self._seed_from_slot_params(seeds, slot)
|
| 753 |
+
if seed is None:
|
| 754 |
+
continue
|
| 755 |
+
position = _position(slot)
|
| 756 |
+
if position is None or position < 0:
|
| 757 |
+
continue
|
| 758 |
+
self.seed_counters[slot] = max(0, position + offset)
|
| 759 |
+
|
| 760 |
+
def has_active_request_seed(self) -> bool:
|
| 761 |
+
return self._active_request_seed
|
| 762 |
+
|
| 763 |
+
def _debug_state(self, slots=None):
|
| 764 |
+
if slots is None:
|
| 765 |
+
slots = range(self.max_batch_size)
|
| 766 |
+
state = []
|
| 767 |
+
for slot in slots:
|
| 768 |
+
slot = int(slot)
|
| 769 |
+
if slot < 0 or slot >= self.max_batch_size:
|
| 770 |
+
continue
|
| 771 |
+
seed = self.seeds[slot]
|
| 772 |
+
if seed is not None:
|
| 773 |
+
state.append((slot, seed))
|
| 774 |
+
return _compact_debug_list(state)
|
| 775 |
+
|
| 776 |
+
def apply_slot_remap(self, remap):
|
| 777 |
+
"""Reindex RNG state after batch condense.
|
| 778 |
+
|
| 779 |
+
``remap`` is a 1-D int tensor of length ``max_batch_size`` where
|
| 780 |
+
``remap[i] = j`` means slot *i* now holds the request that was
|
| 781 |
+
previously at slot *j*. Identity entries (``remap[i] == i``) are
|
| 782 |
+
no-ops. Only non-identity entries trigger a move.
|
| 783 |
+
"""
|
| 784 |
+
if not self._seed_active:
|
| 785 |
+
return
|
| 786 |
+
moves = [(int(remap[i]), i) for i in range(len(remap)) if int(remap[i]) != i]
|
| 787 |
+
if not moves:
|
| 788 |
+
_log_sampling_debug(
|
| 789 |
+
self._sampling_debug_enabled, "SeedManager slot remap identity", seed_active=self._seed_active
|
| 790 |
+
)
|
| 791 |
+
return
|
| 792 |
+
# Snapshot the state we're about to overwrite.
|
| 793 |
+
_log_sampling_debug(
|
| 794 |
+
self._sampling_debug_enabled,
|
| 795 |
+
"SeedManager slot remap",
|
| 796 |
+
moves=_compact_debug_list(moves),
|
| 797 |
+
state_before=self._debug_state(),
|
| 798 |
+
)
|
| 799 |
+
old_seeds = list(self.seeds)
|
| 800 |
+
old_counters = list(self.seed_counters)
|
| 801 |
+
old_rngs = list(self.rngs)
|
| 802 |
+
moved_sources = {old_slot for old_slot, _ in moves}
|
| 803 |
+
moved_destinations = {new_slot for _, new_slot in moves}
|
| 804 |
+
for old_slot, new_slot in moves:
|
| 805 |
+
self.seeds[new_slot] = old_seeds[old_slot]
|
| 806 |
+
self.seed_counters[new_slot] = old_counters[old_slot]
|
| 807 |
+
# copy.copy preserves internal RNG state but creates an
|
| 808 |
+
# independent object so the old slot reference does not alias
|
| 809 |
+
# the new one.
|
| 810 |
+
self.rngs[new_slot] = copy.copy(old_rngs[old_slot])
|
| 811 |
+
for old_slot in moved_sources - moved_destinations:
|
| 812 |
+
self.seeds[old_slot] = None
|
| 813 |
+
self.seed_counters[old_slot] = 0
|
| 814 |
+
self._seed_active = any(s is not None for s in self.seeds)
|
| 815 |
+
_log_sampling_debug(
|
| 816 |
+
self._sampling_debug_enabled,
|
| 817 |
+
"SeedManager slot remap done",
|
| 818 |
+
seed_active=self._seed_active,
|
| 819 |
+
state_after=self._debug_state(),
|
| 820 |
+
)
|
| 821 |
+
|
| 822 |
+
def reset_seed(self, seeds, user_ids):
|
| 823 |
+
"""Update RNG state for the given user slots after a prefill.
|
| 824 |
+
|
| 825 |
+
Args:
|
| 826 |
+
seeds: Seed values in request order. Accepts a list, tensor, scalar,
|
| 827 |
+
or None (treated as all unseeded).
|
| 828 |
+
user_ids: Batch slot indices being prefilled.
|
| 829 |
+
"""
|
| 830 |
+
user_ids = [int(user) for user in user_ids]
|
| 831 |
+
_log_sampling_debug(
|
| 832 |
+
self._sampling_debug_enabled,
|
| 833 |
+
"SeedManager reset prefill",
|
| 834 |
+
user_ids=_compact_debug_list(user_ids),
|
| 835 |
+
requested_seeds=_compact_debug_list(seeds),
|
| 836 |
+
state_before=self._debug_state(user_ids),
|
| 837 |
+
)
|
| 838 |
+
for i, user in enumerate(user_ids):
|
| 839 |
+
slot = int(user)
|
| 840 |
+
seed = self._seed_from_slot_params(seeds, i)
|
| 841 |
+
self.seeds[slot] = seed
|
| 842 |
+
self.seed_counters[slot] = 0
|
| 843 |
+
if seed is None:
|
| 844 |
+
self.rngs[slot].seed(self._next_unseeded_rng_seed())
|
| 845 |
+
else:
|
| 846 |
+
self.rngs[slot].seed(int(seed))
|
| 847 |
+
self._seed_active = any(s is not None for s in self.seeds)
|
| 848 |
+
self._reseted = True
|
| 849 |
+
_log_sampling_debug(
|
| 850 |
+
self._sampling_debug_enabled,
|
| 851 |
+
"SeedManager reset prefill done",
|
| 852 |
+
seed_active=self._seed_active,
|
| 853 |
+
state_after=self._debug_state(user_ids),
|
| 854 |
+
)
|
| 855 |
+
|
| 856 |
+
def get_new_values(self, empty_slots=None, replicate_seeds=False):
|
| 857 |
+
"""Generate and push new seed values to the device.
|
| 858 |
+
|
| 859 |
+
**Seeded path** (``_seed_active=True``):
|
| 860 |
+
Advances each active slot seed state and copies the new values to
|
| 861 |
+
the device every step. Explicit request seeds produce slot-independent
|
| 862 |
+
device seeds derived from the request seed and the slot counter. Some
|
| 863 |
+
decode callers align that counter to the absolute token position so
|
| 864 |
+
vLLM batch-layout changes cannot reset a request's random stream.
|
| 865 |
+
|
| 866 |
+
**Unseeded path** (``_seed_active=False``):
|
| 867 |
+
Uses a three-state machine to ensure each user gets a unique device
|
| 868 |
+
RNG state without redundant host-to-device copies during decode:
|
| 869 |
+
|
| 870 |
+
State 1 - **init** (``_reseted=True``):
|
| 871 |
+
Push varied per-user values from system entropy.
|
| 872 |
+
|
| 873 |
+
State 2 - **transition** (``_needs_skip=True``):
|
| 874 |
+
Push MAX_UINT32 (SKIP) so the device stops reinitializing and
|
| 875 |
+
starts advancing via rand_tile().
|
| 876 |
+
|
| 877 |
+
State 3 - **steady** (both flags clear):
|
| 878 |
+
Early-return with no device copy.
|
| 879 |
+
"""
|
| 880 |
+
if empty_slots is None:
|
| 881 |
+
empty_slots = list(range(self.max_batch_size))
|
| 882 |
+
else:
|
| 883 |
+
empty_slots = [int(slot) for slot in empty_slots]
|
| 884 |
+
empty_slot_set = set(empty_slots)
|
| 885 |
+
self._active_request_seed = any(self.seeds[i] is not None for i in empty_slot_set)
|
| 886 |
+
|
| 887 |
+
if not self._seed_active:
|
| 888 |
+
self._active_request_seed = False
|
| 889 |
+
if self._reseted:
|
| 890 |
+
new_seeds = [self._next_unseeded_device_seed() for _ in range(self.max_batch_size)]
|
| 891 |
+
self._needs_skip = True
|
| 892 |
+
elif self._needs_skip:
|
| 893 |
+
new_seeds = [MAX_UINT32] * self.max_batch_size
|
| 894 |
+
self._needs_skip = False
|
| 895 |
+
else:
|
| 896 |
+
# State 3 (steady): device already has SKIP, rand_tile
|
| 897 |
+
# advances on its own, so no host-to-device copy is needed.
|
| 898 |
+
_log_sampling_debug(
|
| 899 |
+
self._sampling_debug_enabled,
|
| 900 |
+
"SeedManager seed update skipped",
|
| 901 |
+
active_slots=_compact_debug_list(empty_slots),
|
| 902 |
+
seed_active=self._seed_active,
|
| 903 |
+
reseted=self._reseted,
|
| 904 |
+
needs_skip=self._needs_skip,
|
| 905 |
+
)
|
| 906 |
+
return
|
| 907 |
+
else:
|
| 908 |
+
new_seeds = [
|
| 909 |
+
self._next_device_seed_for_slot(i) if i in empty_slot_set else MAX_UINT32
|
| 910 |
+
for i in range(self.max_batch_size)
|
| 911 |
+
]
|
| 912 |
+
if replicate_seeds:
|
| 913 |
+
assert len(empty_slots) == 1, "Cannot replicate seeds if empty_slots is not length 1"
|
| 914 |
+
new_seeds = self.max_batch_size * [new_seeds[empty_slots[0]]]
|
| 915 |
+
|
| 916 |
+
_log_sampling_debug(
|
| 917 |
+
self._sampling_debug_enabled,
|
| 918 |
+
"SeedManager seed update",
|
| 919 |
+
active_slots=_compact_debug_list(empty_slots),
|
| 920 |
+
replicate_seeds=replicate_seeds,
|
| 921 |
+
seed_active=self._seed_active,
|
| 922 |
+
reseted=self._reseted,
|
| 923 |
+
needs_skip=self._needs_skip,
|
| 924 |
+
new_device_seeds=_compact_debug_list(new_seeds),
|
| 925 |
+
state_after_counter_advance=self._debug_state(empty_slots),
|
| 926 |
+
)
|
| 927 |
+
|
| 928 |
+
new_seed_tt = ttnn.from_torch(
|
| 929 |
+
torch.tensor(new_seeds), dtype=ttnn.uint32, layout=ttnn.ROW_MAJOR_LAYOUT, mesh_mapper=self._seed_mapper
|
| 930 |
+
)
|
| 931 |
+
ttnn.copy_host_to_device_tensor(new_seed_tt, self.tt_sampling.seeds_tt_tensor)
|
| 932 |
+
self._reseted = False
|
code/models/common/sampling/sampling_params.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
|
| 2 |
+
#
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@dataclass(frozen=True)
|
| 10 |
+
class SamplingParams:
|
| 11 |
+
"""
|
| 12 |
+
Used in Generator decode forward functions for greedy decoding / sampling on device.
|
| 13 |
+
The same data class exists in vLLM at vllm/v1/worker/tt_model_runner.py.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
temperature: float | list[float]
|
| 17 |
+
top_k: int | list[int]
|
| 18 |
+
top_p: float | list[float]
|
| 19 |
+
presence_penalty: float | list[float] = 0.0
|
| 20 |
+
frequency_penalty: float | list[float] = 0.0
|
| 21 |
+
repetition_penalty: float | list[float] = 1.0
|
| 22 |
+
seed: int | list[int] | None = None
|
| 23 |
+
enable_log_probs: bool | list[bool] = False
|
| 24 |
+
num_logprobs: int | list[int] = 0
|
code/models/common/sampling/tt_log_probs.py
ADDED
|
@@ -0,0 +1,713 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
|
| 2 |
+
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
|
| 5 |
+
import inspect
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from typing import Optional
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from loguru import logger
|
| 11 |
+
|
| 12 |
+
import ttnn
|
| 13 |
+
from models.common.sampling._utils import filter_none
|
| 14 |
+
|
| 15 |
+
# Maximum number of top logprobs that can be requested (OpenAI API limit)
|
| 16 |
+
MAX_TOP_LOGPROBS = 20
|
| 17 |
+
# Number of top logprobs computed on device (gathered top-k from all devices)
|
| 18 |
+
DEVICE_TOP_K = 32
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class LogProbsResult:
|
| 23 |
+
"""Result of log-probs calculation for a batch.
|
| 24 |
+
|
| 25 |
+
Contains logprobs and global indices for the gathered top-k tokens across all
|
| 26 |
+
devices. The sampled token is always part of the gathered top-k (it was selected
|
| 27 |
+
from them by ttnn.sampling), so its logprob can be looked up by matching its
|
| 28 |
+
index in ``topk_indices``.
|
| 29 |
+
|
| 30 |
+
Attributes:
|
| 31 |
+
topk_logprobs: Optional ttnn.Tensor of shape (1, 1, batch_size, DEVICE_TOP_K)
|
| 32 |
+
containing logprobs for the gathered top-k tokens.
|
| 33 |
+
topk_indices: Optional ttnn.Tensor of shape (1, 1, batch_size, DEVICE_TOP_K)
|
| 34 |
+
containing global vocabulary indices for the gathered top-k tokens.
|
| 35 |
+
topk_logprobs_host: Optional ttnn.Tensor on host of shape (1, 1, batch_size, DEVICE_TOP_K)
|
| 36 |
+
containing logprobs for the gathered top-k tokens after moving to host.
|
| 37 |
+
topk_indices_host: Optional ttnn.Tensor on host of shape (1, 1, batch_size, DEVICE_TOP_K)
|
| 38 |
+
containing global vocabulary indices for the gathered top-k tokens after moving to host.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
topk_logprobs: Optional[ttnn.Tensor]
|
| 42 |
+
topk_indices: Optional[ttnn.Tensor]
|
| 43 |
+
topk_logprobs_host: Optional[ttnn.Tensor]
|
| 44 |
+
topk_indices_host: Optional[ttnn.Tensor]
|
| 45 |
+
|
| 46 |
+
def cpu(self, blocking: bool = True) -> "LogProbsResult":
|
| 47 |
+
"""Transfer device tensors to host CPU.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
blocking: If True, wait for the host tensor to be ready before returning.
|
| 51 |
+
|
| 52 |
+
Returns:
|
| 53 |
+
A new LogProbsResult so that each iteration gets its own host
|
| 54 |
+
tensor references (avoids race with trace-captured singletons).
|
| 55 |
+
"""
|
| 56 |
+
return LogProbsResult(
|
| 57 |
+
topk_logprobs=None,
|
| 58 |
+
topk_indices=None,
|
| 59 |
+
topk_logprobs_host=self.topk_logprobs.cpu(blocking=blocking),
|
| 60 |
+
topk_indices_host=self.topk_indices.cpu(blocking=blocking),
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
def extract_user(self, user_batch_idx: int) -> "LogProbsResult":
|
| 64 |
+
"""Extract a single user's top-K logprobs from a batched host result.
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
user_batch_idx: Index of the user within the batch dimension.
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
A new LogProbsResult with host tensors sliced to shape (1, DEVICE_TOP_K).
|
| 71 |
+
"""
|
| 72 |
+
lp_torch = ttnn.to_torch(ttnn.get_device_tensors(self.topk_logprobs_host)[0])
|
| 73 |
+
idx_torch = ttnn.to_torch(ttnn.get_device_tensors(self.topk_indices_host)[0])
|
| 74 |
+
return LogProbsResult(
|
| 75 |
+
topk_logprobs=None,
|
| 76 |
+
topk_indices=None,
|
| 77 |
+
topk_logprobs_host=lp_torch[0, 0, user_batch_idx, :].unsqueeze(0),
|
| 78 |
+
topk_indices_host=idx_torch[0, 0, user_batch_idx, :].unsqueeze(0),
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
def to_torch_pair(self):
|
| 82 |
+
"""Convert host tensors to a (logprobs, indices) pair of torch tensors.
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
Tuple of (logprobs_tensor, indices_tensor), each of shape (DEVICE_TOP_K,).
|
| 86 |
+
"""
|
| 87 |
+
lp = self.topk_logprobs_host
|
| 88 |
+
idx = self.topk_indices_host
|
| 89 |
+
if isinstance(lp, torch.Tensor):
|
| 90 |
+
return lp.reshape(-1)[:DEVICE_TOP_K].float(), idx.reshape(-1)[:DEVICE_TOP_K].int()
|
| 91 |
+
return ttnn.to_torch(lp).reshape(-1)[:DEVICE_TOP_K].float(), ttnn.to_torch(idx).reshape(-1)[:DEVICE_TOP_K].int()
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def reformat_logprobs(output_log_probs, batch_size):
|
| 95 |
+
"""Convert a list of per-user logprobs into the format expected by vLLM.
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
output_log_probs: List of LogProbsResult, torch.Tensor, float, int, or None per user.
|
| 99 |
+
batch_size: Total batch size.
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
For new path (LogProbsResult): tuple of (topk_lp[B, DEVICE_TOP_K], topk_idx[B, DEVICE_TOP_K])
|
| 103 |
+
For old path: flat tensor of shape [B]
|
| 104 |
+
"""
|
| 105 |
+
has_logprobs_result = any(isinstance(lp, LogProbsResult) for lp in output_log_probs)
|
| 106 |
+
if has_logprobs_result:
|
| 107 |
+
all_lp = torch.zeros(batch_size, DEVICE_TOP_K, dtype=torch.float32)
|
| 108 |
+
all_idx = torch.zeros(batch_size, DEVICE_TOP_K, dtype=torch.int32)
|
| 109 |
+
for i, lp in enumerate(output_log_probs):
|
| 110 |
+
if isinstance(lp, LogProbsResult) and lp.topk_logprobs_host is not None:
|
| 111 |
+
all_lp[i], all_idx[i] = lp.to_torch_pair()
|
| 112 |
+
return (all_lp, all_idx)
|
| 113 |
+
else:
|
| 114 |
+
flat_lp = torch.ones(batch_size, dtype=torch.float32)
|
| 115 |
+
for i, lp in enumerate(output_log_probs):
|
| 116 |
+
if lp is not None and isinstance(lp, (torch.Tensor, float, int)):
|
| 117 |
+
flat_lp[i] = float(lp)
|
| 118 |
+
return flat_lp
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
class LogProbsCalculator:
|
| 122 |
+
"""
|
| 123 |
+
Class to calculate log-probs for a given logits tensor and indices tensor.
|
| 124 |
+
|
| 125 |
+
Supports two modes:
|
| 126 |
+
- Old mode (backward compat): calculate_log_probs() returns single sampled-token logprob
|
| 127 |
+
- New mode (gpt-oss-120b): calculate_topk_log_probs() returns top-32 logprobs + indices
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
mesh_device: MeshDevice to use for all-gather operations
|
| 131 |
+
sub_core_grids: Sub-core grid configuration for operations (optional)
|
| 132 |
+
tt_ccl: CCL object for distributed operations (optional)
|
| 133 |
+
batch_size: Maximum batch size for log-probs calculation (default: 32)
|
| 134 |
+
use_topk_logprobs: If True, allocate tensors for new top-K path instead of old path
|
| 135 |
+
"""
|
| 136 |
+
|
| 137 |
+
def __init__(
|
| 138 |
+
self,
|
| 139 |
+
mesh_device: ttnn.MeshDevice,
|
| 140 |
+
sub_core_grids: ttnn.CoreRangeSet = None,
|
| 141 |
+
tt_ccl=None,
|
| 142 |
+
batch_size: int = 32,
|
| 143 |
+
use_topk_logprobs: bool = False,
|
| 144 |
+
):
|
| 145 |
+
self.global_max = None
|
| 146 |
+
self.global_exp_sum = None
|
| 147 |
+
self.mesh_device = mesh_device
|
| 148 |
+
self.enable_log_probs = False # default to False
|
| 149 |
+
# Per-user boolean array tracking which users have logprobs enabled
|
| 150 |
+
self.logprobs_enabled = [False] * batch_size
|
| 151 |
+
# Per-user integer array tracking how many top logprobs each user requested (0-20)
|
| 152 |
+
self.num_logprobs = [0] * batch_size
|
| 153 |
+
# Flag: True when at least one user needs top-k logprobs (num_logprobs > 0)
|
| 154 |
+
self.topk_logprobs_needed = False
|
| 155 |
+
self.cluster_shape = list(mesh_device.shape)
|
| 156 |
+
self.sub_core_grids = sub_core_grids
|
| 157 |
+
self.tt_ccl = tt_ccl
|
| 158 |
+
self.batch_size = batch_size
|
| 159 |
+
self._use_topk_logprobs = use_topk_logprobs
|
| 160 |
+
self.common_args = filter_none(
|
| 161 |
+
{
|
| 162 |
+
"sub_core_grids": sub_core_grids,
|
| 163 |
+
}
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
# CCL introspection (same pattern as TTSampling)
|
| 167 |
+
self._line_all_gather = getattr(self.tt_ccl, "line_all_gather", None)
|
| 168 |
+
self._line_all_gather_supports_buffer_key = False
|
| 169 |
+
if callable(self._line_all_gather):
|
| 170 |
+
try:
|
| 171 |
+
sig = inspect.signature(self._line_all_gather)
|
| 172 |
+
params = sig.parameters
|
| 173 |
+
self._line_all_gather_supports_buffer_key = "buffer_key" in params or any(
|
| 174 |
+
p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()
|
| 175 |
+
)
|
| 176 |
+
except (TypeError, ValueError):
|
| 177 |
+
logger.warning("Unable to inspect line_all_gather signature; assuming no buffer_key support.")
|
| 178 |
+
|
| 179 |
+
num_devices = self.mesh_device.get_num_devices()
|
| 180 |
+
|
| 181 |
+
# Determine the TP dimension: for 2D meshes, logits are sharded across
|
| 182 |
+
# the larger dimension (TP). For 1D or single-device, use all devices.
|
| 183 |
+
if self.cluster_shape[0] > 1 and self.cluster_shape[1] > 1:
|
| 184 |
+
# 2D mesh: TP axis is the larger dimension
|
| 185 |
+
tp_axis = 0 if self.cluster_shape[0] >= self.cluster_shape[1] else 1
|
| 186 |
+
num_devices_for_sharding = self.cluster_shape[tp_axis]
|
| 187 |
+
self._all_gather_cluster_axis = tp_axis
|
| 188 |
+
elif num_devices > 1:
|
| 189 |
+
# 1D mesh
|
| 190 |
+
num_devices_for_sharding = num_devices
|
| 191 |
+
self._all_gather_cluster_axis = None
|
| 192 |
+
else:
|
| 193 |
+
# Single device
|
| 194 |
+
num_devices_for_sharding = num_devices
|
| 195 |
+
self._all_gather_cluster_axis = None
|
| 196 |
+
|
| 197 |
+
self.num_devices_for_sharding = num_devices_for_sharding
|
| 198 |
+
|
| 199 |
+
# Initialize tensors based on mode
|
| 200 |
+
# Old path tensors (backward compat for non-gpt-oss models)
|
| 201 |
+
self.mask = None
|
| 202 |
+
self.output_tensor = None
|
| 203 |
+
# New path tensors (gpt-oss-120b top-K logprobs)
|
| 204 |
+
self.topk_logprobs_output = None
|
| 205 |
+
self.topk_indices_output = None
|
| 206 |
+
|
| 207 |
+
if use_topk_logprobs:
|
| 208 |
+
# New path: allocate top-K output tensors
|
| 209 |
+
self.topk_logprobs_output = ttnn.as_tensor(
|
| 210 |
+
torch.zeros(1, 1, batch_size, DEVICE_TOP_K),
|
| 211 |
+
dtype=ttnn.bfloat16,
|
| 212 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 213 |
+
device=self.mesh_device,
|
| 214 |
+
layout=ttnn.TILE_LAYOUT,
|
| 215 |
+
mesh_mapper=ttnn.ReplicateTensorToMesh(self.mesh_device),
|
| 216 |
+
)
|
| 217 |
+
self.topk_indices_output = ttnn.as_tensor(
|
| 218 |
+
torch.zeros(1, 1, batch_size, DEVICE_TOP_K, dtype=torch.int32),
|
| 219 |
+
dtype=ttnn.uint32,
|
| 220 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 221 |
+
device=self.mesh_device,
|
| 222 |
+
layout=ttnn.TILE_LAYOUT,
|
| 223 |
+
mesh_mapper=ttnn.ReplicateTensorToMesh(self.mesh_device),
|
| 224 |
+
)
|
| 225 |
+
else:
|
| 226 |
+
# Old path: allocate mask and output tensors
|
| 227 |
+
mask_tensor = (
|
| 228 |
+
torch.arange(num_devices_for_sharding).unsqueeze(1).expand(num_devices_for_sharding, batch_size)
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
if self.cluster_shape[0] > 1 and self.cluster_shape[1] > 1:
|
| 232 |
+
dims = (0, None) if self._all_gather_cluster_axis == 0 else (None, 0)
|
| 233 |
+
mesh_mapper = ttnn.ShardTensor2dMesh(self.mesh_device, dims=dims, mesh_shape=self.cluster_shape)
|
| 234 |
+
elif num_devices > 1:
|
| 235 |
+
mesh_mapper = ttnn.ShardTensorToMesh(self.mesh_device, dim=0)
|
| 236 |
+
else:
|
| 237 |
+
mesh_mapper = ttnn.ReplicateTensorToMesh(self.mesh_device)
|
| 238 |
+
|
| 239 |
+
self.mask = ttnn.as_tensor(
|
| 240 |
+
mask_tensor,
|
| 241 |
+
dtype=ttnn.bfloat16,
|
| 242 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 243 |
+
device=self.mesh_device,
|
| 244 |
+
layout=ttnn.ROW_MAJOR_LAYOUT,
|
| 245 |
+
preprocess=lambda x: x.to(torch.bfloat16),
|
| 246 |
+
mesh_mapper=mesh_mapper,
|
| 247 |
+
)
|
| 248 |
+
self.output_tensor = ttnn.as_tensor(
|
| 249 |
+
torch.ones(1, 1, 1, batch_size),
|
| 250 |
+
dtype=ttnn.bfloat16,
|
| 251 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 252 |
+
device=self.mesh_device,
|
| 253 |
+
layout=ttnn.TILE_LAYOUT,
|
| 254 |
+
mesh_mapper=ttnn.ReplicateTensorToMesh(self.mesh_device),
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
def _perform_all_gather(self, tensor: ttnn.Tensor, dim: int, num_links: int, buffer_key: str = None):
|
| 258 |
+
if callable(self._line_all_gather):
|
| 259 |
+
kwargs = {
|
| 260 |
+
"dim": dim,
|
| 261 |
+
"num_links": num_links,
|
| 262 |
+
"memory_config": tensor.memory_config(),
|
| 263 |
+
"cluster_axis": self._all_gather_cluster_axis,
|
| 264 |
+
}
|
| 265 |
+
if self._line_all_gather_supports_buffer_key and buffer_key is not None:
|
| 266 |
+
kwargs["buffer_key"] = buffer_key
|
| 267 |
+
return self._line_all_gather(tensor, **kwargs)
|
| 268 |
+
|
| 269 |
+
return ttnn.all_gather(
|
| 270 |
+
tensor,
|
| 271 |
+
dim=dim,
|
| 272 |
+
num_links=num_links,
|
| 273 |
+
memory_config=tensor.memory_config(),
|
| 274 |
+
cluster_axis=self._all_gather_cluster_axis,
|
| 275 |
+
topology=ttnn.Topology.Linear,
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
def set_log_probs_mode(
|
| 279 |
+
self,
|
| 280 |
+
enable_log_probs: bool | list[bool] = False,
|
| 281 |
+
num_logprobs: int | list[int] | None = None,
|
| 282 |
+
empty_slots: list[int] | None = None,
|
| 283 |
+
):
|
| 284 |
+
"""Set logprobs mode for the current batch.
|
| 285 |
+
|
| 286 |
+
Args:
|
| 287 |
+
enable_log_probs: Boolean or per-user boolean list. If any user has logprobs
|
| 288 |
+
enabled, the entire batch runs logprobs computation.
|
| 289 |
+
num_logprobs: Integer or per-user integer list (0-20). Specifies how many
|
| 290 |
+
top logprobs to return per user. 0 means sampled token logprob only.
|
| 291 |
+
Values > 0 trigger top-k logprobs computation on device.
|
| 292 |
+
empty_slots: Optional list of batch indices at which to apply the new
|
| 293 |
+
logprobs settings. When provided, only those positions are updated
|
| 294 |
+
and the rest of the batch retains its previous values.
|
| 295 |
+
"""
|
| 296 |
+
if empty_slots is not None:
|
| 297 |
+
# Partial update: only modify the specified batch positions
|
| 298 |
+
if isinstance(enable_log_probs, list):
|
| 299 |
+
for i, slot in enumerate(empty_slots):
|
| 300 |
+
self.logprobs_enabled[slot] = enable_log_probs[i]
|
| 301 |
+
else:
|
| 302 |
+
for slot in empty_slots:
|
| 303 |
+
self.logprobs_enabled[slot] = enable_log_probs
|
| 304 |
+
|
| 305 |
+
if num_logprobs is not None:
|
| 306 |
+
if isinstance(num_logprobs, list):
|
| 307 |
+
for i, slot in enumerate(empty_slots):
|
| 308 |
+
self.num_logprobs[slot] = num_logprobs[i]
|
| 309 |
+
else:
|
| 310 |
+
for slot in empty_slots:
|
| 311 |
+
self.num_logprobs[slot] = num_logprobs
|
| 312 |
+
else:
|
| 313 |
+
# Full batch update
|
| 314 |
+
if isinstance(enable_log_probs, list):
|
| 315 |
+
self.logprobs_enabled = list(enable_log_probs)
|
| 316 |
+
else:
|
| 317 |
+
self.logprobs_enabled = [enable_log_probs] * self.batch_size
|
| 318 |
+
|
| 319 |
+
if num_logprobs is not None:
|
| 320 |
+
if isinstance(num_logprobs, list):
|
| 321 |
+
self.num_logprobs = list(num_logprobs)
|
| 322 |
+
else:
|
| 323 |
+
self.num_logprobs = [num_logprobs] * self.batch_size
|
| 324 |
+
else:
|
| 325 |
+
self.num_logprobs = [0] * self.batch_size
|
| 326 |
+
|
| 327 |
+
# Recompute derived flags from the full arrays
|
| 328 |
+
self.enable_log_probs = any(self.logprobs_enabled)
|
| 329 |
+
# Top-K computation is needed whenever logprobs are enabled (even with
|
| 330 |
+
# num_logprobs=0) because the sampled token's logprob is extracted from
|
| 331 |
+
# the top-K results in the new path.
|
| 332 |
+
self.topk_logprobs_needed = self.enable_log_probs
|
| 333 |
+
|
| 334 |
+
def _compute_global_stats(
|
| 335 |
+
self,
|
| 336 |
+
logits_tensor: ttnn.Tensor,
|
| 337 |
+
):
|
| 338 |
+
"""
|
| 339 |
+
To calculate log-probs, we need to calculate the global max and global sum(exp(logits - global_max)) for each chip.
|
| 340 |
+
This is done by all-gathering the max and sum(exp(logits - global_max)) for each chip and then taking the max and sum of the gathered tensors.
|
| 341 |
+
log-prob formula: log-prob(x) = logits(x) - global_max - log(sum(exp(logits - global_max)))
|
| 342 |
+
|
| 343 |
+
Args:
|
| 344 |
+
logits_tensor (ttnn.Tensor): Logits as model output (1, 1, batch_size, vocab_size_per_device)
|
| 345 |
+
"""
|
| 346 |
+
# Calculate local max
|
| 347 |
+
local_max_tensor = ttnn.max(logits_tensor, dim=-1, keepdim=True, **self.common_args)
|
| 348 |
+
|
| 349 |
+
gathered_max_tensors = self._perform_all_gather(
|
| 350 |
+
local_max_tensor,
|
| 351 |
+
dim=1,
|
| 352 |
+
num_links=1,
|
| 353 |
+
buffer_key="LOGPROBS_MAX_REDUCTION",
|
| 354 |
+
)
|
| 355 |
+
# Convert to ROW_MAJOR_LAYOUT due to memory clobbering which affects all ttnn.reshape ops with TILE_LAYOUT
|
| 356 |
+
gathered_max_tensors = ttnn.to_layout(gathered_max_tensors, ttnn.ROW_MAJOR_LAYOUT, **self.common_args)
|
| 357 |
+
ttnn.deallocate(local_max_tensor)
|
| 358 |
+
D = self.num_devices_for_sharding
|
| 359 |
+
B = gathered_max_tensors.shape[2]
|
| 360 |
+
gathered_max_tensors = ttnn.reshape(gathered_max_tensors, (1, 1, D, B), **self.common_args)
|
| 361 |
+
gathered_max_tensors = ttnn.to_layout(gathered_max_tensors, ttnn.TILE_LAYOUT, **self.common_args)
|
| 362 |
+
|
| 363 |
+
self.global_max = ttnn.max(gathered_max_tensors, dim=2, keepdim=True, **self.common_args)
|
| 364 |
+
|
| 365 |
+
global_max_to_subtract = ttnn.to_layout(self.global_max, ttnn.ROW_MAJOR_LAYOUT, **self.common_args)
|
| 366 |
+
global_max_to_subtract = ttnn.reshape(global_max_to_subtract, (1, 1, B, 1), **self.common_args)
|
| 367 |
+
global_max_to_subtract = ttnn.to_layout(global_max_to_subtract, ttnn.TILE_LAYOUT, **self.common_args)
|
| 368 |
+
|
| 369 |
+
# Calculate stable local sum-exp using subtract of global-max from each local logit
|
| 370 |
+
subtracted_tensor = ttnn.subtract(logits_tensor, global_max_to_subtract, **self.common_args)
|
| 371 |
+
exp_tensor = ttnn.exp(subtracted_tensor, **self.common_args)
|
| 372 |
+
ttnn.deallocate(global_max_to_subtract)
|
| 373 |
+
ttnn.deallocate(subtracted_tensor)
|
| 374 |
+
sum_exp_tensor = ttnn.sum(exp_tensor, dim=-1, keepdim=True, **self.common_args)
|
| 375 |
+
ttnn.deallocate(exp_tensor)
|
| 376 |
+
|
| 377 |
+
gathered_sum_exp_tensors = self._perform_all_gather(
|
| 378 |
+
sum_exp_tensor,
|
| 379 |
+
dim=1,
|
| 380 |
+
num_links=1,
|
| 381 |
+
buffer_key="LOGPROBS_SUM_EXP_REDUCTION",
|
| 382 |
+
)
|
| 383 |
+
gathered_sum_exp_tensors = ttnn.to_layout(gathered_sum_exp_tensors, ttnn.ROW_MAJOR_LAYOUT, **self.common_args)
|
| 384 |
+
ttnn.deallocate(sum_exp_tensor)
|
| 385 |
+
B_sum = gathered_sum_exp_tensors.shape[2]
|
| 386 |
+
gathered_sum_exp_tensors = ttnn.reshape(gathered_sum_exp_tensors, (1, 1, D, B_sum), **self.common_args)
|
| 387 |
+
gathered_sum_exp_tensors = ttnn.to_layout(gathered_sum_exp_tensors, ttnn.TILE_LAYOUT, **self.common_args)
|
| 388 |
+
|
| 389 |
+
self.global_exp_sum = ttnn.sum(gathered_sum_exp_tensors, dim=2, keepdim=True, **self.common_args)
|
| 390 |
+
ttnn.deallocate(gathered_sum_exp_tensors)
|
| 391 |
+
|
| 392 |
+
def _is_supported(self):
|
| 393 |
+
"""Check if logprobs computation is supported on this device configuration."""
|
| 394 |
+
num_devices = self.mesh_device.get_num_devices()
|
| 395 |
+
if num_devices not in (8, 32):
|
| 396 |
+
return False
|
| 397 |
+
if self.num_devices_for_sharding < 2:
|
| 398 |
+
return False
|
| 399 |
+
return True
|
| 400 |
+
|
| 401 |
+
# -----------------------------------------------------------------------
|
| 402 |
+
# Old path (backward compat for non-gpt-oss models)
|
| 403 |
+
# -----------------------------------------------------------------------
|
| 404 |
+
|
| 405 |
+
def _prepare_relevant_logits(self, logits_tensor: ttnn.Tensor, global_idx_tensor: ttnn.Tensor):
|
| 406 |
+
"""
|
| 407 |
+
Prepare global idx tensor with correct values on all devices.
|
| 408 |
+
"""
|
| 409 |
+
size_per_device = logits_tensor.shape[-1]
|
| 410 |
+
|
| 411 |
+
# convert global_idx_tensor to ttnn.TILE_LAYOUT
|
| 412 |
+
global_idx_tilized_tensor = ttnn.to_layout(global_idx_tensor, ttnn.TILE_LAYOUT, **self.common_args)
|
| 413 |
+
|
| 414 |
+
# TODO: Raise an issue on this since for UINT_32 ttnn.div produces incorrect output (all zeros)
|
| 415 |
+
global_idx_tilized_tensor = ttnn.typecast(global_idx_tilized_tensor, ttnn.float32, **self.common_args)
|
| 416 |
+
|
| 417 |
+
# Get chip_id for each user based on global_idx values in global_idx_tensor
|
| 418 |
+
chip_ids_tensor = ttnn.div(
|
| 419 |
+
global_idx_tilized_tensor,
|
| 420 |
+
size_per_device,
|
| 421 |
+
rounding_mode="floor",
|
| 422 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 423 |
+
**self.common_args,
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
# Get local index for each user based on global_idx values in global_idx_tensor
|
| 427 |
+
remainder_tensor = ttnn.remainder(
|
| 428 |
+
global_idx_tilized_tensor,
|
| 429 |
+
size_per_device,
|
| 430 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 431 |
+
**self.common_args,
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
# Convert remainder_tensor to int32
|
| 435 |
+
remainder_tensor = ttnn.typecast(remainder_tensor, ttnn.uint32, **self.common_args)
|
| 436 |
+
# convert to ROW_MAJOR_LAYOUT due to memory clobbering which affects all ttnn.reshape ops with TILE_LAYOUT
|
| 437 |
+
remainder_tensor = ttnn.to_layout(remainder_tensor, ttnn.ROW_MAJOR_LAYOUT, **self.common_args)
|
| 438 |
+
batch_vol = remainder_tensor.shape[2] * remainder_tensor.shape[3]
|
| 439 |
+
remainder_tensor = ttnn.reshape(remainder_tensor, (1, 1, batch_vol, 1), **self.common_args)
|
| 440 |
+
remainder_tensor = ttnn.to_layout(remainder_tensor, ttnn.TILE_LAYOUT, **self.common_args)
|
| 441 |
+
|
| 442 |
+
# Get logits for each user on each chip based on local index
|
| 443 |
+
selected_logits_tensor = ttnn.gather(logits_tensor, dim=3, index=remainder_tensor, **self.common_args)
|
| 444 |
+
|
| 445 |
+
# convert to ROW_MAJOR_LAYOUT due to memory clobbering which affects all ttnn.reshape ops with TILE_LAYOUT
|
| 446 |
+
selected_logits_tensor = ttnn.to_layout(selected_logits_tensor, ttnn.ROW_MAJOR_LAYOUT, **self.common_args)
|
| 447 |
+
batch_vol_s = selected_logits_tensor.shape[2] * selected_logits_tensor.shape[3]
|
| 448 |
+
selected_logits_tensor = ttnn.reshape(selected_logits_tensor, (1, 1, 1, batch_vol_s), **self.common_args)
|
| 449 |
+
selected_logits_tensor = ttnn.to_layout(selected_logits_tensor, ttnn.TILE_LAYOUT, **self.common_args)
|
| 450 |
+
# Compare mask to chip_ids tensor and select correct positions for each user on all chips inplace
|
| 451 |
+
ttnn.eq_(chip_ids_tensor, self.mask, **self.common_args)
|
| 452 |
+
|
| 453 |
+
# Multiply selected_logits_tensor with chip_ids_tensor to get expected logits for each user
|
| 454 |
+
selected_logits_tensor = ttnn.multiply(selected_logits_tensor, chip_ids_tensor, **self.common_args)
|
| 455 |
+
|
| 456 |
+
# All gather logits across all devices
|
| 457 |
+
selected_logits_tensor = self._perform_all_gather(
|
| 458 |
+
selected_logits_tensor,
|
| 459 |
+
dim=1,
|
| 460 |
+
num_links=1,
|
| 461 |
+
buffer_key="LOGPROBS_LOGITS",
|
| 462 |
+
)
|
| 463 |
+
|
| 464 |
+
selected_logits_tensor = ttnn.to_layout(selected_logits_tensor, ttnn.ROW_MAJOR_LAYOUT, **self.common_args)
|
| 465 |
+
D_s = self.num_devices_for_sharding
|
| 466 |
+
B_g = selected_logits_tensor.shape[3]
|
| 467 |
+
selected_logits_tensor = ttnn.reshape(selected_logits_tensor, (1, 1, D_s, B_g), **self.common_args)
|
| 468 |
+
selected_logits_tensor = ttnn.to_layout(selected_logits_tensor, ttnn.TILE_LAYOUT, **self.common_args)
|
| 469 |
+
|
| 470 |
+
# Apply sum over device dimension to get logits for each user on all chips
|
| 471 |
+
selected_logits_tensor = ttnn.sum(selected_logits_tensor, dim=2, keepdim=True, **self.common_args)
|
| 472 |
+
|
| 473 |
+
return selected_logits_tensor
|
| 474 |
+
|
| 475 |
+
def _calculate_log_probs(self, sampled_logits_tensor: ttnn.Tensor):
|
| 476 |
+
"""
|
| 477 |
+
Calculate log-probs for a given logits tensor with formula:
|
| 478 |
+
log-prob(x) = logits(x) - global_max - log(global_exp_sum)
|
| 479 |
+
"""
|
| 480 |
+
out = ttnn.subtract(sampled_logits_tensor, self.global_max, **self.common_args)
|
| 481 |
+
log_global_exp_sum = ttnn.log(self.global_exp_sum, **self.common_args)
|
| 482 |
+
# Subtract and put result to self.output_tensor
|
| 483 |
+
ttnn.subtract(out, log_global_exp_sum, output_tensor=self.output_tensor, **self.common_args)
|
| 484 |
+
|
| 485 |
+
def calculate_log_probs(
|
| 486 |
+
self,
|
| 487 |
+
logits_tensor: ttnn.Tensor,
|
| 488 |
+
indices_tensor: ttnn.Tensor,
|
| 489 |
+
):
|
| 490 |
+
"""
|
| 491 |
+
Calculate log-probs for a given logits tensor and indices tensor.
|
| 492 |
+
Returns None if log-probs are not requested, not supported, or the device count is not 8 or 32.
|
| 493 |
+
(Old path — backward compat for non-gpt-oss models)
|
| 494 |
+
"""
|
| 495 |
+
if not self.enable_log_probs:
|
| 496 |
+
return None
|
| 497 |
+
|
| 498 |
+
if not self._is_supported():
|
| 499 |
+
return None
|
| 500 |
+
|
| 501 |
+
# Calculating log-probs requires bfloat16 precision for near-stable sum-exp calculation
|
| 502 |
+
if logits_tensor.dtype == ttnn.bfloat8_b:
|
| 503 |
+
logits_tensor = ttnn.typecast(logits_tensor, ttnn.bfloat16, **self.common_args)
|
| 504 |
+
|
| 505 |
+
# Compute global max and global sum(exp(logits - global_max)) for each chip
|
| 506 |
+
self._compute_global_stats(logits_tensor)
|
| 507 |
+
|
| 508 |
+
# Prepare relevant logits for each user on each chip
|
| 509 |
+
relevant_logits = self._prepare_relevant_logits(logits_tensor, indices_tensor)
|
| 510 |
+
|
| 511 |
+
# Calculate log-probs for each user on each chip and stores in self.output_tensor
|
| 512 |
+
self._calculate_log_probs(relevant_logits)
|
| 513 |
+
|
| 514 |
+
return self.output_tensor
|
| 515 |
+
|
| 516 |
+
# -----------------------------------------------------------------------
|
| 517 |
+
# New path (gpt-oss-120b top-K logprobs)
|
| 518 |
+
# -----------------------------------------------------------------------
|
| 519 |
+
|
| 520 |
+
def _calculate_topk_log_probs_from_values(self, topk_values: ttnn.Tensor):
|
| 521 |
+
"""Compute logprobs for gathered top-k values using pre-computed global stats.
|
| 522 |
+
|
| 523 |
+
Applies the log-softmax formula: logprob = logit - global_max - log(global_exp_sum)
|
| 524 |
+
to each of the gathered top-k values.
|
| 525 |
+
|
| 526 |
+
Args:
|
| 527 |
+
topk_values: Gathered top-k values tensor of shape (1, 1, batch_size, num_topk).
|
| 528 |
+
"""
|
| 529 |
+
B = topk_values.shape[2]
|
| 530 |
+
|
| 531 |
+
# Reshape global_max from (1,1,1,B) to (1,1,B,1) for broadcasting with (1,1,B,K)
|
| 532 |
+
global_max_bcast = ttnn.to_layout(self.global_max, ttnn.ROW_MAJOR_LAYOUT, **self.common_args)
|
| 533 |
+
global_max_bcast = ttnn.reshape(global_max_bcast, (1, 1, B, 1), **self.common_args)
|
| 534 |
+
global_max_bcast = ttnn.to_layout(global_max_bcast, ttnn.TILE_LAYOUT, **self.common_args)
|
| 535 |
+
|
| 536 |
+
# Compute log(global_exp_sum) and reshape for broadcasting
|
| 537 |
+
log_global_exp_sum = ttnn.log(self.global_exp_sum, **self.common_args)
|
| 538 |
+
log_global_exp_sum_bcast = ttnn.to_layout(log_global_exp_sum, ttnn.ROW_MAJOR_LAYOUT, **self.common_args)
|
| 539 |
+
log_global_exp_sum_bcast = ttnn.reshape(log_global_exp_sum_bcast, (1, 1, B, 1), **self.common_args)
|
| 540 |
+
log_global_exp_sum_bcast = ttnn.to_layout(log_global_exp_sum_bcast, ttnn.TILE_LAYOUT, **self.common_args)
|
| 541 |
+
ttnn.deallocate(log_global_exp_sum)
|
| 542 |
+
|
| 543 |
+
# Apply log-softmax formula: logprob = logit - global_max - log(global_exp_sum)
|
| 544 |
+
ttnn.subtract(topk_values, global_max_bcast, output_tensor=self.topk_logprobs_output, **self.common_args)
|
| 545 |
+
ttnn.subtract(
|
| 546 |
+
self.topk_logprobs_output,
|
| 547 |
+
log_global_exp_sum_bcast,
|
| 548 |
+
output_tensor=self.topk_logprobs_output,
|
| 549 |
+
**self.common_args,
|
| 550 |
+
)
|
| 551 |
+
ttnn.deallocate(global_max_bcast)
|
| 552 |
+
ttnn.deallocate(log_global_exp_sum_bcast)
|
| 553 |
+
|
| 554 |
+
return self.topk_logprobs_output
|
| 555 |
+
|
| 556 |
+
def calculate_topk_log_probs(
|
| 557 |
+
self,
|
| 558 |
+
logits_tensor: ttnn.Tensor,
|
| 559 |
+
topk_values: ttnn.Tensor,
|
| 560 |
+
topk_global_indices: ttnn.Tensor,
|
| 561 |
+
sub_core_grid_topk: ttnn.CoreRangeSet = None,
|
| 562 |
+
) -> LogProbsResult | None:
|
| 563 |
+
"""Calculate logprobs for the gathered top-k tokens in a single pass.
|
| 564 |
+
|
| 565 |
+
Args:
|
| 566 |
+
logits_tensor: Full logits tensor, sharded across devices.
|
| 567 |
+
topk_values: Gathered top-k values from all devices. Shape: (1,1,B,256).
|
| 568 |
+
topk_global_indices: Global vocabulary indices for the gathered top-k. Shape: (1,1,B,256).
|
| 569 |
+
sub_core_grid_topk: Sub-core grid for topk operation.
|
| 570 |
+
|
| 571 |
+
Returns:
|
| 572 |
+
LogProbsResult with top-32 logprobs and indices, or None if disabled/unsupported.
|
| 573 |
+
"""
|
| 574 |
+
if not self.enable_log_probs:
|
| 575 |
+
return None
|
| 576 |
+
|
| 577 |
+
if not self._is_supported():
|
| 578 |
+
return None
|
| 579 |
+
|
| 580 |
+
# Ensure bfloat16 precision for numerical stability
|
| 581 |
+
if logits_tensor.dtype == ttnn.bfloat8_b:
|
| 582 |
+
logits_tensor = ttnn.typecast(logits_tensor, ttnn.bfloat16, **self.common_args)
|
| 583 |
+
|
| 584 |
+
# Compute global stats — large intermediates allocated and freed here
|
| 585 |
+
self._compute_global_stats(logits_tensor)
|
| 586 |
+
|
| 587 |
+
# Narrow 256 -> 32 topk values and indices
|
| 588 |
+
topk_local_values, topk_local_indices = ttnn.topk(
|
| 589 |
+
topk_values,
|
| 590 |
+
k=DEVICE_TOP_K,
|
| 591 |
+
dim=-1,
|
| 592 |
+
sub_core_grids=sub_core_grid_topk,
|
| 593 |
+
)
|
| 594 |
+
|
| 595 |
+
# ttnn.gather requires uint32 indices in TILE_LAYOUT
|
| 596 |
+
topk_local_indices = ttnn.typecast(topk_local_indices, ttnn.uint32, **self.common_args)
|
| 597 |
+
topk_local_indices = ttnn.to_layout(topk_local_indices, ttnn.ROW_MAJOR_LAYOUT, **self.common_args)
|
| 598 |
+
topk_local_indices = ttnn.to_layout(topk_local_indices, ttnn.TILE_LAYOUT, **self.common_args)
|
| 599 |
+
ttnn.gather(
|
| 600 |
+
topk_global_indices, dim=-1, index=topk_local_indices, out=self.topk_indices_output, **self.common_args
|
| 601 |
+
)
|
| 602 |
+
ttnn.deallocate(topk_local_indices)
|
| 603 |
+
|
| 604 |
+
# Ensure topk_values is bfloat16 for consistent computation
|
| 605 |
+
if topk_local_values.dtype != ttnn.bfloat16:
|
| 606 |
+
topk_local_values = ttnn.typecast(topk_local_values, ttnn.bfloat16, **self.common_args)
|
| 607 |
+
|
| 608 |
+
# Single-pass logprob computation for all gathered top-k tokens
|
| 609 |
+
self._calculate_topk_log_probs_from_values(topk_local_values)
|
| 610 |
+
ttnn.deallocate(topk_local_values)
|
| 611 |
+
|
| 612 |
+
return LogProbsResult(
|
| 613 |
+
topk_logprobs=self.topk_logprobs_output,
|
| 614 |
+
topk_indices=self.topk_indices_output,
|
| 615 |
+
topk_logprobs_host=None,
|
| 616 |
+
topk_indices_host=None,
|
| 617 |
+
)
|
| 618 |
+
|
| 619 |
+
# -----------------------------------------------------------------------
|
| 620 |
+
# Host transfer helpers
|
| 621 |
+
# -----------------------------------------------------------------------
|
| 622 |
+
|
| 623 |
+
def _build_mesh_composer(self):
|
| 624 |
+
"""Build the appropriate mesh composer for transferring tensors from device to host."""
|
| 625 |
+
if self.cluster_shape[0] > 1 and self.cluster_shape[1] > 1:
|
| 626 |
+
return ttnn.ConcatMesh2dToTensor(
|
| 627 |
+
self.mesh_device,
|
| 628 |
+
dims=(0, 1),
|
| 629 |
+
mesh_shape=self.cluster_shape,
|
| 630 |
+
)
|
| 631 |
+
else:
|
| 632 |
+
return ttnn.ConcatMeshToTensor(self.mesh_device, dim=0)
|
| 633 |
+
|
| 634 |
+
def transfer_logprobs_to_host(
|
| 635 |
+
self,
|
| 636 |
+
log_probs_result: LogProbsResult | None,
|
| 637 |
+
sampled_token_ids: torch.Tensor,
|
| 638 |
+
num_logprobs_per_user: list[int] | None = None,
|
| 639 |
+
) -> list[dict | None]:
|
| 640 |
+
"""Move logprobs from device to host and build per-user response objects.
|
| 641 |
+
|
| 642 |
+
tt-metal path only (standalone demos/tests). vLLM does NOT call this.
|
| 643 |
+
|
| 644 |
+
Args:
|
| 645 |
+
log_probs_result: LogProbsResult from calculate_topk_log_probs.
|
| 646 |
+
sampled_token_ids: Host tensor of sampled token IDs, shape (batch_size,).
|
| 647 |
+
num_logprobs_per_user: Per-user count of top logprobs to return (0-20).
|
| 648 |
+
If None, uses self.num_logprobs.
|
| 649 |
+
|
| 650 |
+
Returns:
|
| 651 |
+
List of length batch_size. Each element is None for users with
|
| 652 |
+
logprobs disabled, otherwise a dict with returned_token and top_logprobs.
|
| 653 |
+
"""
|
| 654 |
+
if log_probs_result is None:
|
| 655 |
+
return [None] * self.batch_size
|
| 656 |
+
|
| 657 |
+
if num_logprobs_per_user is None:
|
| 658 |
+
num_logprobs_per_user = self.num_logprobs
|
| 659 |
+
|
| 660 |
+
mesh_composer = self._build_mesh_composer()
|
| 661 |
+
|
| 662 |
+
topk_logprobs_host = ttnn.to_torch(
|
| 663 |
+
log_probs_result.topk_logprobs_host
|
| 664 |
+
if log_probs_result.topk_logprobs_host is not None
|
| 665 |
+
else log_probs_result.topk_logprobs,
|
| 666 |
+
mesh_composer=mesh_composer,
|
| 667 |
+
)
|
| 668 |
+
topk_indices_host = ttnn.to_torch(
|
| 669 |
+
log_probs_result.topk_indices_host
|
| 670 |
+
if log_probs_result.topk_indices_host is not None
|
| 671 |
+
else log_probs_result.topk_indices,
|
| 672 |
+
mesh_composer=mesh_composer,
|
| 673 |
+
)
|
| 674 |
+
# Remove replicas
|
| 675 |
+
topk_logprobs_host = topk_logprobs_host[0, 0, ...].float()
|
| 676 |
+
topk_indices_host = topk_indices_host[0, 0, ...].to(torch.int32)
|
| 677 |
+
|
| 678 |
+
results: list[dict | None] = []
|
| 679 |
+
for user_idx in range(self.batch_size):
|
| 680 |
+
if not self.logprobs_enabled[user_idx]:
|
| 681 |
+
results.append(None)
|
| 682 |
+
continue
|
| 683 |
+
|
| 684 |
+
sampled_id = int(sampled_token_ids[user_idx].item())
|
| 685 |
+
user_logprobs = topk_logprobs_host[user_idx]
|
| 686 |
+
user_indices = topk_indices_host[user_idx]
|
| 687 |
+
|
| 688 |
+
# Extract sampled token logprob by matching its ID in the top-k
|
| 689 |
+
match_mask = user_indices == sampled_id
|
| 690 |
+
if match_mask.any():
|
| 691 |
+
sampled_logprob = float(user_logprobs[match_mask][0].item())
|
| 692 |
+
else:
|
| 693 |
+
logger.warning(f"Sampled token {sampled_id} not found in top-k for user {user_idx}")
|
| 694 |
+
sampled_logprob = float("nan")
|
| 695 |
+
|
| 696 |
+
n = num_logprobs_per_user[user_idx] if user_idx < len(num_logprobs_per_user) else 0
|
| 697 |
+
|
| 698 |
+
results.append(
|
| 699 |
+
{
|
| 700 |
+
"returned_token": {
|
| 701 |
+
"token_idx": sampled_id,
|
| 702 |
+
"logprob": sampled_logprob,
|
| 703 |
+
},
|
| 704 |
+
"top_logprobs": {
|
| 705 |
+
"token_indices": user_indices[:n].tolist(),
|
| 706 |
+
"logprobs": user_logprobs[:n].tolist(),
|
| 707 |
+
"num_logprobs": n,
|
| 708 |
+
"logprobs_formatted": False,
|
| 709 |
+
},
|
| 710 |
+
}
|
| 711 |
+
)
|
| 712 |
+
|
| 713 |
+
return results
|
code/models/common/sampling/tt_penalties.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
|
| 2 |
+
#
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
"""
|
| 5 |
+
On-device penalties module with persistent buffers, mirroring TTSampling.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
from typing import Any, List, Optional
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
import ttnn
|
| 16 |
+
from models.common.lightweightmodule import LightweightModule
|
| 17 |
+
from models.common.sampling._utils import compact_debug_list as _compact_debug_list
|
| 18 |
+
from models.common.sampling._utils import is_llama33_70b_model
|
| 19 |
+
from models.common.sampling._utils import log_sampling_debug as _log_sampling_debug
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@dataclass
|
| 23 |
+
class PenaltyContext:
|
| 24 |
+
prompt_mask: ttnn.Tensor
|
| 25 |
+
output_mask: ttnn.Tensor
|
| 26 |
+
output_counts: ttnn.Tensor
|
| 27 |
+
output_counts_gathered: ttnn.Tensor
|
| 28 |
+
presence_penalties: ttnn.Tensor
|
| 29 |
+
frequency_penalties: ttnn.Tensor
|
| 30 |
+
repetition_penalties: ttnn.Tensor
|
| 31 |
+
inverse_repetition_penalties: ttnn.Tensor
|
| 32 |
+
sub_core_grids: Any | None = None
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _tokens_debug_summary(tokens: torch.Tensor) -> dict[str, Any]:
|
| 36 |
+
valid = tokens >= 0
|
| 37 |
+
row_lengths = valid.sum(dim=1).tolist()
|
| 38 |
+
flat_valid = tokens[valid]
|
| 39 |
+
if flat_valid.numel() == 0:
|
| 40 |
+
unique_count = 0
|
| 41 |
+
duplicate_count = 0
|
| 42 |
+
head = []
|
| 43 |
+
else:
|
| 44 |
+
unique_count = int(torch.unique(flat_valid).numel())
|
| 45 |
+
duplicate_count = int(flat_valid.numel() - unique_count)
|
| 46 |
+
head = flat_valid[:16].tolist()
|
| 47 |
+
return {
|
| 48 |
+
"shape": list(tokens.shape),
|
| 49 |
+
"dtype": str(tokens.dtype),
|
| 50 |
+
"valid_tokens": int(valid.sum().item()),
|
| 51 |
+
"unique_tokens": unique_count,
|
| 52 |
+
"duplicate_tokens": duplicate_count,
|
| 53 |
+
"row_lengths": _compact_debug_list(row_lengths),
|
| 54 |
+
"head_tokens": head,
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def apply_penalties(logits: ttnn.Tensor, context: Optional[PenaltyContext]) -> ttnn.Tensor:
|
| 59 |
+
if context is None:
|
| 60 |
+
return logits
|
| 61 |
+
|
| 62 |
+
op_kwargs = {"sub_core_grids": context.sub_core_grids} if context.sub_core_grids else {}
|
| 63 |
+
# presence
|
| 64 |
+
presence_term = ttnn.multiply(
|
| 65 |
+
ttnn.typecast(context.output_mask, ttnn.bfloat16, **op_kwargs), context.presence_penalties, **op_kwargs
|
| 66 |
+
)
|
| 67 |
+
presence_term_bf16 = ttnn.typecast(presence_term, ttnn.bfloat16, **op_kwargs)
|
| 68 |
+
logits = ttnn.subtract(logits, presence_term_bf16, output_tensor=logits, **op_kwargs)
|
| 69 |
+
presence_term_bf16.deallocate()
|
| 70 |
+
|
| 71 |
+
# frequency
|
| 72 |
+
output_counts_bf16 = ttnn.typecast(context.output_counts, ttnn.bfloat16, **op_kwargs)
|
| 73 |
+
|
| 74 |
+
freq_term = ttnn.multiply(output_counts_bf16, context.frequency_penalties, **op_kwargs)
|
| 75 |
+
|
| 76 |
+
freq_term_bf16 = ttnn.typecast(freq_term, ttnn.bfloat16, **op_kwargs)
|
| 77 |
+
logits = ttnn.subtract(logits, freq_term_bf16, output_tensor=logits, **op_kwargs)
|
| 78 |
+
freq_term_bf16.deallocate()
|
| 79 |
+
|
| 80 |
+
# repetition
|
| 81 |
+
|
| 82 |
+
# If token appears in prompt or output, apply, otherwise use 1.0 for no-op.
|
| 83 |
+
|
| 84 |
+
combined_mask_int32 = ttnn.add(context.prompt_mask, context.output_mask, **op_kwargs)
|
| 85 |
+
combined_mask = ttnn.typecast(combined_mask_int32, ttnn.bfloat16, **op_kwargs)
|
| 86 |
+
combined_mask_int32.deallocate()
|
| 87 |
+
penalties = ttnn.where(combined_mask, context.repetition_penalties, 1.0, **op_kwargs)
|
| 88 |
+
inverse_penalties = ttnn.where(combined_mask, context.inverse_repetition_penalties, 1.0, **op_kwargs)
|
| 89 |
+
combined_mask.deallocate()
|
| 90 |
+
|
| 91 |
+
# If logits are >0, divide by penalty, otherwise multiply by penalty.
|
| 92 |
+
logits_bf16 = ttnn.typecast(logits, ttnn.bfloat16, **op_kwargs)
|
| 93 |
+
logits_gt1 = ttnn.gt(logits_bf16, 0, **op_kwargs)
|
| 94 |
+
scaling = ttnn.where(logits_gt1, inverse_penalties, penalties, **op_kwargs)
|
| 95 |
+
logits_gt1.deallocate()
|
| 96 |
+
penalties.deallocate()
|
| 97 |
+
inverse_penalties.deallocate()
|
| 98 |
+
logits = ttnn.multiply(logits, scaling, output_tensor=logits, **op_kwargs)
|
| 99 |
+
scaling.deallocate()
|
| 100 |
+
|
| 101 |
+
return logits
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class TTPenalties(LightweightModule):
|
| 105 |
+
"""
|
| 106 |
+
Penalty module with persistent device tensors, similar to TTSampling.
|
| 107 |
+
"""
|
| 108 |
+
|
| 109 |
+
def __init__(self, mesh_device, args):
|
| 110 |
+
super().__init__()
|
| 111 |
+
self.mesh_device = mesh_device
|
| 112 |
+
self._sampling_debug_enabled = is_llama33_70b_model(args)
|
| 113 |
+
self.cluster_shape = mesh_device.shape
|
| 114 |
+
# Floor at 32 so that ROW_MAJOR [batch, vocab] buffers passed to
|
| 115 |
+
# ttnn.tilize always have physical_volume divisible by TILE_HW
|
| 116 |
+
# (32*32 = 1024). 32 * V is 1024-aligned for any 32-aligned V.
|
| 117 |
+
self.max_batch_size = max(getattr(args, "max_batch_size", 32), 32)
|
| 118 |
+
|
| 119 |
+
padded_vocab_size = getattr(args, "padded_vocab_size", None)
|
| 120 |
+
self.vocab_size = padded_vocab_size if padded_vocab_size is not None else args.vocab_size
|
| 121 |
+
|
| 122 |
+
self.sub_core_grids = getattr(args, "sub_core_grids", None)
|
| 123 |
+
self._op_kwargs = {"sub_core_grids": self.sub_core_grids} if self.sub_core_grids else {}
|
| 124 |
+
|
| 125 |
+
# sampling_dp > 1 when multiple mesh rows each sample independently
|
| 126 |
+
# (e.g. GPT-OSS on [4,8] Galaxy: 4 rows × 32 users = 128 total)
|
| 127 |
+
self._sampling_dp = getattr(args, "sampling_dp", 1)
|
| 128 |
+
|
| 129 |
+
# When rows are used for data parallelism (sampling_dp > 1), vocab
|
| 130 |
+
# must be sharded along columns; otherwise pick the larger dimension.
|
| 131 |
+
if self._sampling_dp > 1:
|
| 132 |
+
num_devices = mesh_device.shape[-1]
|
| 133 |
+
else:
|
| 134 |
+
num_devices = max(mesh_device.shape[-1], mesh_device.shape[-2])
|
| 135 |
+
self.num_devices = num_devices
|
| 136 |
+
# Total batch across all rows. Host tensors use this size; after
|
| 137 |
+
# (0, ...) sharding each row gets max_batch_size entries.
|
| 138 |
+
self._total_batch = self.max_batch_size * self._sampling_dp
|
| 139 |
+
|
| 140 |
+
# shard vocab size over larger cluster dim
|
| 141 |
+
if mesh_device.shape[-1] == self.num_devices:
|
| 142 |
+
shard_dims = (None, 1)
|
| 143 |
+
shard_dims_slice = (None, 0)
|
| 144 |
+
else:
|
| 145 |
+
shard_dims = (1, None)
|
| 146 |
+
shard_dims_slice = (0, None)
|
| 147 |
+
|
| 148 |
+
# For row-sharded mode (sampling_dp > 1), also shard the batch dimension
|
| 149 |
+
# across mesh rows so each row gets its own per-user penalty state.
|
| 150 |
+
if self._sampling_dp > 1:
|
| 151 |
+
assert (
|
| 152 |
+
mesh_device.shape[-1] == self.num_devices
|
| 153 |
+
), "Row-sharded penalties require vocab sharding along mesh columns"
|
| 154 |
+
shard_dims = (0, 1) # batch across rows, vocab across cols
|
| 155 |
+
shard_dims_gathered = (0, None) # batch across rows, vocab replicated
|
| 156 |
+
shard_dims_bf16 = (0, None) # per-row penalty params
|
| 157 |
+
per_row_batch = self.max_batch_size # NOT divided: each row gets max_batch_size
|
| 158 |
+
else:
|
| 159 |
+
shard_dims_gathered = (None, None)
|
| 160 |
+
shard_dims_bf16 = None
|
| 161 |
+
per_row_batch = self.max_batch_size
|
| 162 |
+
|
| 163 |
+
self.per_row_batch_size = per_row_batch
|
| 164 |
+
self._shard_dims_gathered = shard_dims_gathered
|
| 165 |
+
|
| 166 |
+
self.prompt_mask = self._alloc_int_buffer(shard_dims=shard_dims)
|
| 167 |
+
self.output_mask = self._alloc_int_buffer(shard_dims=shard_dims)
|
| 168 |
+
self.output_counts_gathered = self._alloc_int_buffer(shard_dims=shard_dims_gathered)
|
| 169 |
+
self.output_counts = self._alloc_int_buffer(shard_dims=shard_dims)
|
| 170 |
+
self._shard_dims_mask = shard_dims
|
| 171 |
+
self.decode_src = self._alloc_int_buffer(
|
| 172 |
+
host=torch.ones(self._total_batch, 1), shard_dims=shard_dims_gathered, layout=ttnn.ROW_MAJOR_LAYOUT
|
| 173 |
+
)
|
| 174 |
+
self.zeros = self._alloc_int_buffer(shard_dims=shard_dims_gathered, layout=ttnn.ROW_MAJOR_LAYOUT)
|
| 175 |
+
self.presence_penalties = self._alloc_bf16_buffer(shard_dims=shard_dims_bf16)
|
| 176 |
+
self.frequency_penalties = self._alloc_bf16_buffer(shard_dims=shard_dims_bf16)
|
| 177 |
+
self.repetition_penalties = self._alloc_bf16_buffer(shard_dims=shard_dims_bf16)
|
| 178 |
+
self.inverse_repetition_penalties = self._alloc_bf16_buffer(shard_dims=shard_dims_bf16)
|
| 179 |
+
|
| 180 |
+
vocab_per_dev = self.vocab_size // self.num_devices
|
| 181 |
+
d = torch.arange(self.num_devices, dtype=torch.int32)
|
| 182 |
+
|
| 183 |
+
# [0, 0, 0, vocab_per_dev, 0, 2*vocab_per_dev, ...]
|
| 184 |
+
start_1d = torch.empty(2 * self.num_devices, dtype=torch.int32)
|
| 185 |
+
start_1d[0::2] = 0
|
| 186 |
+
start_1d[1::2] = d * vocab_per_dev
|
| 187 |
+
|
| 188 |
+
# [batch, vocab_per_dev, batch, 2*vocab_per_dev, ...]
|
| 189 |
+
end_1d = torch.empty(2 * self.num_devices, dtype=torch.int32)
|
| 190 |
+
end_1d[0::2] = per_row_batch # per-row batch size, exclusive
|
| 191 |
+
end_1d[1::2] = (d + 1) * vocab_per_dev # exclusive
|
| 192 |
+
|
| 193 |
+
self.slice_start = ttnn.from_torch(
|
| 194 |
+
start_1d,
|
| 195 |
+
device=self.mesh_device,
|
| 196 |
+
mesh_mapper=ttnn.ShardTensor2dMesh(self.mesh_device, dims=shard_dims_slice, mesh_shape=self.cluster_shape),
|
| 197 |
+
)
|
| 198 |
+
self.slice_end = ttnn.from_torch(
|
| 199 |
+
end_1d,
|
| 200 |
+
device=self.mesh_device,
|
| 201 |
+
mesh_mapper=ttnn.ShardTensor2dMesh(self.mesh_device, dims=shard_dims_slice, mesh_shape=self.cluster_shape),
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
def _alloc_int_buffer(self, shard_dims, host=None, layout=ttnn.TILE_LAYOUT):
|
| 205 |
+
if host is None:
|
| 206 |
+
host = torch.zeros((self._total_batch, self.vocab_size), dtype=torch.int32)
|
| 207 |
+
return ttnn.from_torch(
|
| 208 |
+
host,
|
| 209 |
+
dtype=ttnn.int32,
|
| 210 |
+
layout=layout,
|
| 211 |
+
device=self.mesh_device,
|
| 212 |
+
mesh_mapper=ttnn.ShardTensor2dMesh(self.mesh_device, dims=shard_dims, mesh_shape=self.cluster_shape),
|
| 213 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
def _alloc_bf16_buffer(self, shard_dims=None):
|
| 217 |
+
host = torch.zeros((self._total_batch, 1), dtype=torch.float32)
|
| 218 |
+
if shard_dims is not None:
|
| 219 |
+
return ttnn.from_torch(
|
| 220 |
+
host,
|
| 221 |
+
dtype=ttnn.bfloat16,
|
| 222 |
+
layout=ttnn.TILE_LAYOUT,
|
| 223 |
+
device=self.mesh_device,
|
| 224 |
+
mesh_mapper=ttnn.ShardTensor2dMesh(self.mesh_device, dims=shard_dims, mesh_shape=self.cluster_shape),
|
| 225 |
+
)
|
| 226 |
+
return ttnn.from_torch(host, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT, device=self.mesh_device)
|
| 227 |
+
|
| 228 |
+
def _copy_host_to_device(self, dst: ttnn.Tensor, src: torch.Tensor):
|
| 229 |
+
if self._sampling_dp > 1:
|
| 230 |
+
# For row-sharded buffers, create a properly sharded host tensor
|
| 231 |
+
# so copy_host_to_device_tensor writes per-row shards correctly.
|
| 232 |
+
mapper = ttnn.ShardTensor2dMesh(self.mesh_device, dims=(0, None), mesh_shape=self.cluster_shape)
|
| 233 |
+
src_tt = ttnn.from_torch(src, dtype=dst.dtype, layout=ttnn.TILE_LAYOUT, device=None, mesh_mapper=mapper)
|
| 234 |
+
else:
|
| 235 |
+
src_tt = ttnn.from_torch(src, dtype=dst.dtype, layout=ttnn.TILE_LAYOUT, device=None)
|
| 236 |
+
ttnn.copy_host_to_device_tensor(src_tt, dst)
|
| 237 |
+
|
| 238 |
+
def _copy_int_host_to_device(self, dst: ttnn.Tensor, src: torch.Tensor, shard_dims):
|
| 239 |
+
mapper = ttnn.ShardTensor2dMesh(self.mesh_device, dims=shard_dims, mesh_shape=self.cluster_shape)
|
| 240 |
+
src_tt = ttnn.from_torch(src, dtype=dst.dtype, layout=ttnn.TILE_LAYOUT, device=None, mesh_mapper=mapper)
|
| 241 |
+
ttnn.copy_host_to_device_tensor(src_tt, dst)
|
| 242 |
+
|
| 243 |
+
def _token_counts_host(self, tokens_2d: torch.Tensor) -> torch.Tensor:
|
| 244 |
+
valid = (tokens_2d >= 0) & (tokens_2d < self.vocab_size)
|
| 245 |
+
token_ids = torch.where(valid, tokens_2d, torch.zeros_like(tokens_2d)).to(torch.int64)
|
| 246 |
+
counts = torch.zeros((self._total_batch, self.vocab_size), dtype=torch.int32)
|
| 247 |
+
counts.scatter_add_(1, token_ids, valid.to(torch.int32))
|
| 248 |
+
return counts
|
| 249 |
+
|
| 250 |
+
def reset_params(self, presence: List[float], frequency: List[float], repetition: List[float]):
|
| 251 |
+
presence_tensor = self._pad_params(presence)
|
| 252 |
+
frequency_tensor = self._pad_params(frequency)
|
| 253 |
+
repetition_tensor = self._pad_params(repetition)
|
| 254 |
+
inverse_repetition_tensor = 1 / repetition_tensor
|
| 255 |
+
|
| 256 |
+
self._copy_host_to_device(self.presence_penalties, presence_tensor)
|
| 257 |
+
self._copy_host_to_device(self.frequency_penalties, frequency_tensor)
|
| 258 |
+
self._copy_host_to_device(self.repetition_penalties, repetition_tensor)
|
| 259 |
+
self._copy_host_to_device(self.inverse_repetition_penalties, inverse_repetition_tensor)
|
| 260 |
+
|
| 261 |
+
def _pad_params(self, values: List[float]) -> torch.Tensor:
|
| 262 |
+
tensor = torch.tensor(values, dtype=torch.float32)
|
| 263 |
+
if tensor.numel() < self._total_batch:
|
| 264 |
+
pad_value = tensor[-1] if tensor.numel() > 0 else torch.tensor(0.0)
|
| 265 |
+
pad = pad_value.repeat(self._total_batch - tensor.numel())
|
| 266 |
+
tensor = torch.cat([tensor, pad])
|
| 267 |
+
elif tensor.numel() > self._total_batch:
|
| 268 |
+
tensor = tensor[: self._total_batch]
|
| 269 |
+
return tensor.view(self._total_batch, 1)
|
| 270 |
+
|
| 271 |
+
def _pad_batch_to_max(self, tokens_2d: torch.Tensor, pad_value: int) -> torch.Tensor:
|
| 272 |
+
"""Pad/truncate first dim to _total_batch."""
|
| 273 |
+
if tokens_2d.dim() != 2:
|
| 274 |
+
raise ValueError(f"Expected 2D tensor [B, S], got {tokens_2d.shape}")
|
| 275 |
+
B, S = tokens_2d.shape
|
| 276 |
+
if B < self._total_batch:
|
| 277 |
+
pad = torch.full((self._total_batch - B, S), pad_value, dtype=tokens_2d.dtype)
|
| 278 |
+
return torch.cat([tokens_2d, pad], dim=0)
|
| 279 |
+
if B > self._total_batch:
|
| 280 |
+
return tokens_2d[: self._total_batch]
|
| 281 |
+
return tokens_2d
|
| 282 |
+
|
| 283 |
+
def reset_prompt_tokens(self, prompt_tokens: torch.Tensor):
|
| 284 |
+
prompt_tokens_2d = prompt_tokens.reshape(-1, prompt_tokens.shape[-1])
|
| 285 |
+
prompt_tokens_2d = self._pad_batch_to_max(prompt_tokens_2d, pad_value=-1)
|
| 286 |
+
_log_sampling_debug(
|
| 287 |
+
self._sampling_debug_enabled,
|
| 288 |
+
"TTPenalties reset prompt tokens",
|
| 289 |
+
tokens=_tokens_debug_summary(prompt_tokens_2d),
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
# Build reset masks on host to avoid device scatter_add races on
|
| 293 |
+
# duplicate prompt token ids (common in penalty tests/prompts).
|
| 294 |
+
prompt_counts = self._token_counts_host(prompt_tokens_2d)
|
| 295 |
+
prompt_mask = (prompt_counts > 0).to(torch.int32)
|
| 296 |
+
self._copy_int_host_to_device(self.prompt_mask, prompt_mask, self._shard_dims_mask)
|
| 297 |
+
|
| 298 |
+
def reset_output_tokens(self, tokens=None):
|
| 299 |
+
# ALWAYS reset output buffers to zero first (this is the core accuracy fix from issue #35731)
|
| 300 |
+
# This ensures penalty statistics are cleared between prefill and decode phases
|
| 301 |
+
self.output_mask = ttnn.mul(self.output_mask, 0, output_tensor=self.output_mask, **self._op_kwargs)
|
| 302 |
+
self.output_counts = ttnn.mul(self.output_counts, 0, output_tensor=self.output_counts, **self._op_kwargs)
|
| 303 |
+
self.output_counts_gathered = ttnn.mul(
|
| 304 |
+
self.output_counts_gathered, 0, output_tensor=self.output_counts_gathered, **self._op_kwargs
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
# THEN optionally repopulate if tokens are provided
|
| 308 |
+
if tokens is not None:
|
| 309 |
+
tokens_2d = tokens.reshape(-1, tokens.shape[-1])
|
| 310 |
+
tokens_2d = self._pad_batch_to_max(tokens_2d, pad_value=-1)
|
| 311 |
+
_log_sampling_debug(
|
| 312 |
+
self._sampling_debug_enabled,
|
| 313 |
+
"TTPenalties reset output tokens",
|
| 314 |
+
tokens=_tokens_debug_summary(tokens_2d),
|
| 315 |
+
)
|
| 316 |
+
output_counts = self._token_counts_host(tokens_2d)
|
| 317 |
+
output_mask = (output_counts > 0).to(torch.int32)
|
| 318 |
+
self._copy_int_host_to_device(self.output_counts_gathered, output_counts, self._shard_dims_gathered)
|
| 319 |
+
self._copy_int_host_to_device(self.output_counts, output_counts, self._shard_dims_mask)
|
| 320 |
+
self._copy_int_host_to_device(self.output_mask, output_mask, self._shard_dims_mask)
|
| 321 |
+
else:
|
| 322 |
+
_log_sampling_debug(self._sampling_debug_enabled, "TTPenalties reset output tokens", tokens=None)
|
| 323 |
+
|
| 324 |
+
def update_output_tokens(self, new_tokens):
|
| 325 |
+
# Reshape decode token to [batch, 1] for scatter_add.
|
| 326 |
+
# Non-row-sharded: token shape is [1,1,1,batch] → shape[-1]==batch, shape[-2]==1
|
| 327 |
+
# Row-sharded: token shape is [1,1,batch,1] → shape[-2]==batch, shape[-1]==1
|
| 328 |
+
batch = self.per_row_batch_size
|
| 329 |
+
if (new_tokens.shape[-1] == batch and new_tokens.shape[-2] == 1) or (
|
| 330 |
+
new_tokens.shape[-2] == batch and new_tokens.shape[-1] == 1
|
| 331 |
+
):
|
| 332 |
+
new_tokens = ttnn.reshape(new_tokens, [batch, 1], **self._op_kwargs)
|
| 333 |
+
src = self.decode_src
|
| 334 |
+
else:
|
| 335 |
+
src = self._alloc_int_buffer(
|
| 336 |
+
host=torch.ones(self._total_batch, new_tokens.shape[-1]),
|
| 337 |
+
shard_dims=self._shard_dims_gathered,
|
| 338 |
+
layout=ttnn.ROW_MAJOR_LAYOUT,
|
| 339 |
+
)
|
| 340 |
+
self.token_bin_counts_and_mask(
|
| 341 |
+
new_tokens=new_tokens,
|
| 342 |
+
counts=self.output_counts_gathered,
|
| 343 |
+
src=src,
|
| 344 |
+
counts_sliced=self.output_counts,
|
| 345 |
+
mask=self.output_mask,
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
def token_bin_counts_and_mask(self, new_tokens, src, counts=None, mask=None, counts_sliced=None):
|
| 349 |
+
counts_new = ttnn.scatter_add(self.zeros, 1, new_tokens, src, **self._op_kwargs)
|
| 350 |
+
|
| 351 |
+
new_tokens.deallocate()
|
| 352 |
+
# need to use use_low_perf because llama galaxy runs out of L1 otherwise
|
| 353 |
+
counts_new = ttnn.tilize(
|
| 354 |
+
counts_new, **self._op_kwargs, use_low_perf=True if self.sub_core_grids is not None else False
|
| 355 |
+
)
|
| 356 |
+
if counts:
|
| 357 |
+
counts = ttnn.add(counts, counts_new, output_tensor=counts, **self._op_kwargs)
|
| 358 |
+
else:
|
| 359 |
+
counts = counts_new
|
| 360 |
+
counts_sliced = ttnn.slice(
|
| 361 |
+
counts,
|
| 362 |
+
self.slice_start,
|
| 363 |
+
self.slice_end,
|
| 364 |
+
output_tensor=counts_sliced,
|
| 365 |
+
slice_dim=1,
|
| 366 |
+
num_devices=self.num_devices,
|
| 367 |
+
**self._op_kwargs,
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
mask = ttnn.gt(counts_sliced, 0, output_tensor=mask, **self._op_kwargs)
|
| 371 |
+
return counts, mask
|
| 372 |
+
|
| 373 |
+
def apply(self, tt_logits: ttnn.Tensor) -> ttnn.Tensor:
|
| 374 |
+
if tt_logits is None:
|
| 375 |
+
return tt_logits
|
| 376 |
+
context = PenaltyContext(
|
| 377 |
+
prompt_mask=self.prompt_mask,
|
| 378 |
+
output_mask=self.output_mask,
|
| 379 |
+
output_counts=self.output_counts,
|
| 380 |
+
output_counts_gathered=self.output_counts_gathered,
|
| 381 |
+
presence_penalties=self.presence_penalties,
|
| 382 |
+
frequency_penalties=self.frequency_penalties,
|
| 383 |
+
repetition_penalties=self.repetition_penalties,
|
| 384 |
+
inverse_repetition_penalties=self.inverse_repetition_penalties,
|
| 385 |
+
sub_core_grids=self.sub_core_grids,
|
| 386 |
+
)
|
| 387 |
+
original_shape = tt_logits.shape
|
| 388 |
+
reshaped = ttnn.reshape(tt_logits, (-1, original_shape[-1]))
|
| 389 |
+
apply_penalties(reshaped, context)
|
| 390 |
+
return ttnn.reshape(reshaped, original_shape)
|
code/models/common/sampling/tt_sampling.py
ADDED
|
@@ -0,0 +1,655 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
|
| 2 |
+
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
|
| 5 |
+
import inspect
|
| 6 |
+
import sys
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from loguru import logger
|
| 10 |
+
|
| 11 |
+
import ttnn
|
| 12 |
+
from models.common.lightweightmodule import LightweightModule
|
| 13 |
+
from models.common.sampling._utils import compact_debug_list as _compact_debug_list
|
| 14 |
+
from models.common.sampling._utils import is_default_value, is_llama33_70b_model, is_power_of_2
|
| 15 |
+
from models.common.sampling._utils import log_sampling_debug as _log_sampling_debug
|
| 16 |
+
from models.common.sampling._utils import upper_power_of_2
|
| 17 |
+
from models.common.sampling.tt_log_probs import LogProbsCalculator
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class TTSampling(LightweightModule):
|
| 21 |
+
"""
|
| 22 |
+
On-device sampling module supporting top-k, top-p, and temperature-based sampling.
|
| 23 |
+
|
| 24 |
+
This class implements high-performance on-device sampling that can work across different
|
| 25 |
+
model implementations by accepting configuration parameters rather than assuming specific
|
| 26 |
+
args structures.
|
| 27 |
+
|
| 28 |
+
Multi-device sampling works by partitioning the vocabulary across devices. Each device
|
| 29 |
+
computes top-k locally on its vocabulary partition, then all-gather operations combine
|
| 30 |
+
the results across devices to perform global top-k selection before final sampling.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
mesh_device: The device or MeshDevice for computations
|
| 34 |
+
tt_ccl: CCL object for distributed operations (supports both line_all_gather and tt_all_gather)
|
| 35 |
+
vocab_size: Vocabulary size of the model
|
| 36 |
+
padded_vocab_size: Padded vocabulary size (must be divisible by num devices)
|
| 37 |
+
max_batch_size: Maximum batch size supported
|
| 38 |
+
max_top_k: Maximum number of top-k tokens to consider
|
| 39 |
+
cluster_shape: Shape of the device cluster (rows, cols)
|
| 40 |
+
sampling_all_gather_axis: Axis to all-gather over in 2D meshes (0=rows, 1=cols, default: 0)
|
| 41 |
+
sub_core_grids: Sub-core grid configuration for operations
|
| 42 |
+
sub_core_grid_topk: Sub-core grid configuration specifically for top-k operations
|
| 43 |
+
start_core: Starting core coordinate for sampling operations
|
| 44 |
+
num_gather_links: Number of links to use for all-gather operations (optional)
|
| 45 |
+
sampling_memory_config: Memory configuration for sampling tensors (optional)
|
| 46 |
+
k, p, temp: Initial sampling parameters (tensors of size max_batch_size)
|
| 47 |
+
|
| 48 |
+
Note:
|
| 49 |
+
Uses persistent buffers when CCL supports line_all_gather (llama3_70b_galaxy),
|
| 50 |
+
otherwise uses standard all_gather where the CCL API handles memory allocation (tt-transformers).
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
def _is_force_argmax_sampling(self, k, p, temp):
|
| 54 |
+
"""Detect whether all users request deterministic greedy decoding.
|
| 55 |
+
|
| 56 |
+
When every user in the batch has k=1 (top-1), p=0.0 or p=1.0 (no top-p filter),
|
| 57 |
+
and temp=1.0 (no temperature scaling), we can skip the full top-k / top-p /
|
| 58 |
+
temperature / RNG pipeline and use a single all-gather + argmax instead.
|
| 59 |
+
This is significantly faster because argmax needs only one all-gather of the
|
| 60 |
+
full logits tensor vs. three gathers (values, indices, sampled tokens) in the
|
| 61 |
+
normal path.
|
| 62 |
+
|
| 63 |
+
Note: callers may represent greedy rows with p=1.0, while the
|
| 64 |
+
device argmax-style representation uses p=0.0.
|
| 65 |
+
The model config must also set allow_force_argmax=True for this to activate.
|
| 66 |
+
|
| 67 |
+
Changing this state between decode steps invalidates captured traces, so
|
| 68 |
+
SamplingGenerator maintains separate trace slots keyed by force_argmax.
|
| 69 |
+
"""
|
| 70 |
+
return (
|
| 71 |
+
self._allow_force_argmax_sampling
|
| 72 |
+
and is_default_value(k, 1)
|
| 73 |
+
and (is_default_value(p, 1.0) or is_default_value(p, 0.0))
|
| 74 |
+
and is_default_value(temp, 1.0)
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
def _select_topk_indices_dtype(self, per_device_vocab_size: int, multi_step_reduction: bool):
|
| 78 |
+
# if vocab is larger than uint16 max, return uint32 for indices
|
| 79 |
+
if per_device_vocab_size > torch.iinfo(torch.uint16).max:
|
| 80 |
+
return ttnn.uint32
|
| 81 |
+
|
| 82 |
+
# if vocab size is missaligned with tile size and multi-step reduction is used, we need uint32 because of slice op compatibility
|
| 83 |
+
if multi_step_reduction and (per_device_vocab_size // 2) % ttnn.TILE_SIZE != 0:
|
| 84 |
+
return ttnn.uint32
|
| 85 |
+
|
| 86 |
+
return ttnn.uint16
|
| 87 |
+
|
| 88 |
+
@property
|
| 89 |
+
def force_argmax_sampling(self) -> bool:
|
| 90 |
+
return self._force_argmax_sampling
|
| 91 |
+
|
| 92 |
+
def __init__(
|
| 93 |
+
self,
|
| 94 |
+
mesh_device,
|
| 95 |
+
tt_ccl,
|
| 96 |
+
args,
|
| 97 |
+
k=None,
|
| 98 |
+
p=None,
|
| 99 |
+
temp=None,
|
| 100 |
+
):
|
| 101 |
+
super().__init__()
|
| 102 |
+
self.mesh_device = mesh_device
|
| 103 |
+
self._sampling_debug_enabled = is_llama33_70b_model(args)
|
| 104 |
+
# Multi-step reduction is supported only on single device
|
| 105 |
+
self.multi_step_reduction = list(mesh_device.shape) == [1, 1]
|
| 106 |
+
self.tt_ccl = tt_ccl
|
| 107 |
+
self._line_all_gather = getattr(self.tt_ccl, "line_all_gather", None)
|
| 108 |
+
self._line_all_gather_supports_buffer_key = False
|
| 109 |
+
self._line_all_gather_supports_dtype = False
|
| 110 |
+
self.pad_to_power_of_2 = getattr(args, "pad_logits_to_power_of_2", False)
|
| 111 |
+
if callable(self._line_all_gather):
|
| 112 |
+
try:
|
| 113 |
+
line_all_gather_sig = inspect.signature(self._line_all_gather)
|
| 114 |
+
line_all_gather_params = line_all_gather_sig.parameters
|
| 115 |
+
self._line_all_gather_supports_buffer_key = "buffer_key" in line_all_gather_params or any(
|
| 116 |
+
param.kind == inspect.Parameter.VAR_KEYWORD for param in line_all_gather_params.values()
|
| 117 |
+
)
|
| 118 |
+
self._line_all_gather_supports_dtype = "dtype" in line_all_gather_params or any(
|
| 119 |
+
param.kind == inspect.Parameter.VAR_KEYWORD for param in line_all_gather_params.values()
|
| 120 |
+
)
|
| 121 |
+
except (TypeError, ValueError):
|
| 122 |
+
logger.warning("Unable to inspect line_all_gather signature; assuming no buffer_key or dtype support.")
|
| 123 |
+
|
| 124 |
+
padded_vocab_size = getattr(args, "padded_vocab_size", None)
|
| 125 |
+
self.padded_vocab_size = padded_vocab_size if padded_vocab_size is not None else args.vocab_size
|
| 126 |
+
self.vocab_size = args.vocab_size
|
| 127 |
+
# Round up to the next tile boundary (32) — device tensors must be tile-aligned.
|
| 128 |
+
raw_batch = getattr(args, "max_batch_size", 32)
|
| 129 |
+
self.max_batch_size = max(32, ((raw_batch + 31) // 32) * 32)
|
| 130 |
+
self.max_top_k = getattr(args, "max_top_k", 32)
|
| 131 |
+
self.cluster_shape = args.cluster_shape
|
| 132 |
+
|
| 133 |
+
self.sampling_all_gather_axis = getattr(args, "sampling_all_gather_axis", 0)
|
| 134 |
+
self.sub_core_grids = getattr(args, "sub_core_grids", None)
|
| 135 |
+
self.sub_core_grid_topk = getattr(args, "sub_core_grid_topk", None)
|
| 136 |
+
self.start_core = getattr(args, "start_core", ttnn.CoreCoord(0, 0))
|
| 137 |
+
self._sampling_sub_core_grids = (
|
| 138 |
+
ttnn.num_cores_to_corerangeset_in_subcoregrids(
|
| 139 |
+
self.start_core, self.max_batch_size, self.sub_core_grids, row_wise=True
|
| 140 |
+
)
|
| 141 |
+
if self.sub_core_grids is not None
|
| 142 |
+
else None
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
# sampling_dp > 1 when multiple mesh groups each sample users independently
|
| 146 |
+
# (e.g. GPT-OSS on [4,8]: 4 rows × 32 users; Llama Galaxy on [8,4]: 4 cols × 8 users)
|
| 147 |
+
self._sampling_dp = getattr(args, "sampling_dp", 1)
|
| 148 |
+
# Shard params along the non-all-gather axis; replicate along the all-gather axis
|
| 149 |
+
if self._sampling_dp > 1:
|
| 150 |
+
if self.sampling_all_gather_axis == 0:
|
| 151 |
+
self._param_dims = (None, 0) # shard along cols
|
| 152 |
+
else:
|
| 153 |
+
self._param_dims = (0, None) # shard along rows
|
| 154 |
+
else:
|
| 155 |
+
self._param_dims = (None, None)
|
| 156 |
+
|
| 157 |
+
if hasattr(args, "model_config") and "GALAXY_NUM_LINKS" in args.model_config:
|
| 158 |
+
# Calculate num_gather_links based on model config
|
| 159 |
+
max_num_gather_links = args.model_config["GALAXY_NUM_LINKS"]
|
| 160 |
+
self.num_gather_links = (
|
| 161 |
+
args.max_top_k // 32 if args.max_top_k // 32 <= max_num_gather_links else max_num_gather_links
|
| 162 |
+
)
|
| 163 |
+
else:
|
| 164 |
+
self.num_gather_links = 1
|
| 165 |
+
if hasattr(args, "model_config") and "DECODE_SAMPLING_INPUT_MEMCFG" in args.model_config:
|
| 166 |
+
self.sampling_memory_config = args.model_config["DECODE_SAMPLING_INPUT_MEMCFG"]
|
| 167 |
+
else:
|
| 168 |
+
self.sampling_memory_config = ttnn.DRAM_MEMORY_CONFIG
|
| 169 |
+
|
| 170 |
+
# Force argmax sampling
|
| 171 |
+
if hasattr(args, "model_config") and "SAMPLING_AG_CONFIG" in args.model_config:
|
| 172 |
+
# The model config may describe the fastest full-size Galaxy path, but
|
| 173 |
+
# the actual CCL shape is resolved from the runtime mesh below.
|
| 174 |
+
sampling_ag_config = args.model_config["SAMPLING_AG_CONFIG"]
|
| 175 |
+
self._allow_force_argmax_sampling = sampling_ag_config["allow_force_argmax"]
|
| 176 |
+
self.num_argmax_gather_links = sampling_ag_config["num_links"]
|
| 177 |
+
self.argmax_chunks_per_sync = sampling_ag_config.get("chunks_per_sync", 10)
|
| 178 |
+
self.argmax_num_workers_per_link = 1
|
| 179 |
+
self.ag_topology = sampling_ag_config["topology"]
|
| 180 |
+
else:
|
| 181 |
+
self._allow_force_argmax_sampling = False
|
| 182 |
+
self.num_argmax_gather_links = self.num_gather_links
|
| 183 |
+
self.argmax_chunks_per_sync = 10
|
| 184 |
+
self.argmax_num_workers_per_link = 1
|
| 185 |
+
self.ag_topology = ttnn.Topology.Linear
|
| 186 |
+
|
| 187 |
+
# Set defaults for sampling parameters if not provided
|
| 188 |
+
# Default: k=1 (top-1), p=0 (effectively argmax), temp=1 (no temperature scaling)
|
| 189 |
+
# When p=0, the sampling operation will select the token with highest probability (argmax)
|
| 190 |
+
total_param_size = self.max_batch_size * self._sampling_dp
|
| 191 |
+
if k is None:
|
| 192 |
+
k = torch.ones(total_param_size)
|
| 193 |
+
if p is None:
|
| 194 |
+
p = torch.zeros(total_param_size)
|
| 195 |
+
if temp is None:
|
| 196 |
+
temp = torch.ones(total_param_size)
|
| 197 |
+
|
| 198 |
+
self._force_argmax_sampling = self._is_force_argmax_sampling(k, p, temp)
|
| 199 |
+
|
| 200 |
+
# Create sampling parameter tensors on device
|
| 201 |
+
# When _sampling_dp > 1, dims=(0, None) shards the [128] tensor across 4 rows → [32] per row
|
| 202 |
+
self.k_tensor = ttnn.from_torch(
|
| 203 |
+
k,
|
| 204 |
+
device=self.mesh_device,
|
| 205 |
+
dtype=ttnn.uint32,
|
| 206 |
+
layout=ttnn.ROW_MAJOR_LAYOUT,
|
| 207 |
+
mesh_mapper=ttnn.ShardTensor2dMesh(self.mesh_device, dims=self._param_dims, mesh_shape=self.cluster_shape),
|
| 208 |
+
)
|
| 209 |
+
self.p_tensor = ttnn.from_torch(
|
| 210 |
+
p,
|
| 211 |
+
device=self.mesh_device,
|
| 212 |
+
dtype=ttnn.bfloat16,
|
| 213 |
+
layout=ttnn.ROW_MAJOR_LAYOUT,
|
| 214 |
+
mesh_mapper=ttnn.ShardTensor2dMesh(self.mesh_device, dims=self._param_dims, mesh_shape=self.cluster_shape),
|
| 215 |
+
)
|
| 216 |
+
self.temp_tensor = ttnn.from_torch(
|
| 217 |
+
temp,
|
| 218 |
+
device=self.mesh_device,
|
| 219 |
+
dtype=ttnn.bfloat16,
|
| 220 |
+
layout=ttnn.ROW_MAJOR_LAYOUT,
|
| 221 |
+
mesh_mapper=ttnn.ShardTensor2dMesh(self.mesh_device, dims=self._param_dims, mesh_shape=self.cluster_shape),
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
# Create device offset indices for global indexing
|
| 225 |
+
self._create_indices_tensors()
|
| 226 |
+
# Log-probs tensor to store the log-probs for the batch
|
| 227 |
+
self.tt_log_probs = None
|
| 228 |
+
self.log_probs_calculator = LogProbsCalculator(
|
| 229 |
+
self.mesh_device,
|
| 230 |
+
self.sub_core_grids,
|
| 231 |
+
self.tt_ccl,
|
| 232 |
+
batch_size=self.max_batch_size,
|
| 233 |
+
use_topk_logprobs=getattr(args, "use_topk_logprobs", False),
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
# Seeds tensor: one RNG slot per user across all rows.
|
| 237 |
+
# When sampling_dp > 1, shard across rows so each row gets its own slice.
|
| 238 |
+
# user_ids tensor: core routing only (32 per row, replicated).
|
| 239 |
+
self.seeds_tt_tensor = ttnn.from_torch(
|
| 240 |
+
torch.arange(total_param_size).to(torch.uint32),
|
| 241 |
+
device=self.mesh_device,
|
| 242 |
+
dtype=ttnn.uint32,
|
| 243 |
+
layout=ttnn.ROW_MAJOR_LAYOUT,
|
| 244 |
+
mesh_mapper=ttnn.ShardTensor2dMesh(self.mesh_device, dims=self._param_dims, mesh_shape=self.cluster_shape)
|
| 245 |
+
if self._sampling_dp > 1
|
| 246 |
+
else None,
|
| 247 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 248 |
+
)
|
| 249 |
+
self.user_ids_tt_tensor = ttnn.as_tensor(
|
| 250 |
+
torch.arange(self.max_batch_size).to(torch.uint32),
|
| 251 |
+
dtype=ttnn.uint32,
|
| 252 |
+
layout=ttnn.ROW_MAJOR_LAYOUT,
|
| 253 |
+
device=self.mesh_device,
|
| 254 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
def _create_indices_tensors(self):
|
| 258 |
+
"""Create the indices tensors needed for distributed top-k operations."""
|
| 259 |
+
# Create indices tensor for device offsets
|
| 260 |
+
# For multi-step reduction, we use reduce over 2 steps in a single device
|
| 261 |
+
if self.multi_step_reduction:
|
| 262 |
+
num_devices_in_mesh = 2
|
| 263 |
+
else:
|
| 264 |
+
# If the mesh is effectively 1D, use the non-singleton dimension.
|
| 265 |
+
# If the mesh is 2D, use the configured gather axis.
|
| 266 |
+
if 1 in self.cluster_shape:
|
| 267 |
+
num_devices_in_mesh = max(self.cluster_shape[0], self.cluster_shape[1])
|
| 268 |
+
else:
|
| 269 |
+
assert self.sampling_all_gather_axis in (
|
| 270 |
+
0,
|
| 271 |
+
1,
|
| 272 |
+
), f"sampling_all_gather_axis must be 0 or 1 for 2D meshes, got {self.sampling_all_gather_axis}"
|
| 273 |
+
num_devices_in_mesh = self.cluster_shape[self.sampling_all_gather_axis]
|
| 274 |
+
indices_device_offsets = torch.ones(
|
| 275 |
+
1, 1, self.max_batch_size, self.max_top_k * num_devices_in_mesh, dtype=torch.int64
|
| 276 |
+
)
|
| 277 |
+
# padded_per_device: tile-aligned width matching actual logit tensors (for indices tensor)
|
| 278 |
+
padded_per_device = self.padded_vocab_size // num_devices_in_mesh
|
| 279 |
+
|
| 280 |
+
for device_id in range(num_devices_in_mesh):
|
| 281 |
+
indices_device_offsets[:, :, :, device_id * self.max_top_k : (device_id + 1) * self.max_top_k] = (
|
| 282 |
+
device_id * padded_per_device
|
| 283 |
+
)
|
| 284 |
+
self.tt_indices_device_offsets = ttnn.from_torch(
|
| 285 |
+
indices_device_offsets,
|
| 286 |
+
device=self.mesh_device,
|
| 287 |
+
dtype=ttnn.int32,
|
| 288 |
+
layout=ttnn.TILE_LAYOUT,
|
| 289 |
+
mesh_mapper=ttnn.ShardTensor2dMesh(self.mesh_device, dims=(None, None), mesh_shape=self.cluster_shape),
|
| 290 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
# Create local indices tensor for top-k operations (must match logit width)
|
| 294 |
+
indices_tensor_torch = torch.zeros(1, 1, self.max_batch_size, padded_per_device, dtype=torch.int32)
|
| 295 |
+
for i in range(padded_per_device):
|
| 296 |
+
indices_tensor_torch[:, :, :, i] = i
|
| 297 |
+
|
| 298 |
+
# pad to power of 2 if needed
|
| 299 |
+
if self.pad_to_power_of_2 and not is_power_of_2(indices_tensor_torch.shape[-1]):
|
| 300 |
+
padded_value = upper_power_of_2(indices_tensor_torch.shape[-1])
|
| 301 |
+
indices_tensor_torch = torch.nn.functional.pad(
|
| 302 |
+
indices_tensor_torch,
|
| 303 |
+
(0, padded_value - indices_tensor_torch.shape[-1]), # pad only last dim
|
| 304 |
+
mode="constant",
|
| 305 |
+
value=-1, # invalid index to ensure that the padding values are not used
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
indices_dtype = self._select_topk_indices_dtype(padded_per_device, self.multi_step_reduction)
|
| 309 |
+
self.tt_indices_tensor = ttnn.from_torch(
|
| 310 |
+
indices_tensor_torch,
|
| 311 |
+
dtype=indices_dtype,
|
| 312 |
+
layout=ttnn.Layout.TILE,
|
| 313 |
+
device=self.mesh_device,
|
| 314 |
+
mesh_mapper=ttnn.ShardTensor2dMesh(self.mesh_device, dims=(None, None), mesh_shape=self.cluster_shape),
|
| 315 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
def _perform_all_gather(self, tensor, dim, cluster_axis, memory_config, num_links, buffer_key=None, dtype=None):
|
| 319 |
+
"""
|
| 320 |
+
Flexible all-gather that works across different CCL implementations.
|
| 321 |
+
|
| 322 |
+
- If `tt_ccl` exposes `line_all_gather`, prefer it (enables persistent buffer usage on some stacks).
|
| 323 |
+
- Otherwise fall back to `ttnn.all_gather`.
|
| 324 |
+
"""
|
| 325 |
+
if callable(self._line_all_gather):
|
| 326 |
+
# Some implementations accept `buffer_key` (for persistent buffers), others may not.
|
| 327 |
+
line_all_gather_kwargs = {
|
| 328 |
+
"dim": dim,
|
| 329 |
+
"cluster_axis": cluster_axis,
|
| 330 |
+
"memory_config": memory_config,
|
| 331 |
+
"num_links": num_links,
|
| 332 |
+
}
|
| 333 |
+
if self._line_all_gather_supports_buffer_key and buffer_key is not None:
|
| 334 |
+
line_all_gather_kwargs["buffer_key"] = buffer_key
|
| 335 |
+
if self._line_all_gather_supports_dtype and dtype is not None:
|
| 336 |
+
line_all_gather_kwargs["dtype"] = dtype
|
| 337 |
+
return self._line_all_gather(tensor, **line_all_gather_kwargs)
|
| 338 |
+
|
| 339 |
+
return ttnn.all_gather(
|
| 340 |
+
tensor,
|
| 341 |
+
dim=dim,
|
| 342 |
+
num_links=num_links,
|
| 343 |
+
memory_config=memory_config,
|
| 344 |
+
cluster_axis=cluster_axis,
|
| 345 |
+
topology=ttnn.Topology.Linear,
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
def _get_sampling_cluster_axis(self):
|
| 349 |
+
if self.mesh_device.get_num_devices() <= 1:
|
| 350 |
+
return None
|
| 351 |
+
# 1D submeshes should use the default CCL axis; forcing axis 1 can make
|
| 352 |
+
# smaller Galaxy DP groups request routes outside the submesh.
|
| 353 |
+
if 1 in self.cluster_shape:
|
| 354 |
+
return None
|
| 355 |
+
return self.sampling_all_gather_axis
|
| 356 |
+
|
| 357 |
+
def _get_force_argmax_all_gather_config(self, cluster_axis):
|
| 358 |
+
num_links = self.num_argmax_gather_links
|
| 359 |
+
if hasattr(self.tt_ccl, "get_num_links"):
|
| 360 |
+
# Clamp the tuned config to the links available on the actual submesh.
|
| 361 |
+
num_links = min(num_links, self.tt_ccl.get_num_links(cluster_axis))
|
| 362 |
+
|
| 363 |
+
topology = self.ag_topology
|
| 364 |
+
# Ring is available for T3K-like 8-device groups; smaller DP groups need
|
| 365 |
+
# linear routing to avoid wraparound routes such as D0 -> D12.
|
| 366 |
+
if self.mesh_device.get_num_devices() < 8:
|
| 367 |
+
topology = ttnn.Topology.Linear
|
| 368 |
+
|
| 369 |
+
return max(1, num_links), topology
|
| 370 |
+
|
| 371 |
+
def reset_params(
|
| 372 |
+
self,
|
| 373 |
+
k,
|
| 374 |
+
p,
|
| 375 |
+
temp,
|
| 376 |
+
enable_log_probs: bool | list[bool] = None,
|
| 377 |
+
num_logprobs: int | list[int] = None,
|
| 378 |
+
empty_slots: list[int] | None = None,
|
| 379 |
+
):
|
| 380 |
+
"""Update sampling parameters (k, p, temperature, logprobs) dynamically."""
|
| 381 |
+
self._force_argmax_sampling = self._is_force_argmax_sampling(k, p, temp)
|
| 382 |
+
_log_sampling_debug(
|
| 383 |
+
self._sampling_debug_enabled,
|
| 384 |
+
"TTSampling reset params",
|
| 385 |
+
force_argmax=self._force_argmax_sampling,
|
| 386 |
+
empty_slots=_compact_debug_list(empty_slots),
|
| 387 |
+
top_k=_compact_debug_list(k),
|
| 388 |
+
top_p=_compact_debug_list(p),
|
| 389 |
+
temperature=_compact_debug_list(temp),
|
| 390 |
+
enable_log_probs=_compact_debug_list(enable_log_probs),
|
| 391 |
+
num_logprobs=_compact_debug_list(num_logprobs),
|
| 392 |
+
sampling_dp=self._sampling_dp,
|
| 393 |
+
)
|
| 394 |
+
if not self._force_argmax_sampling:
|
| 395 |
+
# When _sampling_dp > 1, create multi-device host tensors so
|
| 396 |
+
# copy_host_to_device_tensor writes per-row shards correctly.
|
| 397 |
+
if self._sampling_dp > 1:
|
| 398 |
+
mapper = ttnn.ShardTensor2dMesh(self.mesh_device, dims=self._param_dims, mesh_shape=self.cluster_shape)
|
| 399 |
+
else:
|
| 400 |
+
mapper = None
|
| 401 |
+
|
| 402 |
+
self.k_tensor_new = ttnn.from_torch(
|
| 403 |
+
torch.tensor(k),
|
| 404 |
+
device=None,
|
| 405 |
+
dtype=ttnn.uint32,
|
| 406 |
+
layout=ttnn.ROW_MAJOR_LAYOUT,
|
| 407 |
+
mesh_mapper=mapper,
|
| 408 |
+
)
|
| 409 |
+
self.p_tensor_new = ttnn.from_torch(
|
| 410 |
+
torch.tensor(p),
|
| 411 |
+
device=None,
|
| 412 |
+
dtype=ttnn.bfloat16,
|
| 413 |
+
layout=ttnn.ROW_MAJOR_LAYOUT,
|
| 414 |
+
mesh_mapper=mapper,
|
| 415 |
+
)
|
| 416 |
+
self.temp_tensor_new = ttnn.from_torch(
|
| 417 |
+
torch.tensor(temp),
|
| 418 |
+
device=None,
|
| 419 |
+
dtype=ttnn.bfloat16,
|
| 420 |
+
layout=ttnn.ROW_MAJOR_LAYOUT,
|
| 421 |
+
mesh_mapper=mapper,
|
| 422 |
+
)
|
| 423 |
+
|
| 424 |
+
ttnn.copy_host_to_device_tensor(self.k_tensor_new, self.k_tensor)
|
| 425 |
+
ttnn.copy_host_to_device_tensor(self.p_tensor_new, self.p_tensor)
|
| 426 |
+
ttnn.copy_host_to_device_tensor(self.temp_tensor_new, self.temp_tensor)
|
| 427 |
+
|
| 428 |
+
self.log_probs_calculator.set_log_probs_mode(
|
| 429 |
+
enable_log_probs, num_logprobs=num_logprobs, empty_slots=empty_slots
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
def forward(
|
| 433 |
+
self,
|
| 434 |
+
x: ttnn.Tensor,
|
| 435 |
+
tt_out_tok: ttnn.Tensor = None,
|
| 436 |
+
):
|
| 437 |
+
"""
|
| 438 |
+
Perform on-device sampling on logits tensor.
|
| 439 |
+
The logits are sharded over the devices in the cluster.
|
| 440 |
+
We perform local top-k on each device, then all-gather the top-k values and indices across all devices.
|
| 441 |
+
We then convert the gathered values and indices to the appropriate format, add the device offsets to get the global vocabulary indices,
|
| 442 |
+
and perform the actual sampling with top-k, top-p, and temperature.
|
| 443 |
+
|
| 444 |
+
Args:
|
| 445 |
+
x: Input logits tensor
|
| 446 |
+
tt_out_tok: Optional output tensor to write results to
|
| 447 |
+
|
| 448 |
+
Returns:
|
| 449 |
+
Sampled token indices tensor
|
| 450 |
+
"""
|
| 451 |
+
_log_sampling_debug(
|
| 452 |
+
self._sampling_debug_enabled,
|
| 453 |
+
"TTSampling forward",
|
| 454 |
+
force_argmax=self._force_argmax_sampling,
|
| 455 |
+
logits_shape=list(x.shape),
|
| 456 |
+
tt_out_tok_shape=list(tt_out_tok.shape) if tt_out_tok is not None else None,
|
| 457 |
+
max_top_k=self.max_top_k,
|
| 458 |
+
multi_step_reduction=self.multi_step_reduction,
|
| 459 |
+
sampling_dp=self._sampling_dp,
|
| 460 |
+
)
|
| 461 |
+
if self._force_argmax_sampling:
|
| 462 |
+
logger.info("Forcing argmax sampling")
|
| 463 |
+
# Gather the output across all devices and untilize the tensor (for argmax)
|
| 464 |
+
num_devices = self.mesh_device.get_num_devices()
|
| 465 |
+
if num_devices > 1:
|
| 466 |
+
cluster_axis = self._get_sampling_cluster_axis()
|
| 467 |
+
num_links, topology = self._get_force_argmax_all_gather_config(cluster_axis)
|
| 468 |
+
logger.debug(
|
| 469 |
+
f"Force argmax sampling all-gather: cluster_axis={cluster_axis}, "
|
| 470 |
+
f"num_links={num_links}, topology={topology}"
|
| 471 |
+
)
|
| 472 |
+
x = ttnn.experimental.all_gather_async(
|
| 473 |
+
x,
|
| 474 |
+
persistent_output_buffer=None,
|
| 475 |
+
dim=3,
|
| 476 |
+
multi_device_global_semaphore=self.tt_ccl.get_and_cycle_ag_semaphore_handles(cluster_axis),
|
| 477 |
+
num_links=num_links,
|
| 478 |
+
memory_config=x.memory_config(),
|
| 479 |
+
cluster_axis=cluster_axis,
|
| 480 |
+
topology=topology,
|
| 481 |
+
barrier_semaphore=self.tt_ccl.get_and_cycle_barrier_semaphore_handle(cluster_axis),
|
| 482 |
+
chunks_per_sync=self.argmax_chunks_per_sync,
|
| 483 |
+
num_workers_per_link=self.argmax_num_workers_per_link,
|
| 484 |
+
num_buffers_per_channel=2,
|
| 485 |
+
)
|
| 486 |
+
x_untilized = ttnn.untilize(x, use_multicore=True)
|
| 487 |
+
tt_out_tok = ttnn.argmax(
|
| 488 |
+
x_untilized,
|
| 489 |
+
dim=-1,
|
| 490 |
+
output_tensor=tt_out_tok,
|
| 491 |
+
keepdim=False,
|
| 492 |
+
)
|
| 493 |
+
# Argmax path: logprobs not supported (force-argmax is disabled
|
| 494 |
+
# when logprobs are enabled via format_sampling_params guard).
|
| 495 |
+
self.tt_log_probs = None
|
| 496 |
+
return tt_out_tok, self.tt_log_probs
|
| 497 |
+
|
| 498 |
+
# Convert to bfloat16 for top-k operations (typecast is no-op if already bfloat16)
|
| 499 |
+
x_bf16 = ttnn.typecast(x, dtype=ttnn.bfloat16, sub_core_grids=self.sub_core_grids)
|
| 500 |
+
|
| 501 |
+
if self.multi_step_reduction:
|
| 502 |
+
x_bf16_list = ttnn.split(x_bf16, x_bf16.shape[-1] // 2, dim=3)
|
| 503 |
+
indices_tensor_list = ttnn.split(self.tt_indices_tensor, self.tt_indices_tensor.shape[-1] // 2, dim=3)
|
| 504 |
+
topk_values_list = []
|
| 505 |
+
topk_indices_list = []
|
| 506 |
+
|
| 507 |
+
for i in range(len(x_bf16_list)):
|
| 508 |
+
topk_values, topk_indices = ttnn.topk(
|
| 509 |
+
x_bf16_list[i],
|
| 510 |
+
k=self.max_top_k,
|
| 511 |
+
dim=-1,
|
| 512 |
+
sub_core_grids=self.sub_core_grid_topk,
|
| 513 |
+
indices_tensor=indices_tensor_list[i],
|
| 514 |
+
)
|
| 515 |
+
topk_values_list.append(topk_values)
|
| 516 |
+
topk_indices_list.append(topk_indices)
|
| 517 |
+
x_bf16_list[i].deallocate()
|
| 518 |
+
indices_tensor_list[i].deallocate()
|
| 519 |
+
|
| 520 |
+
topk_values_gathered_bf16_interleaved = ttnn.concat(topk_values_list, dim=3)
|
| 521 |
+
topk_indices_gathered = ttnn.concat(topk_indices_list, dim=3)
|
| 522 |
+
|
| 523 |
+
for i in range(len(topk_indices_list)):
|
| 524 |
+
ttnn.deallocate(topk_values_list[i])
|
| 525 |
+
ttnn.deallocate(topk_indices_list[i])
|
| 526 |
+
|
| 527 |
+
else:
|
| 528 |
+
# apply padding to the input tensor if needed
|
| 529 |
+
# if number is not power of 2, pad to upper power of 2
|
| 530 |
+
# pad only last dimension with float::min value to upper_power_of_2
|
| 531 |
+
# This is necessary to use full optimization in the topk operation.
|
| 532 |
+
if self.pad_to_power_of_2 and not is_power_of_2(x_bf16.shape[-1]):
|
| 533 |
+
padded_value = upper_power_of_2(x_bf16.shape[-1])
|
| 534 |
+
x_bf16 = ttnn.pad(
|
| 535 |
+
x_bf16,
|
| 536 |
+
[(0, 0), (0, 0), (0, 0), (0, padded_value - x_bf16.shape[-1])],
|
| 537 |
+
value=-sys.float_info.max,
|
| 538 |
+
sub_core_grids=self.sub_core_grids,
|
| 539 |
+
)
|
| 540 |
+
# Perform local top-k on each device
|
| 541 |
+
topk_values, topk_indices = ttnn.topk(
|
| 542 |
+
x_bf16,
|
| 543 |
+
k=self.max_top_k,
|
| 544 |
+
dim=-1,
|
| 545 |
+
sub_core_grids=self.sub_core_grid_topk,
|
| 546 |
+
indices_tensor=self.tt_indices_tensor,
|
| 547 |
+
)
|
| 548 |
+
|
| 549 |
+
# For 1D meshes use `cluster_axis=None`. For 2D meshes, use the configured gather axis.
|
| 550 |
+
sampling_cluster_axis = self._get_sampling_cluster_axis()
|
| 551 |
+
|
| 552 |
+
# Gather top-k values across all devices
|
| 553 |
+
topk_values_gathered = self._perform_all_gather(
|
| 554 |
+
topk_values,
|
| 555 |
+
dim=3,
|
| 556 |
+
cluster_axis=sampling_cluster_axis,
|
| 557 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 558 |
+
num_links=self.num_gather_links,
|
| 559 |
+
buffer_key="SAMPLING_VALUES",
|
| 560 |
+
)
|
| 561 |
+
|
| 562 |
+
ttnn.deallocate(topk_values)
|
| 563 |
+
|
| 564 |
+
# Convert gathered values to appropriate format
|
| 565 |
+
if self.sampling_memory_config != ttnn.DRAM_MEMORY_CONFIG:
|
| 566 |
+
topk_values_gathered_bf16 = ttnn.to_memory_config(
|
| 567 |
+
topk_values_gathered,
|
| 568 |
+
memory_config=self.sampling_memory_config,
|
| 569 |
+
dtype=ttnn.bfloat16,
|
| 570 |
+
)
|
| 571 |
+
topk_values_gathered_bf16_interleaved = ttnn.to_memory_config(
|
| 572 |
+
topk_values_gathered_bf16, memory_config=ttnn.DRAM_MEMORY_CONFIG
|
| 573 |
+
)
|
| 574 |
+
ttnn.deallocate(topk_values_gathered_bf16)
|
| 575 |
+
else:
|
| 576 |
+
topk_values_gathered_bf16_interleaved = topk_values_gathered
|
| 577 |
+
|
| 578 |
+
# Gather top-k indices across all devices
|
| 579 |
+
topk_indices_gathered = self._perform_all_gather(
|
| 580 |
+
topk_indices,
|
| 581 |
+
dim=3,
|
| 582 |
+
cluster_axis=sampling_cluster_axis,
|
| 583 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 584 |
+
num_links=self.num_gather_links,
|
| 585 |
+
buffer_key="SAMPLING_INDICES",
|
| 586 |
+
dtype=ttnn.uint16,
|
| 587 |
+
)
|
| 588 |
+
ttnn.deallocate(topk_indices)
|
| 589 |
+
|
| 590 |
+
# Convert indices to appropriate data types
|
| 591 |
+
|
| 592 |
+
topk_indices_gathered_int32 = ttnn.typecast(
|
| 593 |
+
topk_indices_gathered, dtype=ttnn.int32, sub_core_grids=self.sub_core_grids
|
| 594 |
+
)
|
| 595 |
+
|
| 596 |
+
if self.sampling_memory_config != ttnn.DRAM_MEMORY_CONFIG:
|
| 597 |
+
topk_indices_gathered_int32_sharded = ttnn.to_memory_config(
|
| 598 |
+
topk_indices_gathered_int32, self.sampling_memory_config
|
| 599 |
+
)
|
| 600 |
+
ttnn.deallocate(topk_indices_gathered_int32)
|
| 601 |
+
else:
|
| 602 |
+
topk_indices_gathered_int32_sharded = topk_indices_gathered_int32
|
| 603 |
+
|
| 604 |
+
# Add device offsets to get global vocabulary indices
|
| 605 |
+
topk_global_indices = ttnn.add(
|
| 606 |
+
self.tt_indices_device_offsets,
|
| 607 |
+
topk_indices_gathered_int32_sharded,
|
| 608 |
+
dtype=ttnn.uint32,
|
| 609 |
+
memory_config=self.sampling_memory_config,
|
| 610 |
+
)
|
| 611 |
+
|
| 612 |
+
ttnn.deallocate(topk_indices_gathered_int32_sharded)
|
| 613 |
+
|
| 614 |
+
topk_global_indices_interleaved = ttnn.to_memory_config(topk_global_indices, ttnn.DRAM_MEMORY_CONFIG)
|
| 615 |
+
|
| 616 |
+
# Untilize indices for sampling operation
|
| 617 |
+
topk_global_indices_interleaved_untilised = ttnn.untilize(
|
| 618 |
+
topk_global_indices_interleaved, use_multicore=True, sub_core_grids=self.sub_core_grids
|
| 619 |
+
)
|
| 620 |
+
ttnn.manual_seed(
|
| 621 |
+
seeds=self.seeds_tt_tensor,
|
| 622 |
+
user_ids=self.user_ids_tt_tensor,
|
| 623 |
+
sub_core_grids=self._sampling_sub_core_grids,
|
| 624 |
+
)
|
| 625 |
+
# Perform the actual sampling with top-k, top-p, and temperature
|
| 626 |
+
tt_out_tok = ttnn.sampling(
|
| 627 |
+
topk_values_gathered_bf16_interleaved,
|
| 628 |
+
topk_global_indices_interleaved_untilised,
|
| 629 |
+
k=self.k_tensor,
|
| 630 |
+
p=self.p_tensor,
|
| 631 |
+
temp=self.temp_tensor,
|
| 632 |
+
sub_core_grids=self._sampling_sub_core_grids,
|
| 633 |
+
output_tensor=tt_out_tok,
|
| 634 |
+
)
|
| 635 |
+
|
| 636 |
+
# Compute logprobs if enabled
|
| 637 |
+
if self.log_probs_calculator.enable_log_probs and self.log_probs_calculator._use_topk_logprobs:
|
| 638 |
+
# New path: top-K logprobs for gpt-oss-120b
|
| 639 |
+
self.tt_log_probs = self.log_probs_calculator.calculate_topk_log_probs(
|
| 640 |
+
logits_tensor=x,
|
| 641 |
+
topk_values=topk_values_gathered_bf16_interleaved,
|
| 642 |
+
topk_global_indices=topk_global_indices_interleaved,
|
| 643 |
+
sub_core_grid_topk=self.sub_core_grid_topk,
|
| 644 |
+
)
|
| 645 |
+
elif self.log_probs_calculator.enable_log_probs:
|
| 646 |
+
# Old path: single sampled-token logprob
|
| 647 |
+
self.tt_log_probs = self.log_probs_calculator.calculate_log_probs(x, tt_out_tok)
|
| 648 |
+
else:
|
| 649 |
+
self.tt_log_probs = None
|
| 650 |
+
|
| 651 |
+
ttnn.deallocate(topk_values_gathered_bf16_interleaved)
|
| 652 |
+
ttnn.deallocate(topk_global_indices_interleaved)
|
| 653 |
+
ttnn.deallocate(topk_global_indices_interleaved_untilised)
|
| 654 |
+
|
| 655 |
+
return tt_out_tok, self.tt_log_probs
|
code/models/common/tensor_utils.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 re
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
|
| 13 |
+
import ttnn
|
| 14 |
+
|
| 15 |
+
# Standard tile size - hardware constant
|
| 16 |
+
TILE_SIZE = ttnn.TILE_SIZE # 32
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def get_rot_transformation_mat(dhead: int = TILE_SIZE) -> torch.Tensor:
|
| 20 |
+
"""
|
| 21 |
+
Create rotation transformation matrix for RoPE.
|
| 22 |
+
|
| 23 |
+
Constructs a permutation matrix that pairs adjacent dimensions with
|
| 24 |
+
signs (+1, -1) for the RoPE rotation:
|
| 25 |
+
[0, 1] → +1 at (0,1), -1 at (1,0)
|
| 26 |
+
[2, 3] → +1 at (2,3), -1 at (3,2)
|
| 27 |
+
...
|
| 28 |
+
|
| 29 |
+
Used by ttnn.experimental.rotary_embedding_llama.
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
dhead: Matrix dimension. Must equal TILE_SIZE. Use TILE_SIZE for decode.
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
torch.Tensor of shape [1, 1, dhead, dhead].
|
| 36 |
+
"""
|
| 37 |
+
rot_emb_matrix = torch.zeros(1, 1, dhead, dhead)
|
| 38 |
+
rot_emb_matrix[..., torch.arange(0, dhead, 2), torch.arange(1, dhead, 2)] = 1
|
| 39 |
+
rot_emb_matrix[..., torch.arange(1, dhead, 2), torch.arange(0, dhead, 2)] = -1
|
| 40 |
+
return rot_emb_matrix
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def zeros_like_kv_cache(batch_size: int, n_kv_heads: int, max_seq_len: int, head_dim: int) -> torch.Tensor:
|
| 44 |
+
"""Create zeros tensor for standard KV cache."""
|
| 45 |
+
return torch.zeros((batch_size, n_kv_heads, max_seq_len, head_dim))
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def zeros_like_paged_cache(paged_config, n_kv_heads: int, head_dim: int) -> torch.Tensor:
|
| 49 |
+
"""Create zeros tensor for paged KV cache."""
|
| 50 |
+
return torch.zeros((paged_config.max_num_blocks, n_kv_heads, paged_config.block_size, head_dim))
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# todo)) add a on-device pad_dim_to_size function?
|
| 54 |
+
def pad_dim_to_size(x: "torch.Tensor", dim: int, size: int) -> "torch.Tensor":
|
| 55 |
+
"""Pads the specified dimension of the input tensor with zeros."""
|
| 56 |
+
if dim < 0:
|
| 57 |
+
dim = x.dim() + dim
|
| 58 |
+
current_size = x.size(dim)
|
| 59 |
+
pad_size = size - current_size
|
| 60 |
+
|
| 61 |
+
if pad_size < 0:
|
| 62 |
+
raise ValueError(f"Target size {size} is smaller than current size {current_size} on dim {dim}")
|
| 63 |
+
|
| 64 |
+
if pad_size == 0:
|
| 65 |
+
return x
|
| 66 |
+
|
| 67 |
+
pad = [0] * (2 * x.dim())
|
| 68 |
+
pad_index = 2 * (x.dim() - dim - 1)
|
| 69 |
+
pad[pad_index + 1] = pad_size
|
| 70 |
+
|
| 71 |
+
return torch.nn.functional.pad(x, pad, mode="constant", value=0)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def pad_to_shape(x: "torch.Tensor", target_shape: tuple[int, ...], pad_value: float = 0.0) -> "torch.Tensor":
|
| 75 |
+
"""Pad tensor to target_shape in a single F.pad call (more efficient than per-dim padding)."""
|
| 76 |
+
if x.shape == target_shape:
|
| 77 |
+
return x
|
| 78 |
+
|
| 79 |
+
# F.pad expects: (left_last, right_last, left_second_last, right_second_last, ...)
|
| 80 |
+
pad = []
|
| 81 |
+
for orig, target in zip(reversed(x.shape), reversed(target_shape)):
|
| 82 |
+
if target < orig:
|
| 83 |
+
raise ValueError(f"Target size {target} is smaller than current size {orig}")
|
| 84 |
+
pad.extend([0, target - orig])
|
| 85 |
+
|
| 86 |
+
return torch.nn.functional.pad(x, pad, mode="constant", value=pad_value)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def get_padded_hidden_dim(hidden_dim: int, num_devices: int, tile_size: int = 32) -> int:
|
| 90 |
+
"""
|
| 91 |
+
Compute padded hidden_dim to satisfy ttnn.from_torch's tile alignment constraint.
|
| 92 |
+
|
| 93 |
+
ttnn.from_torch requires physical shard shapes to be tile-aligned. When sharding
|
| 94 |
+
a tensor across devices, each shard_dim = hidden_dim / num_devices must be
|
| 95 |
+
divisible by tile_size.
|
| 96 |
+
|
| 97 |
+
We pad the global tensor first, then shard evenly so only the last shard has padding.
|
| 98 |
+
"""
|
| 99 |
+
shard_dim = hidden_dim // num_devices
|
| 100 |
+
padded_shard = ((shard_dim + tile_size - 1) // tile_size) * tile_size
|
| 101 |
+
return padded_shard * num_devices
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def parse_shard_dims_from_mesh_mapper_config(mesh_mapper_config: ttnn.MeshMapperConfig) -> list[int]:
|
| 105 |
+
"""
|
| 106 |
+
Parse shard dimensions from MeshMapperConfig's repr.
|
| 107 |
+
|
| 108 |
+
MeshMapperConfig doesn't expose .placements directly, but repr shows them:
|
| 109 |
+
'MeshMapperConfig(placements: [PlacementShard(-1)], mesh_shape_override=MeshShape([8]))'
|
| 110 |
+
|
| 111 |
+
This parses out the shard dimensions (e.g., [-1]) from PlacementShard entries.
|
| 112 |
+
Returns empty list if no PlacementShard found (e.g., replicated).
|
| 113 |
+
|
| 114 |
+
Note: This is a workaround until TTNN exposes .placements directly.
|
| 115 |
+
"""
|
| 116 |
+
config_repr = repr(mesh_mapper_config)
|
| 117 |
+
matches = re.findall(r"PlacementShard\((-?\d+)\)", config_repr)
|
| 118 |
+
return [int(d) for d in matches]
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def memory_config_to_dict(memory_config: ttnn.MemoryConfig):
|
| 122 |
+
# Convert to plain types for deterministic serialization.
|
| 123 |
+
return {
|
| 124 |
+
"memory_layout": str(memory_config.memory_layout),
|
| 125 |
+
"buffer_type": str(memory_config.buffer_type),
|
| 126 |
+
"shard_spec": str(memory_config.shard_spec),
|
| 127 |
+
"is_sharded": bool(memory_config.is_sharded()),
|
| 128 |
+
"interleaved": bool(memory_config.interleaved),
|
| 129 |
+
"hash": int(memory_config.__hash__()),
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def compute_kernel_config_to_str(compute_kernel_config: ttnn.WormholeComputeKernelConfig):
|
| 134 |
+
# Backward compat shim; prefer compute_kernel_config_to_dict + serialize_config.
|
| 135 |
+
cfg = compute_kernel_config_to_dict(compute_kernel_config)
|
| 136 |
+
return serialize_config(cfg)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def compute_kernel_config_to_dict(compute_kernel_config: ttnn.WormholeComputeKernelConfig):
|
| 140 |
+
return {
|
| 141 |
+
"math_fidelity": str(compute_kernel_config.math_fidelity),
|
| 142 |
+
"math_approx_mode": str(compute_kernel_config.math_approx_mode),
|
| 143 |
+
"fp32_dest_acc_en": bool(compute_kernel_config.fp32_dest_acc_en),
|
| 144 |
+
"packer_l1_acc": bool(compute_kernel_config.packer_l1_acc),
|
| 145 |
+
"dst_full_sync_en": bool(compute_kernel_config.dst_full_sync_en),
|
| 146 |
+
"throttle_level": str(compute_kernel_config.throttle_level),
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def program_config_to_str(program_config: ttnn.MatmulMultiCoreReuseMultiCastDRAMShardedProgramConfig):
|
| 151 |
+
# Backward compat shim; prefer program_config_to_dict + serialize_config.
|
| 152 |
+
cfg = program_config_to_dict(program_config)
|
| 153 |
+
return serialize_config(cfg)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def program_config_to_dict(program_config):
|
| 157 |
+
if hasattr(program_config, "to_json"):
|
| 158 |
+
d = json.loads(program_config.to_json())
|
| 159 |
+
d["type"] = type(program_config).__name__
|
| 160 |
+
return d
|
| 161 |
+
else:
|
| 162 |
+
return {"type": type(program_config).__name__, "repr": repr(program_config)}
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def serialize_config(cfg_dict: dict, fmt: str = "json") -> str:
|
| 166 |
+
if fmt == "json":
|
| 167 |
+
return json.dumps(cfg_dict, sort_keys=True)
|
| 168 |
+
if fmt == "yaml":
|
| 169 |
+
try:
|
| 170 |
+
import yaml
|
| 171 |
+
except ImportError as exc: # pragma: no cover - optional dependency
|
| 172 |
+
raise RuntimeError("PyYAML is required for yaml serialization") from exc
|
| 173 |
+
return yaml.safe_dump(cfg_dict, sort_keys=True)
|
| 174 |
+
raise ValueError(f"Unsupported format: {fmt}")
|
code/models/common/tests/conftest.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
import contextlib
|
| 6 |
+
import fcntl
|
| 7 |
+
import os
|
| 8 |
+
import time
|
| 9 |
+
|
| 10 |
+
import pytest
|
| 11 |
+
|
| 12 |
+
import ttnn
|
| 13 |
+
|
| 14 |
+
# ==============================================================================
|
| 15 |
+
# Device Lock - Coordinates exclusive access to TT devices across processes
|
| 16 |
+
# ==============================================================================
|
| 17 |
+
|
| 18 |
+
_TT_DEVICE_LOCK_PATH = os.environ.get("TT_DEVICE_LOCK_PATH", "/tmp/tt_device.lock")
|
| 19 |
+
_TT_DEVICE_LOCK_TIMEOUT = float(os.environ.get("TT_DEVICE_LOCK_TIMEOUT", "60")) # 1 min default
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class DeviceLockTimeout(Exception):
|
| 23 |
+
"""Raised when acquiring the device lock times out."""
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# todo)) the UMD already provides a lock mechanism -- use it instead of this?
|
| 27 |
+
@contextlib.contextmanager
|
| 28 |
+
def tt_device_lock(lock_path: str = _TT_DEVICE_LOCK_PATH, timeout: float = _TT_DEVICE_LOCK_TIMEOUT):
|
| 29 |
+
"""
|
| 30 |
+
Context manager for exclusive access to TT devices.
|
| 31 |
+
|
| 32 |
+
Uses flock for cross-process coordination. Blocks until lock is acquired
|
| 33 |
+
or timeout is reached.
|
| 34 |
+
|
| 35 |
+
Usage:
|
| 36 |
+
with tt_device_lock():
|
| 37 |
+
mesh = ttnn.open_mesh_device(...)
|
| 38 |
+
# ... do work ...
|
| 39 |
+
ttnn.close_mesh_device(mesh)
|
| 40 |
+
|
| 41 |
+
Debug stuck locks with: lsof /tmp/tt_device.lock
|
| 42 |
+
|
| 43 |
+
Environment variables:
|
| 44 |
+
TT_DEVICE_LOCK_PATH: Override lock file path (default: /tmp/tt_device.lock)
|
| 45 |
+
TT_DEVICE_LOCK_TIMEOUT: Override timeout in seconds (default: 300)
|
| 46 |
+
"""
|
| 47 |
+
lock_dir = os.path.dirname(lock_path)
|
| 48 |
+
if lock_dir and not os.path.exists(lock_dir):
|
| 49 |
+
os.makedirs(lock_dir, exist_ok=True)
|
| 50 |
+
|
| 51 |
+
lock_file = open(lock_path, "a+") # open the file in append mode to avoid truncation race condition among processes
|
| 52 |
+
start_time = time.monotonic()
|
| 53 |
+
lock_acquired = False
|
| 54 |
+
|
| 55 |
+
try:
|
| 56 |
+
# Poll for lock with timeout
|
| 57 |
+
logged_waiting = False
|
| 58 |
+
while True:
|
| 59 |
+
try:
|
| 60 |
+
fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
| 61 |
+
lock_acquired = True
|
| 62 |
+
break
|
| 63 |
+
except BlockingIOError:
|
| 64 |
+
pass # Lock held by another process
|
| 65 |
+
|
| 66 |
+
if not logged_waiting:
|
| 67 |
+
print(f"[tt_device_lock] Waiting for device lock (held by another process)...")
|
| 68 |
+
print(f"[tt_device_lock] Debug with: lsof {lock_path}")
|
| 69 |
+
logged_waiting = True
|
| 70 |
+
|
| 71 |
+
if time.monotonic() - start_time >= timeout:
|
| 72 |
+
lock_file.close()
|
| 73 |
+
raise DeviceLockTimeout(
|
| 74 |
+
f"Timed out after {timeout}s waiting for device lock. " f"Check: lsof {lock_path}"
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
time.sleep(1) # sleep for 1 second to avoid busy-waiting
|
| 78 |
+
|
| 79 |
+
if logged_waiting:
|
| 80 |
+
print(f"[tt_device_lock] Lock acquired after {time.monotonic() - start_time:.1f}s")
|
| 81 |
+
|
| 82 |
+
# Write PID for debugging
|
| 83 |
+
lock_file.truncate(0) # clear the file
|
| 84 |
+
lock_file.write(f"{os.getpid()}\n")
|
| 85 |
+
lock_file.flush()
|
| 86 |
+
|
| 87 |
+
yield
|
| 88 |
+
|
| 89 |
+
finally:
|
| 90 |
+
if lock_acquired:
|
| 91 |
+
fcntl.flock(lock_file, fcntl.LOCK_UN)
|
| 92 |
+
lock_file.close()
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def pytest_collection_modifyitems(config, items):
|
| 96 |
+
"""Deselect tests where ttnn_mesh_device fixture doesn't match mesh_shape param.
|
| 97 |
+
|
| 98 |
+
This enables tests to use cross-product parametrization (all meshes × all cases)
|
| 99 |
+
while only running the valid combinations, without noisy skip messages.
|
| 100 |
+
"""
|
| 101 |
+
selected = []
|
| 102 |
+
deselected = []
|
| 103 |
+
|
| 104 |
+
for item in items:
|
| 105 |
+
if not hasattr(item, "callspec"):
|
| 106 |
+
selected.append(item)
|
| 107 |
+
continue
|
| 108 |
+
|
| 109 |
+
params = item.callspec.params
|
| 110 |
+
fixture_mesh = params.get("ttnn_mesh_device")
|
| 111 |
+
required_mesh = params.get("mesh_shape")
|
| 112 |
+
|
| 113 |
+
# Keep test if no mesh_shape param or if meshes match
|
| 114 |
+
if required_mesh is None or fixture_mesh == required_mesh:
|
| 115 |
+
selected.append(item)
|
| 116 |
+
else:
|
| 117 |
+
deselected.append(item)
|
| 118 |
+
|
| 119 |
+
items[:] = selected
|
| 120 |
+
if deselected:
|
| 121 |
+
config.hook.pytest_deselected(items=deselected)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
@pytest.fixture(scope="module")
|
| 125 |
+
def ttnn_mesh_device(request):
|
| 126 |
+
"""Create and yield a mesh device for a given mesh shape, cleanup on teardown."""
|
| 127 |
+
if not hasattr(request, "param"):
|
| 128 |
+
pytest.skip(f"{__file__}: mesh_device fixture called without parametrization")
|
| 129 |
+
|
| 130 |
+
if ttnn.device.is_blackhole():
|
| 131 |
+
pytest.skip(f"{__file__}: Blackhole device is not supported for this test yet")
|
| 132 |
+
|
| 133 |
+
# request.param is either a Sequence of ints or a dict with fabric_config and etc.
|
| 134 |
+
params = getattr(request, "param", tuple())
|
| 135 |
+
if isinstance(params, tuple):
|
| 136 |
+
mesh_shape = params
|
| 137 |
+
updated_params = dict()
|
| 138 |
+
else:
|
| 139 |
+
try:
|
| 140 |
+
updated_params = params.copy()
|
| 141 |
+
mesh_shape = updated_params.pop("mesh_shape")
|
| 142 |
+
except Exception as e:
|
| 143 |
+
pytest.skip(f"{__file__}: mesh_shape is required: {e}")
|
| 144 |
+
|
| 145 |
+
# Pre-check: if no devices at all, skip without invoking C++ open.
|
| 146 |
+
# Some environments can throw here (e.g. transient driver/UMD issues); treat as "device unavailable".
|
| 147 |
+
try:
|
| 148 |
+
num_pcie = ttnn.get_num_pcie_devices()
|
| 149 |
+
except Exception as e:
|
| 150 |
+
pytest.skip(f"{__file__}: Unable to query TT devices on this system: {e}")
|
| 151 |
+
|
| 152 |
+
if isinstance(num_pcie, int) and num_pcie == 0:
|
| 153 |
+
pytest.skip(f"{__file__}: No TT devices detected on this system")
|
| 154 |
+
|
| 155 |
+
# Pre-check: skip shapes that cannot fit into the SystemMesh to avoid native exceptions
|
| 156 |
+
sys_desc = ttnn._ttnn.multi_device.SystemMeshDescriptor() # type: ignore[attr-defined]
|
| 157 |
+
sys_shape = tuple(sys_desc.shape())
|
| 158 |
+
req_shape = tuple(mesh_shape)
|
| 159 |
+
allowed = _allowed_req_shapes_for_system(sys_shape)
|
| 160 |
+
if req_shape not in allowed:
|
| 161 |
+
pytest.skip(
|
| 162 |
+
f"{__file__}: Requested mesh {req_shape} unsupported on system {sys_shape}. "
|
| 163 |
+
f"Allowed for this system: {allowed}"
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
parent_shape = _pick_parent_shape_for_submesh(sys_shape, req_shape)
|
| 167 |
+
|
| 168 |
+
# config fabric config
|
| 169 |
+
fabric_config = updated_params.pop("fabric_config", None)
|
| 170 |
+
if parent_shape == (1, 1):
|
| 171 |
+
# Single device does not need fabric config.
|
| 172 |
+
pass
|
| 173 |
+
else:
|
| 174 |
+
# Provide default fabric config for the mesh we actually open (full system mesh).
|
| 175 |
+
num_devices = parent_shape[0] * parent_shape[1]
|
| 176 |
+
if fabric_config is None:
|
| 177 |
+
if num_devices >= 8:
|
| 178 |
+
fabric_config = ttnn.FabricConfig.FABRIC_1D_RING
|
| 179 |
+
else:
|
| 180 |
+
fabric_config = ttnn.FabricConfig.FABRIC_1D
|
| 181 |
+
# set all other input arguments to default values by top-level conftest.py
|
| 182 |
+
ttnn.set_fabric_config(
|
| 183 |
+
fabric_config, ttnn.FabricReliabilityMode.STRICT_INIT, None, ttnn.FabricTensixConfig.DISABLED
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
# config dispatch core to default values by conftest.py
|
| 187 |
+
updated_params["dispatch_core_config"] = ttnn.DispatchCoreConfig(type=None, axis=None, fabric_tensix_config=None)
|
| 188 |
+
|
| 189 |
+
# If a test requests a submesh of a larger system mesh (e.g. request 2x4 on a 8x4 system),
|
| 190 |
+
# fabric cannot be initialized on only the subset of devices. In that case, open the full
|
| 191 |
+
# system mesh first, then return the "first" submesh. We intentionally rely on the default
|
| 192 |
+
# offset behavior here (i.e. no explicit offset selection).
|
| 193 |
+
parent_device = None
|
| 194 |
+
submesh_device = None
|
| 195 |
+
|
| 196 |
+
# Acquire exclusive lock to prevent concurrent device access across processes
|
| 197 |
+
with tt_device_lock():
|
| 198 |
+
try:
|
| 199 |
+
if req_shape != parent_shape:
|
| 200 |
+
parent_device = ttnn.open_mesh_device(mesh_shape=ttnn.MeshShape(parent_shape), **updated_params)
|
| 201 |
+
submesh_device = parent_device.create_submesh(ttnn.MeshShape(req_shape))
|
| 202 |
+
yield submesh_device
|
| 203 |
+
else:
|
| 204 |
+
parent_device = ttnn.open_mesh_device(mesh_shape=ttnn.MeshShape(parent_shape), **updated_params)
|
| 205 |
+
yield parent_device
|
| 206 |
+
except Exception as e:
|
| 207 |
+
pytest.skip(f"{__file__}: Mesh device unavailable or unsupported for this configuration: {e}")
|
| 208 |
+
finally:
|
| 209 |
+
if submesh_device is not None:
|
| 210 |
+
ttnn.close_mesh_device(submesh_device)
|
| 211 |
+
if parent_device is not None:
|
| 212 |
+
ttnn.close_mesh_device(parent_device)
|
| 213 |
+
if fabric_config:
|
| 214 |
+
ttnn.set_fabric_config(ttnn.FabricConfig.DISABLED)
|
| 215 |
+
del parent_device
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def _allowed_req_shapes_for_system(sys_shape: tuple[int, int]) -> set[tuple[int, int]]:
|
| 219 |
+
# todo)) Different cluster has potentially different physical interconnects (in terms of number of links, topology, etc.).
|
| 220 |
+
# Thus, a tuple of ints may not be enough to fingerprint the parent/system mesh device. We need to use a more sophisticated fingerprinting mechanism so we can base the allowed list of (sub)mesh shapes on the parent/system mesh device.
|
| 221 |
+
# [INFO] The most robust way to identify the underlying system is to use ttnn.cluster.get_cluster_type(), which returns a ClusterType enum that precisely identifies your hardware configuration. cluster.cpp:16-37
|
| 222 |
+
|
| 223 |
+
_CANDIDATE_REQ_SHAPES = {
|
| 224 |
+
(1, 1): ((1, 1),),
|
| 225 |
+
(1, 2): ((1, 2), (1, 1)),
|
| 226 |
+
(2, 4): ((2, 4), (1, 8), (1, 4), (1, 2), (1, 1)),
|
| 227 |
+
(8, 4): ((8, 4), (4, 8), (1, 8), (1, 4), (1, 2), (1, 1)),
|
| 228 |
+
# [INFO] add more system shapes here
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
allowed: set[tuple[int, int]] = set()
|
| 232 |
+
|
| 233 |
+
if sys_shape in _CANDIDATE_REQ_SHAPES:
|
| 234 |
+
for mesh_shape in _CANDIDATE_REQ_SHAPES[sys_shape]:
|
| 235 |
+
allowed.add(mesh_shape)
|
| 236 |
+
|
| 237 |
+
return allowed
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def _pick_parent_shape_for_submesh(system_shape: tuple[int, int], requested_shape: tuple[int, int]) -> tuple[int, int]:
|
| 241 |
+
# For multi-device workloads we always open the full system mesh (fabric cannot be launched on a subset),
|
| 242 |
+
# but we may choose the *orientation* of the full mesh such that the requested submesh fits with the
|
| 243 |
+
# default offset (i.e. "first submesh").
|
| 244 |
+
if requested_shape == (1, 1):
|
| 245 |
+
return (1, 1)
|
| 246 |
+
|
| 247 |
+
# If the request uses all devices, treat it as a "full-mesh view" shape and open the parent mesh in that view.
|
| 248 |
+
# This enables shapes like (1,32) on a system whose SystemMeshDescriptor reports (8,4).
|
| 249 |
+
system_num_devices = system_shape[0] * system_shape[1]
|
| 250 |
+
requested_num_devices = requested_shape[0] * requested_shape[1]
|
| 251 |
+
if requested_num_devices == system_num_devices:
|
| 252 |
+
return requested_shape
|
| 253 |
+
|
| 254 |
+
if requested_shape[0] <= system_shape[0] and requested_shape[1] <= system_shape[1]:
|
| 255 |
+
return system_shape
|
| 256 |
+
|
| 257 |
+
rotated = (system_shape[1], system_shape[0])
|
| 258 |
+
if requested_shape[0] <= rotated[0] and requested_shape[1] <= rotated[1]:
|
| 259 |
+
return rotated
|
| 260 |
+
|
| 261 |
+
# No orientation can fit this request without an explicit offset / mapping.
|
| 262 |
+
pytest.skip(
|
| 263 |
+
f"{__file__}: Requested submesh {requested_shape} does not fit within system mesh {system_shape} "
|
| 264 |
+
f"(or its rotated view {rotated}) with default offset."
|
| 265 |
+
)
|
code/models/common/tests/requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pytest-cov
|
code/models/common/tests/setup.cfg
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[coverage:run]
|
| 2 |
+
branch = True
|
| 3 |
+
source = models/common/modules
|
| 4 |
+
|
| 5 |
+
[coverage:report]
|
| 6 |
+
fail_under = 80
|
| 7 |
+
show_missing = True
|
| 8 |
+
exclude_lines =
|
| 9 |
+
pragma: no cover
|
| 10 |
+
if TYPE_CHECKING:
|
| 11 |
+
raise NotImplementedError
|
| 12 |
+
@abstractmethod
|
code/models/common/tests/test_auto_compose.py
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
Tests for automatic composition of multi-device sharded tensors using TensorTopology.
|
| 6 |
+
|
| 7 |
+
This test module validates that the auto-composition logic correctly infers
|
| 8 |
+
MeshToTensor composers from a sharded ttnn.Tensor's topology metadata.
|
| 9 |
+
|
| 10 |
+
It validates both host-sharded and device-sharded cases.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
from unittest.mock import MagicMock, patch
|
| 15 |
+
|
| 16 |
+
import pytest
|
| 17 |
+
import torch
|
| 18 |
+
|
| 19 |
+
import ttnn
|
| 20 |
+
from models.common.auto_compose import _infer_mesh_composer_from_topology, to_torch_auto_compose
|
| 21 |
+
|
| 22 |
+
# ======================================================================================
|
| 23 |
+
# Test Parameters (for device-dependent tests)
|
| 24 |
+
# ======================================================================================
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
_DEVICE_TEST_MARKS = {
|
| 28 |
+
"ttnn_mesh_device": pytest.mark.parametrize(
|
| 29 |
+
"ttnn_mesh_device",
|
| 30 |
+
[
|
| 31 |
+
(1, 1), # single device # [INFO] apply auto_compose on single device would incur error in c++ code
|
| 32 |
+
(1, 2), # 1D mesh, 2 devices
|
| 33 |
+
(1, 4), # 1D mesh, 4 devices
|
| 34 |
+
(1, 8), # 1D mesh, 8 devices
|
| 35 |
+
(2, 4), # 2D mesh, 8 devices
|
| 36 |
+
(4, 8), # 2D mesh, 32 devices
|
| 37 |
+
(8, 4), # 2D mesh, 32 devices
|
| 38 |
+
],
|
| 39 |
+
ids=[
|
| 40 |
+
"1x1",
|
| 41 |
+
"1x2",
|
| 42 |
+
"1x4",
|
| 43 |
+
"1x8",
|
| 44 |
+
"2x4",
|
| 45 |
+
"4x8",
|
| 46 |
+
"8x4",
|
| 47 |
+
],
|
| 48 |
+
indirect=True,
|
| 49 |
+
),
|
| 50 |
+
"layout_dtype": pytest.mark.parametrize(
|
| 51 |
+
"layout,dtype",
|
| 52 |
+
[
|
| 53 |
+
(ttnn.ROW_MAJOR_LAYOUT, ttnn.bfloat16),
|
| 54 |
+
(ttnn.TILE_LAYOUT, ttnn.bfloat16),
|
| 55 |
+
(ttnn.TILE_LAYOUT, ttnn.bfloat8_b), # bfloat8_b only works with TILE_LAYOUT
|
| 56 |
+
(ttnn.TILE_LAYOUT, ttnn.bfloat4_b), # bfloat4_b only works with TILE_LAYOUT
|
| 57 |
+
],
|
| 58 |
+
ids=["row_major_bf16", "tile_bf16", "tile_bf8b", "tile_bf4b"],
|
| 59 |
+
),
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# ======================================================================================
|
| 64 |
+
# Helper Functions
|
| 65 |
+
# ======================================================================================
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _make_known_pattern(num_chunks: int) -> torch.Tensor:
|
| 69 |
+
"""
|
| 70 |
+
Produces shape [num_chunks, 1, 3, 1] with per-chunk distinct values.
|
| 71 |
+
Chunk i contains [i*1, i*2, i*3].
|
| 72 |
+
"""
|
| 73 |
+
rows = []
|
| 74 |
+
for i in range(num_chunks):
|
| 75 |
+
rows.append(torch.tensor([[[i * 1.0], [i * 2.0], [i * 3.0]]]).transpose(0, 1)) # [1,3,1]
|
| 76 |
+
data = torch.stack(rows, dim=0) # [num_chunks,1,3,1]
|
| 77 |
+
return data.to(torch.bfloat16)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _make_arange_dtype(
|
| 81 |
+
shape: tuple[int, ...], dtype: torch.dtype = torch.bfloat16, min_value: float = 0, max_value: float = 100
|
| 82 |
+
) -> torch.Tensor:
|
| 83 |
+
"""Create a deterministic tensor with arange data and specified dtype."""
|
| 84 |
+
numel = 1
|
| 85 |
+
for s in shape:
|
| 86 |
+
numel *= s
|
| 87 |
+
# Generate values from min_value to max_value with step of 1
|
| 88 |
+
values = torch.arange(min_value, max_value + 1, dtype=dtype)
|
| 89 |
+
# Randomly sample indices (with replacement) to fill the tensor
|
| 90 |
+
indices = torch.randint(0, len(values), size=(numel,))
|
| 91 |
+
data = values[indices].reshape(shape)
|
| 92 |
+
return data
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _pos_dim(dim: int, rank: int) -> int:
|
| 96 |
+
"""Convert possibly-negative dim to positive index for given rank."""
|
| 97 |
+
return dim % rank
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def _get_hw_shard_unit() -> int:
|
| 101 |
+
"""
|
| 102 |
+
Hardware-related shard unit threshold (default 32).
|
| 103 |
+
Override via env var TT_TEST_SHARD_UNIT for future hardware.
|
| 104 |
+
"""
|
| 105 |
+
try:
|
| 106 |
+
return int(os.environ.get("TT_TEST_SHARD_UNIT", "32"))
|
| 107 |
+
except Exception:
|
| 108 |
+
return 32
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _build_and_compose_sharded(
|
| 112 |
+
torch_in: torch.Tensor,
|
| 113 |
+
device: ttnn.MeshDevice | None,
|
| 114 |
+
layout,
|
| 115 |
+
ttnn_mesh_device: ttnn.MeshDevice,
|
| 116 |
+
shard_dim: int,
|
| 117 |
+
dtype: torch.dtype = ttnn.bfloat16,
|
| 118 |
+
) -> tuple[ttnn.Tensor, torch.Tensor, torch.Tensor]:
|
| 119 |
+
"""Build sharded tensor and compose it back to torch."""
|
| 120 |
+
tt_sharded = ttnn.from_torch(
|
| 121 |
+
torch_in,
|
| 122 |
+
device=device,
|
| 123 |
+
dtype=dtype,
|
| 124 |
+
layout=layout,
|
| 125 |
+
mesh_mapper=ttnn.ShardTensorToMesh(ttnn_mesh_device, dim=shard_dim),
|
| 126 |
+
)
|
| 127 |
+
torch_auto = to_torch_auto_compose(tt_sharded, device=ttnn_mesh_device if device is None else None)
|
| 128 |
+
torch_ref = ttnn.to_torch(tt_sharded, mesh_composer=ttnn.ConcatMeshToTensor(ttnn_mesh_device, dim=shard_dim))
|
| 129 |
+
return tt_sharded, torch_auto, torch_ref
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
# ======================================================================================
|
| 133 |
+
# Device-Dependent Tests (require mesh device fixture)
|
| 134 |
+
# ======================================================================================
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
@pytest.mark.parametrize("storage", ["host", "device"]) # where the sharded tensor lives
|
| 138 |
+
@_DEVICE_TEST_MARKS["ttnn_mesh_device"]
|
| 139 |
+
@_DEVICE_TEST_MARKS["layout_dtype"]
|
| 140 |
+
def test_sharded_1d_basic(ttnn_mesh_device: ttnn.MeshDevice, layout, dtype, storage: str) -> None:
|
| 141 |
+
"""Basic 1D sharding auto-composition for both host and device storage."""
|
| 142 |
+
num_devices = ttnn_mesh_device.get_num_devices()
|
| 143 |
+
|
| 144 |
+
# Input tensor of shape [num_devices, 1, 3, 1]
|
| 145 |
+
torch_in = _make_known_pattern(num_devices)
|
| 146 |
+
|
| 147 |
+
# Build sharded tensor on host or device along dim=0 and compose back
|
| 148 |
+
device = None if storage == "host" else ttnn_mesh_device
|
| 149 |
+
_, torch_auto, torch_ref = _build_and_compose_sharded(
|
| 150 |
+
torch_in, device, layout, ttnn_mesh_device, shard_dim=0, dtype=dtype
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
if dtype == ttnn.bfloat16:
|
| 154 |
+
assert torch.equal(torch_ref, torch_in), "Explicit composer mismatch"
|
| 155 |
+
assert torch.equal(torch_auto, torch_in), "Auto-composer mismatch"
|
| 156 |
+
else:
|
| 157 |
+
# For quantized dtypes, compare auto vs explicit composed results
|
| 158 |
+
assert torch.equal(torch_auto, torch_ref), "Auto vs explicit composer mismatch for quantized dtype"
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
@pytest.mark.parametrize("storage", ["host", "device"]) # where the replicated tensor lives
|
| 162 |
+
@_DEVICE_TEST_MARKS["ttnn_mesh_device"]
|
| 163 |
+
@_DEVICE_TEST_MARKS["layout_dtype"]
|
| 164 |
+
def test_replicate_1d_basic(ttnn_mesh_device: ttnn.MeshDevice, layout, dtype, storage: str) -> None:
|
| 165 |
+
"""Replicated 1D distribution should compose to identity for host and device storage."""
|
| 166 |
+
# Any shape works; replication does not change global shape
|
| 167 |
+
# ttnn.from_torch perform naive quantization to lower dtypes -- work on existing exponent and mantissa values
|
| 168 |
+
# get range of values for bfloat4_b quantization which has 4 bits for the mantissa and shared 8-bit exponent
|
| 169 |
+
min_value, max_value = -7, 7
|
| 170 |
+
torch_in = _make_arange_dtype((2, 3, 4, 5), dtype=torch.float32, min_value=min_value, max_value=max_value)
|
| 171 |
+
|
| 172 |
+
device = None if storage == "host" else ttnn_mesh_device
|
| 173 |
+
tt_replicated = ttnn.from_torch(
|
| 174 |
+
torch_in,
|
| 175 |
+
device=device,
|
| 176 |
+
dtype=dtype,
|
| 177 |
+
layout=layout,
|
| 178 |
+
mesh_mapper=ttnn.ReplicateTensorToMesh(ttnn_mesh_device),
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
# Auto-composition should detect full replication and yield identity
|
| 182 |
+
if device is None:
|
| 183 |
+
torch_auto = to_torch_auto_compose(tt_replicated, device=ttnn_mesh_device)
|
| 184 |
+
else:
|
| 185 |
+
torch_auto = to_torch_auto_compose(tt_replicated)
|
| 186 |
+
|
| 187 |
+
assert torch.equal(torch_auto, torch_in)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
# --------------------------------------------------------------------------------------
|
| 191 |
+
# Shard various tensor dims on 1D meshes
|
| 192 |
+
# --------------------------------------------------------------------------------------
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
@pytest.mark.parametrize("dim", [0, 1, 2, -1])
|
| 196 |
+
@pytest.mark.parametrize("storage", ["host", "device"]) # where the sharded tensor lives
|
| 197 |
+
@_DEVICE_TEST_MARKS["ttnn_mesh_device"]
|
| 198 |
+
@_DEVICE_TEST_MARKS["layout_dtype"]
|
| 199 |
+
def test_sharded_various_dims(ttnn_mesh_device: ttnn.MeshDevice, layout, dtype, dim: int, storage: str) -> None:
|
| 200 |
+
num_devices = ttnn_mesh_device.get_num_devices()
|
| 201 |
+
|
| 202 |
+
rank = 4
|
| 203 |
+
axis = _pos_dim(dim, rank)
|
| 204 |
+
shape = [2, 3, 4, 1]
|
| 205 |
+
shape[axis] = num_devices
|
| 206 |
+
torch_in = _make_arange_dtype(tuple(shape))
|
| 207 |
+
|
| 208 |
+
device = None if storage == "host" else ttnn_mesh_device
|
| 209 |
+
_, torch_auto, torch_ref = _build_and_compose_sharded(torch_in, device, layout, ttnn_mesh_device, dim, dtype=dtype)
|
| 210 |
+
|
| 211 |
+
if dtype == ttnn.bfloat16:
|
| 212 |
+
assert torch.equal(torch_ref, torch_in)
|
| 213 |
+
assert torch.equal(torch_auto, torch_in)
|
| 214 |
+
else:
|
| 215 |
+
assert torch.equal(torch_auto, torch_ref)
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
# --------------------------------------------------------------------------------------
|
| 219 |
+
# Coverage for 2D mesh sharding: shard-shard and replicate-shard
|
| 220 |
+
# --------------------------------------------------------------------------------------
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
@pytest.mark.parametrize("dims_pair", [(0, 1), (0, -1), (1, -1)])
|
| 224 |
+
@pytest.mark.parametrize("storage", ["host", "device"]) # where the sharded tensor lives
|
| 225 |
+
@_DEVICE_TEST_MARKS["ttnn_mesh_device"]
|
| 226 |
+
@_DEVICE_TEST_MARKS["layout_dtype"]
|
| 227 |
+
def test_sharded_2d_basic(
|
| 228 |
+
ttnn_mesh_device: ttnn.MeshDevice, layout, dtype, dims_pair: tuple[int, int], storage: str
|
| 229 |
+
) -> None:
|
| 230 |
+
mesh_shape = tuple(ttnn_mesh_device.shape)
|
| 231 |
+
if len(mesh_shape) != 2 and torch.prod(torch.tensor(mesh_shape)).item() <= 1:
|
| 232 |
+
pytest.skip("Requires a 2D mesh with both dims > 1")
|
| 233 |
+
|
| 234 |
+
rank = 4
|
| 235 |
+
d0 = _pos_dim(dims_pair[0], rank)
|
| 236 |
+
d1 = _pos_dim(dims_pair[1], rank)
|
| 237 |
+
assert d0 != d1, "Shard dims for 2D sharding must be distinct"
|
| 238 |
+
|
| 239 |
+
shape = [2, 3, 4, 5]
|
| 240 |
+
shape[d0] = mesh_shape[0]
|
| 241 |
+
shape[d1] = mesh_shape[1]
|
| 242 |
+
torch_in = _make_arange_dtype(tuple(shape))
|
| 243 |
+
|
| 244 |
+
mapper = ttnn.ShardTensor2dMesh(ttnn_mesh_device, mesh_shape=mesh_shape, dims=(dims_pair[0], dims_pair[1]))
|
| 245 |
+
device = None if storage == "host" else ttnn_mesh_device
|
| 246 |
+
tt_sharded = ttnn.from_torch(torch_in, device=device, dtype=dtype, layout=layout, mesh_mapper=mapper)
|
| 247 |
+
|
| 248 |
+
if device is None:
|
| 249 |
+
torch_auto = to_torch_auto_compose(tt_sharded, device=ttnn_mesh_device)
|
| 250 |
+
else:
|
| 251 |
+
torch_auto = to_torch_auto_compose(tt_sharded)
|
| 252 |
+
composer = ttnn.ConcatMesh2dToTensor(ttnn_mesh_device, mesh_shape=mesh_shape, dims=(dims_pair[0], dims_pair[1]))
|
| 253 |
+
torch_ref = ttnn.to_torch(tt_sharded, mesh_composer=composer)
|
| 254 |
+
|
| 255 |
+
if dtype == ttnn.bfloat16:
|
| 256 |
+
assert torch.equal(torch_ref, torch_in)
|
| 257 |
+
assert torch.equal(torch_auto, torch_in)
|
| 258 |
+
else:
|
| 259 |
+
assert torch.equal(torch_auto, torch_ref)
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
@pytest.mark.parametrize(
|
| 263 |
+
"dims_pair",
|
| 264 |
+
[
|
| 265 |
+
(None, -1), # replicate along mesh dim 0, shard along last tensor dim
|
| 266 |
+
(1, None), # shard along tensor dim 1 on mesh dim 0, replicate mesh dim 1
|
| 267 |
+
],
|
| 268 |
+
)
|
| 269 |
+
@pytest.mark.parametrize("storage", ["host", "device"]) # host vs device sharded tensor
|
| 270 |
+
@_DEVICE_TEST_MARKS["ttnn_mesh_device"]
|
| 271 |
+
@_DEVICE_TEST_MARKS["layout_dtype"]
|
| 272 |
+
def test_sharded_2d_with_replicate(
|
| 273 |
+
ttnn_mesh_device: ttnn.MeshDevice,
|
| 274 |
+
layout,
|
| 275 |
+
dtype,
|
| 276 |
+
dims_pair: tuple[object, object],
|
| 277 |
+
storage: str,
|
| 278 |
+
) -> None:
|
| 279 |
+
# None indicates replicate axis
|
| 280 |
+
replicate_axis = [i for i, d in enumerate(dims_pair) if d is None][0]
|
| 281 |
+
mesh_shape = tuple(ttnn_mesh_device.shape)
|
| 282 |
+
if len(mesh_shape) != 2 and torch.prod(torch.tensor(mesh_shape)).item() <= 1:
|
| 283 |
+
pytest.skip("Requires a 2D mesh with at least one dim > 1 to observe replication")
|
| 284 |
+
|
| 285 |
+
rank = 4
|
| 286 |
+
# Determine which tensor axis is sharded (the non-None entry)
|
| 287 |
+
shard_dim = [d for d in dims_pair if d is not None][0]
|
| 288 |
+
shard_axis = _pos_dim(shard_dim, rank)
|
| 289 |
+
shape = [2, 3, 4, 5]
|
| 290 |
+
# Set size along sharded axis rounded up to a multiple of the other mesh dim
|
| 291 |
+
other_mesh_dim = mesh_shape[1 - replicate_axis]
|
| 292 |
+
shape[shard_axis] = ((shape[shard_axis] + other_mesh_dim - 1) // other_mesh_dim) * other_mesh_dim
|
| 293 |
+
|
| 294 |
+
# get range of values for bfloat4_b quantization which has 4 bits for the mantissa and shared 8-bit exponent
|
| 295 |
+
torch_in = _make_arange_dtype(tuple(shape), dtype=torch.float32, min_value=-7, max_value=7)
|
| 296 |
+
|
| 297 |
+
mapper = ttnn.ShardTensor2dMesh(ttnn_mesh_device, mesh_shape=mesh_shape, dims=dims_pair) # type: ignore[arg-type]
|
| 298 |
+
device = None if storage == "host" else ttnn_mesh_device
|
| 299 |
+
tt_sharded = ttnn.from_torch(torch_in, device=device, dtype=dtype, layout=layout, mesh_mapper=mapper)
|
| 300 |
+
|
| 301 |
+
if device is None:
|
| 302 |
+
torch_auto = to_torch_auto_compose(tt_sharded, device=ttnn_mesh_device)
|
| 303 |
+
else:
|
| 304 |
+
torch_auto = to_torch_auto_compose(tt_sharded)
|
| 305 |
+
|
| 306 |
+
assert torch.equal(torch_auto, torch_in)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
# --------------------------------------------------------------------------------------
|
| 310 |
+
# Tensor shape categories around hardware threshold (e.g., 32)
|
| 311 |
+
# --------------------------------------------------------------------------------------
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
@pytest.mark.parametrize("category", ["lt", "eq", "gt"]) # per-shard length relative to threshold
|
| 315 |
+
@pytest.mark.parametrize("storage", ["host", "device"]) # where the sharded tensor lives
|
| 316 |
+
@_DEVICE_TEST_MARKS["ttnn_mesh_device"]
|
| 317 |
+
@_DEVICE_TEST_MARKS["layout_dtype"]
|
| 318 |
+
def test_sharded_shape_thresholds(
|
| 319 |
+
ttnn_mesh_device: ttnn.MeshDevice, layout, dtype, category: str, storage: str
|
| 320 |
+
) -> None:
|
| 321 |
+
num_devices = ttnn_mesh_device.get_num_devices()
|
| 322 |
+
|
| 323 |
+
unit = _get_hw_shard_unit()
|
| 324 |
+
if category == "lt":
|
| 325 |
+
per_shard = max(1, unit - 1)
|
| 326 |
+
elif category == "eq":
|
| 327 |
+
per_shard = unit
|
| 328 |
+
else:
|
| 329 |
+
per_shard = unit + 1
|
| 330 |
+
|
| 331 |
+
shard_dim = -1 # test last dimension as sharded axis (rank=4)
|
| 332 |
+
rank = 4
|
| 333 |
+
axis = _pos_dim(shard_dim, rank)
|
| 334 |
+
# Global size across sharded dim = per_shard_len * num_devices
|
| 335 |
+
shape = [2, 3, 4, 5]
|
| 336 |
+
shape[axis] = per_shard * num_devices
|
| 337 |
+
torch_in = _make_arange_dtype(tuple(shape))
|
| 338 |
+
|
| 339 |
+
device = None if storage == "host" else ttnn_mesh_device
|
| 340 |
+
_, torch_auto, torch_ref = _build_and_compose_sharded(
|
| 341 |
+
torch_in, device, layout, ttnn_mesh_device, shard_dim, dtype=dtype
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
if dtype == ttnn.bfloat16:
|
| 345 |
+
assert torch.equal(torch_ref, torch_in)
|
| 346 |
+
assert torch.equal(torch_auto, torch_in)
|
| 347 |
+
else:
|
| 348 |
+
assert torch.equal(torch_auto, torch_ref)
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
# --------------------------------------------------------------------------------------
|
| 352 |
+
# Test coverage for auto_compose
|
| 353 |
+
# --------------------------------------------------------------------------------------
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
def test_to_torch_auto_compose_exception_handler():
|
| 357 |
+
"""Test the exception handler in to_torch_auto_compose (lines 38-40)."""
|
| 358 |
+
mock_tensor = MagicMock(spec=ttnn.Tensor)
|
| 359 |
+
mock_topology = MagicMock()
|
| 360 |
+
mock_tensor.tensor_topology.return_value = mock_topology
|
| 361 |
+
mock_topology.placements.return_value = [ttnn.PlacementShard(0)]
|
| 362 |
+
mock_topology.distribution_shape.return_value = [2]
|
| 363 |
+
|
| 364 |
+
mock_device = MagicMock(spec=ttnn.MeshDevice)
|
| 365 |
+
mock_tensor.device.return_value = mock_device
|
| 366 |
+
|
| 367 |
+
with patch("ttnn.create_mesh_composer", return_value="fake_composer"):
|
| 368 |
+
with patch("ttnn.to_torch", side_effect=RuntimeError("Mock failure")):
|
| 369 |
+
with pytest.raises(RuntimeError, match="Mock failure"):
|
| 370 |
+
to_torch_auto_compose(mock_tensor)
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
def test_to_torch_auto_compose_no_device_error():
|
| 374 |
+
"""Test RuntimeError when tensor is on host and no device is provided/available (lines 102-104)."""
|
| 375 |
+
mock_tensor = MagicMock(spec=ttnn.Tensor)
|
| 376 |
+
mock_topology = MagicMock()
|
| 377 |
+
mock_tensor.tensor_topology.return_value = mock_topology
|
| 378 |
+
mock_topology.placements.return_value = [ttnn.PlacementShard(0)]
|
| 379 |
+
mock_topology.distribution_shape.return_value = [2]
|
| 380 |
+
|
| 381 |
+
# Tensor on host
|
| 382 |
+
mock_tensor.device.return_value = None
|
| 383 |
+
|
| 384 |
+
with patch("ttnn.GetDefaultDevice", return_value=None):
|
| 385 |
+
with pytest.raises(RuntimeError, match="Tensor is on host and no mesh_device provided"):
|
| 386 |
+
to_torch_auto_compose(mock_tensor)
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
def test_infer_composer_1d_sharded_mock():
|
| 390 |
+
"""
|
| 391 |
+
Use mocking to hit the 1D sharded paths (lines 113, 125-131)
|
| 392 |
+
if real 1D meshes are hard to come by.
|
| 393 |
+
"""
|
| 394 |
+
mock_tensor = MagicMock(spec=ttnn.Tensor)
|
| 395 |
+
mock_topology = MagicMock()
|
| 396 |
+
mock_tensor.tensor_topology.return_value = mock_topology
|
| 397 |
+
|
| 398 |
+
# Case 1: 1D Sharded
|
| 399 |
+
mock_topology.placements.return_value = [ttnn.PlacementShard(0)]
|
| 400 |
+
mock_topology.distribution_shape.return_value = [2]
|
| 401 |
+
|
| 402 |
+
mock_device = MagicMock(spec=ttnn.MeshDevice)
|
| 403 |
+
mock_device.shape.dims.return_value = 1
|
| 404 |
+
mock_tensor.device.return_value = mock_device
|
| 405 |
+
|
| 406 |
+
with patch("ttnn.create_mesh_composer") as mock_create:
|
| 407 |
+
mock_create.return_value = "fake_composer"
|
| 408 |
+
composer = _infer_mesh_composer_from_topology(mock_tensor)
|
| 409 |
+
assert composer == "fake_composer"
|
| 410 |
+
mock_create.assert_called_once()
|
| 411 |
+
|
| 412 |
+
# Case 2: 1D Replicated
|
| 413 |
+
mock_topology.placements.return_value = [ttnn.PlacementReplicate()]
|
| 414 |
+
mock_topology.distribution_shape.return_value = [2]
|
| 415 |
+
composer = _infer_mesh_composer_from_topology(mock_tensor)
|
| 416 |
+
assert composer is None
|
code/models/common/tests/test_device_lock.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""Unit tests for tt_device_lock cross-process coordination."""
|
| 5 |
+
|
| 6 |
+
import multiprocessing
|
| 7 |
+
import os
|
| 8 |
+
import time
|
| 9 |
+
|
| 10 |
+
import pytest
|
| 11 |
+
|
| 12 |
+
from models.common.tests.conftest import DeviceLockTimeout, tt_device_lock
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class TestDeviceLock:
|
| 16 |
+
"""Tests for tt_device_lock context manager."""
|
| 17 |
+
|
| 18 |
+
def test_lock_acquires_and_releases(self, tmp_path):
|
| 19 |
+
"""Basic acquire/release works."""
|
| 20 |
+
lock_path = str(tmp_path / "test.lock")
|
| 21 |
+
|
| 22 |
+
with tt_device_lock(lock_path=lock_path, timeout=5):
|
| 23 |
+
assert os.path.exists(lock_path)
|
| 24 |
+
with open(lock_path) as f:
|
| 25 |
+
content = f.read().strip()
|
| 26 |
+
assert content == str(os.getpid())
|
| 27 |
+
|
| 28 |
+
def test_lock_blocks_concurrent_access(self, tmp_path):
|
| 29 |
+
"""Second process waits while first holds lock."""
|
| 30 |
+
lock_path = str(tmp_path / "test.lock")
|
| 31 |
+
result_file = str(tmp_path / "results.txt")
|
| 32 |
+
|
| 33 |
+
def worker(worker_id, hold_time):
|
| 34 |
+
"""Acquire lock, record timestamp, hold for hold_time, release."""
|
| 35 |
+
with tt_device_lock(lock_path=lock_path, timeout=30):
|
| 36 |
+
with open(result_file, "a") as f:
|
| 37 |
+
f.write(f"{worker_id}:acquired:{time.time()}\n")
|
| 38 |
+
time.sleep(hold_time)
|
| 39 |
+
with open(result_file, "a") as f:
|
| 40 |
+
f.write(f"{worker_id}:released:{time.time()}\n")
|
| 41 |
+
|
| 42 |
+
# Start worker 1, let it acquire lock
|
| 43 |
+
p1 = multiprocessing.Process(target=worker, args=(1, 2))
|
| 44 |
+
p1.start()
|
| 45 |
+
time.sleep(0.3) # Give p1 time to acquire
|
| 46 |
+
|
| 47 |
+
# Start worker 2, should block
|
| 48 |
+
p2 = multiprocessing.Process(target=worker, args=(2, 0.1))
|
| 49 |
+
p2.start()
|
| 50 |
+
|
| 51 |
+
p1.join()
|
| 52 |
+
p2.join()
|
| 53 |
+
|
| 54 |
+
# Parse results
|
| 55 |
+
with open(result_file) as f:
|
| 56 |
+
lines = f.read().strip().split("\n")
|
| 57 |
+
|
| 58 |
+
events = []
|
| 59 |
+
for line in lines:
|
| 60 |
+
parts = line.split(":")
|
| 61 |
+
events.append((int(parts[0]), parts[1], float(parts[2])))
|
| 62 |
+
|
| 63 |
+
# Worker 1 should acquire before worker 2
|
| 64 |
+
next(e for e in events if e[0] == 1 and e[1] == "acquired")
|
| 65 |
+
w1_release = next(e for e in events if e[0] == 1 and e[1] == "released")
|
| 66 |
+
w2_acquire = next(e for e in events if e[0] == 2 and e[1] == "acquired")
|
| 67 |
+
|
| 68 |
+
# Worker 2 should only acquire AFTER worker 1 releases
|
| 69 |
+
assert (
|
| 70 |
+
w2_acquire[2] >= w1_release[2]
|
| 71 |
+
), f"Worker 2 acquired at {w2_acquire[2]} but worker 1 released at {w1_release[2]}"
|
| 72 |
+
|
| 73 |
+
def test_lock_timeout_raises(self, tmp_path):
|
| 74 |
+
"""Timeout raises DeviceLockTimeout."""
|
| 75 |
+
lock_path = str(tmp_path / "test.lock")
|
| 76 |
+
|
| 77 |
+
def holder():
|
| 78 |
+
"""Hold lock for a long time."""
|
| 79 |
+
with tt_device_lock(lock_path=lock_path, timeout=60):
|
| 80 |
+
time.sleep(10)
|
| 81 |
+
|
| 82 |
+
# Start holder
|
| 83 |
+
p = multiprocessing.Process(target=holder)
|
| 84 |
+
p.start()
|
| 85 |
+
time.sleep(0.3) # Let it acquire
|
| 86 |
+
|
| 87 |
+
# Try to acquire with short timeout - should raise
|
| 88 |
+
try:
|
| 89 |
+
with pytest.raises(DeviceLockTimeout):
|
| 90 |
+
with tt_device_lock(lock_path=lock_path, timeout=1):
|
| 91 |
+
pass
|
| 92 |
+
finally:
|
| 93 |
+
p.terminate()
|
| 94 |
+
p.join()
|
| 95 |
+
|
| 96 |
+
def test_lock_file_created_if_missing(self, tmp_path):
|
| 97 |
+
"""Lock file is created if it doesn't exist."""
|
| 98 |
+
lock_path = str(tmp_path / "subdir" / "test.lock")
|
| 99 |
+
assert not os.path.exists(lock_path)
|
| 100 |
+
|
| 101 |
+
with tt_device_lock(lock_path=lock_path, timeout=5):
|
| 102 |
+
assert os.path.exists(lock_path)
|
| 103 |
+
|
| 104 |
+
def test_sequential_locks_work(self, tmp_path):
|
| 105 |
+
"""Multiple sequential lock/unlock cycles work."""
|
| 106 |
+
lock_path = str(tmp_path / "test.lock")
|
| 107 |
+
|
| 108 |
+
for i in range(3):
|
| 109 |
+
with tt_device_lock(lock_path=lock_path, timeout=5):
|
| 110 |
+
with open(lock_path) as f:
|
| 111 |
+
assert f.read().strip() == str(os.getpid())
|
code/models/common/tests/test_distribute_as.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
Tests for distributing a torch.Tensor using the topology of an existing TTNN tensor.
|
| 6 |
+
|
| 7 |
+
Follows the same style as test_auto_compose.py, but in the opposite
|
| 8 |
+
direction: given a reference distributed tensor, verify that
|
| 9 |
+
`from_torch_dist_as(torch_tensor, ref_tensor)` produces a TTNN tensor
|
| 10 |
+
with the same topology and data that composes back to the original
|
| 11 |
+
torch input.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import pytest
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
import ttnn
|
| 18 |
+
from models.common.auto_compose import extract_tensor_topology_info, to_torch_auto_compose
|
| 19 |
+
from models.common.distribute_as import from_torch_dist_as
|
| 20 |
+
|
| 21 |
+
# ======================================================================================
|
| 22 |
+
# Test Parameters
|
| 23 |
+
# ======================================================================================
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
pytestmark = [
|
| 27 |
+
pytest.mark.parametrize(
|
| 28 |
+
"ttnn_mesh_device",
|
| 29 |
+
[
|
| 30 |
+
(1, 1), # single device
|
| 31 |
+
(1, 2), # 1D mesh, 2 devices
|
| 32 |
+
(1, 8), # 1D mesh, 8 devices
|
| 33 |
+
(2, 4), # 2D mesh, 8 devices
|
| 34 |
+
],
|
| 35 |
+
ids=[
|
| 36 |
+
"1x1",
|
| 37 |
+
"1x2",
|
| 38 |
+
"1x8",
|
| 39 |
+
"2x4",
|
| 40 |
+
],
|
| 41 |
+
indirect=True,
|
| 42 |
+
),
|
| 43 |
+
pytest.mark.parametrize(
|
| 44 |
+
"layout,dtype",
|
| 45 |
+
[
|
| 46 |
+
(ttnn.ROW_MAJOR_LAYOUT, ttnn.bfloat16),
|
| 47 |
+
(ttnn.TILE_LAYOUT, ttnn.bfloat16),
|
| 48 |
+
(ttnn.TILE_LAYOUT, ttnn.bfloat8_b), # bfloat8_b only works with TILE_LAYOUT
|
| 49 |
+
(ttnn.TILE_LAYOUT, ttnn.bfloat4_b), # bfloat4_b only works with TILE_LAYOUT
|
| 50 |
+
],
|
| 51 |
+
ids=["row_major_bf16", "tile_bf16", "tile_bf8b", "tile_bf4b"],
|
| 52 |
+
),
|
| 53 |
+
pytest.mark.parametrize(
|
| 54 |
+
"memory_config",
|
| 55 |
+
[ttnn.DRAM_MEMORY_CONFIG, ttnn.L1_MEMORY_CONFIG],
|
| 56 |
+
ids=["dram", "l1"],
|
| 57 |
+
),
|
| 58 |
+
]
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# ======================================================================================
|
| 62 |
+
# Helpers
|
| 63 |
+
# ======================================================================================
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _make_arange_dtype(
|
| 67 |
+
shape: tuple[int, ...], dtype: torch.dtype = torch.bfloat16, min_value: float = 0, max_value: float = 100
|
| 68 |
+
) -> torch.Tensor:
|
| 69 |
+
"""Create a deterministic tensor with arange data and specified dtype."""
|
| 70 |
+
numel = 1
|
| 71 |
+
for s in shape:
|
| 72 |
+
numel *= s
|
| 73 |
+
# Generate values from min_value to max_value with step of 1
|
| 74 |
+
values = torch.arange(min_value, max_value + 1, dtype=dtype)
|
| 75 |
+
# Randomly sample indices (with replacement) to fill the tensor
|
| 76 |
+
indices = torch.randint(0, len(values), size=(numel,))
|
| 77 |
+
data = values[indices].reshape(shape)
|
| 78 |
+
return data
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def _pos_dim(dim: int, rank: int) -> int:
|
| 82 |
+
return dim % rank
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _topology_signature(tt: ttnn.Tensor) -> tuple[tuple[str, tuple[int, ...]], tuple[int, ...]]:
|
| 86 |
+
placements, dist_shape = extract_tensor_topology_info(tt)
|
| 87 |
+
sig_p = []
|
| 88 |
+
for p in placements:
|
| 89 |
+
if isinstance(p, ttnn.PlacementShard):
|
| 90 |
+
sig_p.append(("shard", (p.dim,)))
|
| 91 |
+
else:
|
| 92 |
+
assert isinstance(p, ttnn.PlacementReplicate)
|
| 93 |
+
sig_p.append(("replicate", ()))
|
| 94 |
+
return tuple(sig_p), tuple(dist_shape)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
# ======================================================================================
|
| 98 |
+
# Tests
|
| 99 |
+
# ======================================================================================
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
@pytest.mark.parametrize("storage", ["host", "device"])
|
| 103 |
+
def test_sharded_1d(
|
| 104 |
+
ttnn_mesh_device: ttnn.MeshDevice, layout, dtype, memory_config: ttnn.MemoryConfig, storage: str
|
| 105 |
+
) -> None:
|
| 106 |
+
is_host_ref = storage == "host"
|
| 107 |
+
num_devices = ttnn_mesh_device.get_num_devices()
|
| 108 |
+
|
| 109 |
+
# Reference topology: host-sharded along dim 0
|
| 110 |
+
shape = (num_devices, 1, 3, 1)
|
| 111 |
+
# get range of values for bfloat4_b quantization which has 4 bits for the mantissa and shared 8-bit exponent
|
| 112 |
+
max_value = 6 # allows for range of value to grow to 7 for new_input
|
| 113 |
+
ref_input = _make_arange_dtype(shape, dtype=torch.float32, min_value=-7, max_value=max_value)
|
| 114 |
+
ref_tt = ttnn.from_torch(
|
| 115 |
+
ref_input,
|
| 116 |
+
device=None if is_host_ref else ttnn_mesh_device,
|
| 117 |
+
dtype=dtype,
|
| 118 |
+
layout=layout,
|
| 119 |
+
memory_config=memory_config,
|
| 120 |
+
mesh_mapper=ttnn.ShardTensorToMesh(ttnn_mesh_device, dim=0),
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
# Distribute a different tensor using the same topology
|
| 124 |
+
new_input = ref_input + 1 # change values to avoid accidental equality
|
| 125 |
+
if is_host_ref:
|
| 126 |
+
tt_as = from_torch_dist_as(new_input, ref_tt, device=ttnn_mesh_device)
|
| 127 |
+
else:
|
| 128 |
+
tt_as = from_torch_dist_as(new_input, ref_tt)
|
| 129 |
+
|
| 130 |
+
# Verify topology matches reference
|
| 131 |
+
expected_topology = _topology_signature(ref_tt)
|
| 132 |
+
assert _topology_signature(tt_as) == expected_topology
|
| 133 |
+
assert tt_as.dtype == dtype
|
| 134 |
+
assert tt_as.memory_config() == memory_config
|
| 135 |
+
|
| 136 |
+
# Verify round-trip via auto-composition matches the source
|
| 137 |
+
if is_host_ref:
|
| 138 |
+
torch_auto = to_torch_auto_compose(tt_as, device=ttnn_mesh_device)
|
| 139 |
+
else:
|
| 140 |
+
torch_auto = to_torch_auto_compose(tt_as)
|
| 141 |
+
assert torch.equal(torch_auto, new_input)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
@pytest.mark.parametrize("storage", ["host", "device"])
|
| 145 |
+
@pytest.mark.parametrize("dim", [0, 1, 2, -1])
|
| 146 |
+
def test_sharded_various_dims(
|
| 147 |
+
ttnn_mesh_device: ttnn.MeshDevice,
|
| 148 |
+
layout,
|
| 149 |
+
dtype,
|
| 150 |
+
memory_config: ttnn.MemoryConfig,
|
| 151 |
+
dim: int,
|
| 152 |
+
storage: str,
|
| 153 |
+
) -> None:
|
| 154 |
+
is_host_ref = storage == "host"
|
| 155 |
+
num_devices = ttnn_mesh_device.get_num_devices()
|
| 156 |
+
|
| 157 |
+
rank = 4
|
| 158 |
+
axis = _pos_dim(dim, rank)
|
| 159 |
+
shape = [2, 3, 4, 1]
|
| 160 |
+
shape[axis] = num_devices
|
| 161 |
+
# get range of values for bfloat4_b quantization which has 4 bits for the mantissa and shared 8-bit exponent
|
| 162 |
+
max_value = 6 # allows for range of value to grow to 7 for new_input
|
| 163 |
+
ref_input = _make_arange_dtype(shape, dtype=torch.float32, min_value=-7, max_value=max_value)
|
| 164 |
+
|
| 165 |
+
ref_tt = ttnn.from_torch(
|
| 166 |
+
ref_input,
|
| 167 |
+
device=None if is_host_ref else ttnn_mesh_device,
|
| 168 |
+
dtype=dtype,
|
| 169 |
+
layout=layout,
|
| 170 |
+
memory_config=memory_config,
|
| 171 |
+
mesh_mapper=ttnn.ShardTensorToMesh(ttnn_mesh_device, dim=dim),
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
new_input = ref_input + 1 # change values to avoid accidental equality
|
| 175 |
+
if is_host_ref:
|
| 176 |
+
tt_as = from_torch_dist_as(new_input, ref_tt, device=ttnn_mesh_device)
|
| 177 |
+
else:
|
| 178 |
+
tt_as = from_torch_dist_as(new_input, ref_tt)
|
| 179 |
+
|
| 180 |
+
assert _topology_signature(tt_as) == _topology_signature(ref_tt)
|
| 181 |
+
assert tt_as.dtype == dtype
|
| 182 |
+
assert tt_as.memory_config() == memory_config
|
| 183 |
+
if is_host_ref:
|
| 184 |
+
torch_auto = to_torch_auto_compose(tt_as, device=ttnn_mesh_device)
|
| 185 |
+
else:
|
| 186 |
+
torch_auto = to_torch_auto_compose(tt_as)
|
| 187 |
+
assert torch.equal(torch_auto, new_input)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
@pytest.mark.parametrize("storage", ["host", "device"])
|
| 191 |
+
@pytest.mark.parametrize("dims_pair", [(0, 1), (0, -1), (1, -1)])
|
| 192 |
+
def test_sharded_2d(
|
| 193 |
+
ttnn_mesh_device: ttnn.MeshDevice,
|
| 194 |
+
layout,
|
| 195 |
+
dtype,
|
| 196 |
+
memory_config: ttnn.MemoryConfig,
|
| 197 |
+
dims_pair: tuple[int, int],
|
| 198 |
+
storage: str,
|
| 199 |
+
) -> None:
|
| 200 |
+
is_host_ref = storage == "host"
|
| 201 |
+
mesh_shape = tuple(ttnn_mesh_device.shape)
|
| 202 |
+
if len(mesh_shape) != 2 and torch.prod(torch.tensor(mesh_shape)).item() <= 1:
|
| 203 |
+
pytest.skip("Requires a 2D mesh with both dims > 1")
|
| 204 |
+
|
| 205 |
+
rank = 4
|
| 206 |
+
d0 = _pos_dim(dims_pair[0], rank)
|
| 207 |
+
d1 = _pos_dim(dims_pair[1], rank)
|
| 208 |
+
assert d0 != d1
|
| 209 |
+
|
| 210 |
+
shape = [2, 3, 4, 5]
|
| 211 |
+
shape[d0] = mesh_shape[0]
|
| 212 |
+
shape[d1] = mesh_shape[1]
|
| 213 |
+
# get range of values for bfloat4_b quantization which has 4 bits for the mantissa and shared 8-bit exponent
|
| 214 |
+
max_value = 6 # allows for range of value to grow to 7 for new_input
|
| 215 |
+
ref_input = _make_arange_dtype(shape, dtype=torch.float32, min_value=-7, max_value=max_value)
|
| 216 |
+
|
| 217 |
+
mapper = ttnn.ShardTensor2dMesh(ttnn_mesh_device, mesh_shape=mesh_shape, dims=(dims_pair[0], dims_pair[1]))
|
| 218 |
+
ref_tt = ttnn.from_torch(
|
| 219 |
+
ref_input,
|
| 220 |
+
device=None if is_host_ref else ttnn_mesh_device,
|
| 221 |
+
dtype=dtype,
|
| 222 |
+
layout=layout,
|
| 223 |
+
memory_config=memory_config,
|
| 224 |
+
mesh_mapper=mapper,
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
new_input = ref_input + 1 # change values to avoid accidental equality
|
| 228 |
+
if is_host_ref:
|
| 229 |
+
tt_as = from_torch_dist_as(new_input, ref_tt, device=ttnn_mesh_device)
|
| 230 |
+
else:
|
| 231 |
+
tt_as = from_torch_dist_as(new_input, ref_tt)
|
| 232 |
+
|
| 233 |
+
assert _topology_signature(tt_as) == _topology_signature(ref_tt)
|
| 234 |
+
assert tt_as.dtype == dtype
|
| 235 |
+
assert tt_as.memory_config() == memory_config
|
| 236 |
+
if is_host_ref:
|
| 237 |
+
torch_auto = to_torch_auto_compose(tt_as, device=ttnn_mesh_device)
|
| 238 |
+
else:
|
| 239 |
+
torch_auto = to_torch_auto_compose(tt_as)
|
| 240 |
+
assert torch.equal(torch_auto, new_input)
|
code/models/common/tests/test_executor_parity.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
Tests for eager == traced executor output parity.
|
| 6 |
+
|
| 7 |
+
The real contract is "same inputs, same outputs, regardless of executor".
|
| 8 |
+
These tests verify that EagerLLMExecutor and TracedLLMExecutor produce
|
| 9 |
+
identical outputs for the same model and inputs.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import pytest
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
import ttnn
|
| 16 |
+
from models.common.models.executor import EagerLLMExecutor, TensorSpec, TracedLLMExecutor
|
| 17 |
+
from models.common.models.module_input_validation import ConfigMismatch
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class TestTensorSpec:
|
| 21 |
+
"""Unit tests for TensorSpec dataclass."""
|
| 22 |
+
|
| 23 |
+
def test_tensor_spec_creation(self):
|
| 24 |
+
"""Test TensorSpec can be created with expected fields."""
|
| 25 |
+
spec = TensorSpec(
|
| 26 |
+
shape=(32, 1, 128256),
|
| 27 |
+
dtype=ttnn.bfloat16,
|
| 28 |
+
layout=ttnn.TILE_LAYOUT,
|
| 29 |
+
memory_config=None,
|
| 30 |
+
)
|
| 31 |
+
assert spec.shape == (32, 1, 128256)
|
| 32 |
+
assert spec.dtype == ttnn.bfloat16
|
| 33 |
+
assert spec.layout == ttnn.TILE_LAYOUT
|
| 34 |
+
assert spec.memory_config is None
|
| 35 |
+
|
| 36 |
+
def test_tensor_spec_from_tensor_requires_device(self):
|
| 37 |
+
"""Test TensorSpec.from_tensor requires an allocated device tensor."""
|
| 38 |
+
# This test documents expected behavior - from_tensor needs a real device tensor
|
| 39 |
+
# Can't test without device, but we can test the method signature exists
|
| 40 |
+
assert hasattr(TensorSpec, "from_tensor")
|
| 41 |
+
assert callable(TensorSpec.from_tensor)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class TestConfigMismatch:
|
| 45 |
+
"""Unit tests for ConfigMismatch dataclass."""
|
| 46 |
+
|
| 47 |
+
def test_config_mismatch_creation(self):
|
| 48 |
+
"""Test ConfigMismatch can be created with expected fields."""
|
| 49 |
+
mismatch = ConfigMismatch(
|
| 50 |
+
module_name="layer[0].attention",
|
| 51 |
+
expected_memcfg=ttnn.DRAM_MEMORY_CONFIG,
|
| 52 |
+
actual_memcfg=ttnn.L1_MEMORY_CONFIG,
|
| 53 |
+
)
|
| 54 |
+
assert mismatch.module_name == "layer[0].attention"
|
| 55 |
+
assert mismatch.expected_memcfg == ttnn.DRAM_MEMORY_CONFIG
|
| 56 |
+
assert mismatch.actual_memcfg == ttnn.L1_MEMORY_CONFIG
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class TestBoundaryAssertions:
|
| 60 |
+
"""Tests for boundary assertions on public methods."""
|
| 61 |
+
|
| 62 |
+
@pytest.fixture
|
| 63 |
+
def mock_model(self):
|
| 64 |
+
"""Create a minimal mock model for testing assertions."""
|
| 65 |
+
|
| 66 |
+
class MockModel:
|
| 67 |
+
vocab_size = 128256
|
| 68 |
+
n_layers = 32
|
| 69 |
+
num_devices = 1
|
| 70 |
+
sampling = None
|
| 71 |
+
model_args = None
|
| 72 |
+
|
| 73 |
+
def set_kv_cache(self, kv_cache):
|
| 74 |
+
pass
|
| 75 |
+
|
| 76 |
+
return MockModel()
|
| 77 |
+
|
| 78 |
+
@pytest.fixture
|
| 79 |
+
def mock_mesh_device(self):
|
| 80 |
+
"""Return None as placeholder - real device tests need hardware."""
|
| 81 |
+
return None
|
| 82 |
+
|
| 83 |
+
def test_prefill_forward_rejects_1d_tokens(self, mock_model):
|
| 84 |
+
"""Test that prefill_forward rejects 1D token tensor."""
|
| 85 |
+
# Can't fully test without device, but can test assertion message format
|
| 86 |
+
tokens_1d = torch.tensor([1, 2, 3])
|
| 87 |
+
assert tokens_1d.dim() == 1
|
| 88 |
+
|
| 89 |
+
# The assertion should fail with a clear message
|
| 90 |
+
with pytest.raises(AssertionError, match=r"tokens must be \[batch_size, seq_len\], got 1D"):
|
| 91 |
+
# We can't call the actual method without a device, but we can test the assertion logic
|
| 92 |
+
assert tokens_1d.dim() == 2, f"tokens must be [batch_size, seq_len], got {tokens_1d.dim()}D"
|
| 93 |
+
|
| 94 |
+
def test_prefill_forward_accepts_2d_tokens(self):
|
| 95 |
+
"""Test that 2D tokens pass the assertion."""
|
| 96 |
+
tokens_2d = torch.tensor([[1, 2, 3]])
|
| 97 |
+
assert tokens_2d.dim() == 2 # Should pass
|
| 98 |
+
|
| 99 |
+
def test_decode_forward_rejects_2d_tokens(self):
|
| 100 |
+
"""Test that decode_forward rejects 2D token tensor."""
|
| 101 |
+
tokens_2d = torch.tensor([[1, 2, 3]])
|
| 102 |
+
assert tokens_2d.dim() == 2
|
| 103 |
+
|
| 104 |
+
with pytest.raises(AssertionError, match=r"tokens must be \[batch_size\], got 2D"):
|
| 105 |
+
assert tokens_2d.dim() == 1, f"tokens must be [batch_size], got {tokens_2d.dim()}D"
|
| 106 |
+
|
| 107 |
+
def test_decode_forward_accepts_1d_tokens(self):
|
| 108 |
+
"""Test that 1D tokens pass the decode assertion."""
|
| 109 |
+
tokens_1d = torch.tensor([1, 2, 3])
|
| 110 |
+
assert tokens_1d.dim() == 1 # Should pass
|
| 111 |
+
|
| 112 |
+
def test_page_table_must_be_2d(self):
|
| 113 |
+
"""Test that page_table must be 2D when provided."""
|
| 114 |
+
page_table_1d = torch.tensor([0, 1, 2])
|
| 115 |
+
assert page_table_1d.dim() == 1
|
| 116 |
+
|
| 117 |
+
with pytest.raises(AssertionError, match=r"page_table must be \[batch_size, max_blocks\], got 1D"):
|
| 118 |
+
assert page_table_1d.dim() == 2, f"page_table must be [batch_size, max_blocks], got {page_table_1d.dim()}D"
|
| 119 |
+
|
| 120 |
+
def test_start_pos_must_be_1d(self):
|
| 121 |
+
"""Test that start_pos must be 1D."""
|
| 122 |
+
start_pos_2d = torch.tensor([[0, 1, 2]])
|
| 123 |
+
assert start_pos_2d.dim() == 2
|
| 124 |
+
|
| 125 |
+
with pytest.raises(AssertionError, match=r"start_pos must be \[batch_size\], got 2D"):
|
| 126 |
+
assert start_pos_2d.dim() == 1, f"start_pos must be [batch_size], got {start_pos_2d.dim()}D"
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
class TestOutputSpecCapture:
|
| 130 |
+
"""Tests for output spec capture during compile."""
|
| 131 |
+
|
| 132 |
+
def test_eager_executor_has_output_spec_attributes(self):
|
| 133 |
+
"""Test that EagerLLMExecutor has output spec attributes."""
|
| 134 |
+
|
| 135 |
+
class MockModel:
|
| 136 |
+
vocab_size = 128256
|
| 137 |
+
n_layers = 32
|
| 138 |
+
num_devices = 1
|
| 139 |
+
sampling = None
|
| 140 |
+
model_args = None
|
| 141 |
+
|
| 142 |
+
def set_kv_cache(self, kv_cache):
|
| 143 |
+
pass
|
| 144 |
+
|
| 145 |
+
# Can't fully instantiate without device, but can check class structure
|
| 146 |
+
assert hasattr(EagerLLMExecutor, "__init__")
|
| 147 |
+
|
| 148 |
+
def test_traced_executor_has_output_spec_attributes(self):
|
| 149 |
+
"""Test that TracedLLMExecutor has output spec attributes."""
|
| 150 |
+
assert hasattr(TracedLLMExecutor, "__init__")
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
class TestValidateModuleConfigs:
|
| 154 |
+
"""Tests for the _validate_module_configs context manager."""
|
| 155 |
+
|
| 156 |
+
def test_validate_module_configs_method_exists(self):
|
| 157 |
+
"""Test that _validate_module_configs method exists on EagerLLMExecutor."""
|
| 158 |
+
assert hasattr(EagerLLMExecutor, "_validate_module_configs")
|
| 159 |
+
|
| 160 |
+
def test_compile_accepts_validate_configs_flag(self):
|
| 161 |
+
"""Test that compile() accepts validate_configs parameter."""
|
| 162 |
+
import inspect
|
| 163 |
+
|
| 164 |
+
sig = inspect.signature(EagerLLMExecutor.compile)
|
| 165 |
+
params = sig.parameters
|
| 166 |
+
assert "validate_configs" in params
|
| 167 |
+
assert params["validate_configs"].default is False
|
code/models/common/tests/test_executor_trace_prefill.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from models.common.models import executor as executor_module
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def test_easy_trace_prefill_replay_copies_only_mutable_inputs(monkeypatch):
|
| 8 |
+
engine = executor_module.TracedLLMExecutor.__new__(executor_module.TracedLLMExecutor)
|
| 9 |
+
engine.mesh_device = "mesh"
|
| 10 |
+
engine.trace_id_prefill = {128: 7}
|
| 11 |
+
engine.trace_inputs_prefill = {
|
| 12 |
+
128: ("device_tokens", "device_cos", "device_sin", "device_page_table", None),
|
| 13 |
+
}
|
| 14 |
+
engine.trace_output_prefill = {128: "trace_output"}
|
| 15 |
+
|
| 16 |
+
monkeypatch.setattr(
|
| 17 |
+
engine,
|
| 18 |
+
"_prepare_prefill_trace_inputs_host",
|
| 19 |
+
lambda tokens, page_table, last_token_idx: (
|
| 20 |
+
"host_tokens",
|
| 21 |
+
"device_cos_slice",
|
| 22 |
+
"device_sin_slice",
|
| 23 |
+
"host_page_table",
|
| 24 |
+
None,
|
| 25 |
+
),
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
copy_calls = []
|
| 29 |
+
monkeypatch.setattr(
|
| 30 |
+
executor_module,
|
| 31 |
+
"copy_host_to_device",
|
| 32 |
+
lambda host_tensors, device_tensors: copy_calls.append((host_tensors, device_tensors)),
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
execute_calls = []
|
| 36 |
+
monkeypatch.setattr(
|
| 37 |
+
executor_module.ttnn,
|
| 38 |
+
"execute_trace",
|
| 39 |
+
lambda mesh_device, trace_id, cq_id, blocking: execute_calls.append((mesh_device, trace_id, cq_id, blocking)),
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
result = engine._easy_trace_prefill(
|
| 43 |
+
tokens="tokens",
|
| 44 |
+
page_table="page_table",
|
| 45 |
+
user_id=0,
|
| 46 |
+
last_token_idx=127,
|
| 47 |
+
prefill_seq_len=128,
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
assert result == "trace_output"
|
| 51 |
+
assert copy_calls == [
|
| 52 |
+
(
|
| 53 |
+
("host_tokens", "host_page_table", None),
|
| 54 |
+
("device_tokens", "device_page_table", None),
|
| 55 |
+
)
|
| 56 |
+
]
|
| 57 |
+
assert execute_calls == [("mesh", 7, 0, False)]
|
code/models/common/tests/test_lazy_weight.py
ADDED
|
@@ -0,0 +1,988 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
Unit tests for LazyWeight module.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import tempfile
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from unittest.mock import MagicMock, patch
|
| 11 |
+
|
| 12 |
+
import pytest
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
import ttnn
|
| 16 |
+
from models.common.modules.lazy_weight import (
|
| 17 |
+
LazyWeight,
|
| 18 |
+
_auto_pad_for_sharded_tiles,
|
| 19 |
+
_from_torch_and_dump,
|
| 20 |
+
resolve_lazy_weight,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
# ============================================================================
|
| 24 |
+
# Fixtures
|
| 25 |
+
# ============================================================================
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@pytest.fixture
|
| 29 |
+
def mock_tensor():
|
| 30 |
+
"""Create a mock torch tensor for testing."""
|
| 31 |
+
return torch.randn(1, 32, 128)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@pytest.fixture
|
| 35 |
+
def mock_tensor_2d():
|
| 36 |
+
"""Create a 2D mock torch tensor for testing."""
|
| 37 |
+
return torch.randn(64, 128)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@pytest.fixture
|
| 41 |
+
def mock_mesh_device():
|
| 42 |
+
"""Create a mock MeshDevice for unit tests."""
|
| 43 |
+
device = MagicMock(spec=ttnn.MeshDevice)
|
| 44 |
+
device.get_num_devices.return_value = 8
|
| 45 |
+
device.id.return_value = "test_device_0"
|
| 46 |
+
return device
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@pytest.fixture
|
| 50 |
+
def mock_memory_config():
|
| 51 |
+
"""Create a mock MemoryConfig."""
|
| 52 |
+
return ttnn.DRAM_MEMORY_CONFIG
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@pytest.fixture
|
| 56 |
+
def mock_mesh_mapper_config():
|
| 57 |
+
"""Create a mock MeshMapperConfig for sharding."""
|
| 58 |
+
return ttnn.MeshMapperConfig(
|
| 59 |
+
placements=[ttnn.PlacementShard(-1)],
|
| 60 |
+
mesh_shape_override=ttnn.MeshShape([8]),
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# ============================================================================
|
| 65 |
+
# Test LazyWeight Construction
|
| 66 |
+
# ============================================================================
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class TestLazyWeightConstruction:
|
| 70 |
+
"""Tests for LazyWeight dataclass construction and __post_init__."""
|
| 71 |
+
|
| 72 |
+
def test_construction_with_tensor(self, mock_tensor):
|
| 73 |
+
"""Test basic construction with a tensor."""
|
| 74 |
+
lw = LazyWeight(source=mock_tensor)
|
| 75 |
+
assert lw.source is mock_tensor
|
| 76 |
+
assert lw.dtype == ttnn.bfloat16
|
| 77 |
+
assert lw.pad_value == 0.0
|
| 78 |
+
assert lw.device is None
|
| 79 |
+
assert lw.mesh_mapper_config is None
|
| 80 |
+
assert lw.memory_config is None
|
| 81 |
+
assert lw.layout is None
|
| 82 |
+
assert lw._value is None
|
| 83 |
+
|
| 84 |
+
def test_construction_with_all_parameters(
|
| 85 |
+
self, mock_tensor, mock_mesh_device, mock_memory_config, mock_mesh_mapper_config
|
| 86 |
+
):
|
| 87 |
+
"""Test construction with all parameters specified."""
|
| 88 |
+
cache_dir = Path("/tmp/cache")
|
| 89 |
+
lw = LazyWeight(
|
| 90 |
+
source=mock_tensor,
|
| 91 |
+
cache_dir_weight_name=(cache_dir, "test_weight"),
|
| 92 |
+
pad_value=1.0,
|
| 93 |
+
dtype=ttnn.bfloat4_b,
|
| 94 |
+
device=mock_mesh_device,
|
| 95 |
+
mesh_mapper_config=mock_mesh_mapper_config,
|
| 96 |
+
memory_config=mock_memory_config,
|
| 97 |
+
layout=ttnn.TILE_LAYOUT,
|
| 98 |
+
)
|
| 99 |
+
assert lw.source is mock_tensor
|
| 100 |
+
assert lw.cache_dir_weight_name == (cache_dir, "test_weight")
|
| 101 |
+
assert lw.pad_value == 1.0
|
| 102 |
+
assert lw.dtype == ttnn.bfloat4_b
|
| 103 |
+
assert lw.device is mock_mesh_device
|
| 104 |
+
assert lw.mesh_mapper_config is mock_mesh_mapper_config
|
| 105 |
+
assert lw.memory_config is mock_memory_config
|
| 106 |
+
assert lw.layout == ttnn.TILE_LAYOUT
|
| 107 |
+
|
| 108 |
+
def test_post_init_validates_shape(self):
|
| 109 |
+
"""Test that __post_init__ validates tensor shape."""
|
| 110 |
+
mock_source = MagicMock()
|
| 111 |
+
mock_source.shape = None
|
| 112 |
+
|
| 113 |
+
with pytest.raises(AssertionError, match="source must have a shape"):
|
| 114 |
+
LazyWeight(source=mock_source)
|
| 115 |
+
|
| 116 |
+
def test_post_init_validates_empty_shape(self):
|
| 117 |
+
"""Test that __post_init__ rejects empty shape."""
|
| 118 |
+
mock_source = MagicMock()
|
| 119 |
+
mock_source.shape = ()
|
| 120 |
+
|
| 121 |
+
with pytest.raises(AssertionError, match="source must have a shape"):
|
| 122 |
+
LazyWeight(source=mock_source)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
# ============================================================================
|
| 126 |
+
# Test is_resolved Method
|
| 127 |
+
# ============================================================================
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
class TestIsResolved:
|
| 131 |
+
"""Tests for LazyWeight.is_resolved() method."""
|
| 132 |
+
|
| 133 |
+
def test_not_resolved_missing_device(self, mock_tensor, mock_memory_config, mock_mesh_mapper_config):
|
| 134 |
+
"""Test is_resolved returns False when device is missing."""
|
| 135 |
+
lw = LazyWeight(
|
| 136 |
+
source=mock_tensor,
|
| 137 |
+
memory_config=mock_memory_config,
|
| 138 |
+
mesh_mapper_config=mock_mesh_mapper_config,
|
| 139 |
+
layout=ttnn.TILE_LAYOUT,
|
| 140 |
+
)
|
| 141 |
+
assert not lw.is_resolved()
|
| 142 |
+
|
| 143 |
+
def test_not_resolved_missing_layout(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 144 |
+
"""Test is_resolved returns False when layout is missing."""
|
| 145 |
+
lw = LazyWeight(
|
| 146 |
+
source=mock_tensor,
|
| 147 |
+
device=mock_mesh_device,
|
| 148 |
+
memory_config=mock_memory_config,
|
| 149 |
+
)
|
| 150 |
+
assert not lw.is_resolved()
|
| 151 |
+
|
| 152 |
+
def test_not_resolved_missing_memory_config(self, mock_tensor, mock_mesh_device):
|
| 153 |
+
"""Test is_resolved returns False when memory_config is missing."""
|
| 154 |
+
lw = LazyWeight(
|
| 155 |
+
source=mock_tensor,
|
| 156 |
+
device=mock_mesh_device,
|
| 157 |
+
layout=ttnn.TILE_LAYOUT,
|
| 158 |
+
)
|
| 159 |
+
assert not lw.is_resolved()
|
| 160 |
+
|
| 161 |
+
def test_is_resolved_with_all_required_fields(
|
| 162 |
+
self, mock_tensor, mock_mesh_device, mock_memory_config, mock_mesh_mapper_config
|
| 163 |
+
):
|
| 164 |
+
"""Test is_resolved returns True when all required fields are set."""
|
| 165 |
+
lw = LazyWeight(
|
| 166 |
+
source=mock_tensor,
|
| 167 |
+
device=mock_mesh_device,
|
| 168 |
+
memory_config=mock_memory_config,
|
| 169 |
+
mesh_mapper_config=mock_mesh_mapper_config,
|
| 170 |
+
layout=ttnn.TILE_LAYOUT,
|
| 171 |
+
)
|
| 172 |
+
assert lw.is_resolved()
|
| 173 |
+
|
| 174 |
+
def test_is_resolved_with_none_mesh_mapper_config(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 175 |
+
"""Test is_resolved returns False when mesh_mapper_config is None (replicated case requires it to be explicitly set)."""
|
| 176 |
+
lw = LazyWeight(
|
| 177 |
+
source=mock_tensor,
|
| 178 |
+
device=mock_mesh_device,
|
| 179 |
+
memory_config=mock_memory_config,
|
| 180 |
+
layout=ttnn.TILE_LAYOUT,
|
| 181 |
+
mesh_mapper_config=None, # None means not resolved per is_resolved check
|
| 182 |
+
)
|
| 183 |
+
# mesh_mapper_config is in required_fields so None means not resolved
|
| 184 |
+
assert not lw.is_resolved()
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
# ============================================================================
|
| 188 |
+
# Test _get_fingerprint Method
|
| 189 |
+
# ============================================================================
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
class TestGetFingerprint:
|
| 193 |
+
"""Tests for LazyWeight._get_fingerprint() method."""
|
| 194 |
+
|
| 195 |
+
def test_fingerprint_includes_source_shape(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 196 |
+
"""Test that fingerprint includes source shape."""
|
| 197 |
+
lw = LazyWeight(
|
| 198 |
+
source=mock_tensor,
|
| 199 |
+
device=mock_mesh_device,
|
| 200 |
+
memory_config=mock_memory_config,
|
| 201 |
+
layout=ttnn.TILE_LAYOUT,
|
| 202 |
+
)
|
| 203 |
+
fingerprint = lw._get_fingerprint()
|
| 204 |
+
assert "srcshape_1_32_128" in fingerprint
|
| 205 |
+
|
| 206 |
+
def test_fingerprint_includes_non_default_memory_config(self, mock_tensor, mock_mesh_device):
|
| 207 |
+
"""Test that fingerprint includes memory_config hash when non-default."""
|
| 208 |
+
# Use L1_MEMORY_CONFIG which is different from DRAM_MEMORY_CONFIG
|
| 209 |
+
lw = LazyWeight(
|
| 210 |
+
source=mock_tensor,
|
| 211 |
+
device=mock_mesh_device,
|
| 212 |
+
memory_config=ttnn.L1_MEMORY_CONFIG,
|
| 213 |
+
layout=ttnn.TILE_LAYOUT,
|
| 214 |
+
)
|
| 215 |
+
fingerprint = lw._get_fingerprint()
|
| 216 |
+
assert "memcfg_" in fingerprint
|
| 217 |
+
|
| 218 |
+
def test_fingerprint_includes_dtype(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 219 |
+
"""Test that fingerprint includes dtype."""
|
| 220 |
+
lw = LazyWeight(
|
| 221 |
+
source=mock_tensor,
|
| 222 |
+
device=mock_mesh_device,
|
| 223 |
+
memory_config=mock_memory_config,
|
| 224 |
+
layout=ttnn.TILE_LAYOUT,
|
| 225 |
+
dtype=ttnn.bfloat16,
|
| 226 |
+
)
|
| 227 |
+
fingerprint = lw._get_fingerprint()
|
| 228 |
+
assert "dtype_BFLOAT16" in fingerprint
|
| 229 |
+
|
| 230 |
+
def test_fingerprint_includes_layout(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 231 |
+
"""Test that fingerprint includes layout."""
|
| 232 |
+
lw = LazyWeight(
|
| 233 |
+
source=mock_tensor,
|
| 234 |
+
device=mock_mesh_device,
|
| 235 |
+
memory_config=mock_memory_config,
|
| 236 |
+
layout=ttnn.TILE_LAYOUT,
|
| 237 |
+
)
|
| 238 |
+
fingerprint = lw._get_fingerprint()
|
| 239 |
+
assert "layout_TILE" in fingerprint
|
| 240 |
+
|
| 241 |
+
def test_fingerprint_includes_pad_value_when_non_default(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 242 |
+
"""Test that fingerprint includes pad_value when non-default."""
|
| 243 |
+
lw = LazyWeight(
|
| 244 |
+
source=mock_tensor,
|
| 245 |
+
device=mock_mesh_device,
|
| 246 |
+
memory_config=mock_memory_config,
|
| 247 |
+
layout=ttnn.TILE_LAYOUT,
|
| 248 |
+
pad_value=1.5,
|
| 249 |
+
)
|
| 250 |
+
fingerprint = lw._get_fingerprint()
|
| 251 |
+
assert "pad_1.5" in fingerprint
|
| 252 |
+
|
| 253 |
+
def test_fingerprint_excludes_pad_value_when_default(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 254 |
+
"""Test that fingerprint excludes pad_value when default (0.0)."""
|
| 255 |
+
lw = LazyWeight(
|
| 256 |
+
source=mock_tensor,
|
| 257 |
+
device=mock_mesh_device,
|
| 258 |
+
memory_config=mock_memory_config,
|
| 259 |
+
layout=ttnn.TILE_LAYOUT,
|
| 260 |
+
pad_value=0.0,
|
| 261 |
+
)
|
| 262 |
+
fingerprint = lw._get_fingerprint()
|
| 263 |
+
assert "pad_" not in fingerprint
|
| 264 |
+
|
| 265 |
+
def test_fingerprint_includes_device_id(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 266 |
+
"""Test that fingerprint includes device id."""
|
| 267 |
+
lw = LazyWeight(
|
| 268 |
+
source=mock_tensor,
|
| 269 |
+
device=mock_mesh_device,
|
| 270 |
+
memory_config=mock_memory_config,
|
| 271 |
+
layout=ttnn.TILE_LAYOUT,
|
| 272 |
+
)
|
| 273 |
+
fingerprint = lw._get_fingerprint()
|
| 274 |
+
assert "device_" in fingerprint
|
| 275 |
+
|
| 276 |
+
def test_fingerprint_device_without_id_method(self, mock_tensor, mock_memory_config):
|
| 277 |
+
"""Test fingerprint uses 'single' when device has no id() method."""
|
| 278 |
+
device = MagicMock()
|
| 279 |
+
del device.id # Remove id attribute
|
| 280 |
+
lw = LazyWeight(
|
| 281 |
+
source=mock_tensor,
|
| 282 |
+
device=device,
|
| 283 |
+
memory_config=mock_memory_config,
|
| 284 |
+
layout=ttnn.TILE_LAYOUT,
|
| 285 |
+
)
|
| 286 |
+
fingerprint = lw._get_fingerprint()
|
| 287 |
+
assert "device_single" in fingerprint
|
| 288 |
+
|
| 289 |
+
def test_fingerprint_changes_with_different_dtype(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 290 |
+
"""Test that fingerprint changes with different dtype."""
|
| 291 |
+
lw1 = LazyWeight(
|
| 292 |
+
source=mock_tensor,
|
| 293 |
+
device=mock_mesh_device,
|
| 294 |
+
memory_config=mock_memory_config,
|
| 295 |
+
layout=ttnn.TILE_LAYOUT,
|
| 296 |
+
dtype=ttnn.bfloat16,
|
| 297 |
+
)
|
| 298 |
+
lw2 = LazyWeight(
|
| 299 |
+
source=mock_tensor,
|
| 300 |
+
device=mock_mesh_device,
|
| 301 |
+
memory_config=mock_memory_config,
|
| 302 |
+
layout=ttnn.TILE_LAYOUT,
|
| 303 |
+
dtype=ttnn.bfloat4_b,
|
| 304 |
+
)
|
| 305 |
+
assert lw1._get_fingerprint() != lw2._get_fingerprint()
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
# ============================================================================
|
| 309 |
+
# Test _get_mesh_mapper_fingerprint Method
|
| 310 |
+
# ============================================================================
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
class TestGetMeshMapperFingerprint:
|
| 314 |
+
"""Tests for LazyWeight._get_mesh_mapper_fingerprint() method."""
|
| 315 |
+
|
| 316 |
+
def test_fingerprint_replicated_when_none(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 317 |
+
"""Test that fingerprint is 'replicated' when mesh_mapper_config is None."""
|
| 318 |
+
lw = LazyWeight(
|
| 319 |
+
source=mock_tensor,
|
| 320 |
+
device=mock_mesh_device,
|
| 321 |
+
memory_config=mock_memory_config,
|
| 322 |
+
layout=ttnn.TILE_LAYOUT,
|
| 323 |
+
mesh_mapper_config=None,
|
| 324 |
+
)
|
| 325 |
+
fingerprint = lw._get_mesh_mapper_fingerprint()
|
| 326 |
+
assert fingerprint == "replicated"
|
| 327 |
+
|
| 328 |
+
def test_fingerprint_with_mesh_mapper_config(
|
| 329 |
+
self, mock_tensor, mock_mesh_device, mock_memory_config, mock_mesh_mapper_config
|
| 330 |
+
):
|
| 331 |
+
"""Test that fingerprint includes mapper hash when config is set."""
|
| 332 |
+
lw = LazyWeight(
|
| 333 |
+
source=mock_tensor,
|
| 334 |
+
device=mock_mesh_device,
|
| 335 |
+
memory_config=mock_memory_config,
|
| 336 |
+
layout=ttnn.TILE_LAYOUT,
|
| 337 |
+
mesh_mapper_config=mock_mesh_mapper_config,
|
| 338 |
+
)
|
| 339 |
+
fingerprint = lw._get_mesh_mapper_fingerprint()
|
| 340 |
+
assert fingerprint.startswith("mapper_")
|
| 341 |
+
# Hash should be 12 characters
|
| 342 |
+
assert len(fingerprint) == len("mapper_") + 12
|
| 343 |
+
|
| 344 |
+
def test_different_configs_produce_different_fingerprints(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 345 |
+
"""Test that different mesh_mapper_configs produce different fingerprints."""
|
| 346 |
+
config1 = ttnn.MeshMapperConfig(
|
| 347 |
+
placements=[ttnn.PlacementShard(-1)],
|
| 348 |
+
mesh_shape_override=ttnn.MeshShape([8]),
|
| 349 |
+
)
|
| 350 |
+
config2 = ttnn.MeshMapperConfig(
|
| 351 |
+
placements=[ttnn.PlacementShard(-2)],
|
| 352 |
+
mesh_shape_override=ttnn.MeshShape([8]),
|
| 353 |
+
)
|
| 354 |
+
lw1 = LazyWeight(
|
| 355 |
+
source=mock_tensor,
|
| 356 |
+
device=mock_mesh_device,
|
| 357 |
+
memory_config=mock_memory_config,
|
| 358 |
+
layout=ttnn.TILE_LAYOUT,
|
| 359 |
+
mesh_mapper_config=config1,
|
| 360 |
+
)
|
| 361 |
+
lw2 = LazyWeight(
|
| 362 |
+
source=mock_tensor,
|
| 363 |
+
device=mock_mesh_device,
|
| 364 |
+
memory_config=mock_memory_config,
|
| 365 |
+
layout=ttnn.TILE_LAYOUT,
|
| 366 |
+
mesh_mapper_config=config2,
|
| 367 |
+
)
|
| 368 |
+
assert lw1._get_mesh_mapper_fingerprint() != lw2._get_mesh_mapper_fingerprint()
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
# ============================================================================
|
| 372 |
+
# Test _get_cache_fill_path Method
|
| 373 |
+
# ============================================================================
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
class TestGetCacheFillPath:
|
| 377 |
+
"""Tests for LazyWeight._get_cache_fill_path() method."""
|
| 378 |
+
|
| 379 |
+
def test_returns_none_when_cache_dir_none(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 380 |
+
"""Test that _get_cache_fill_path returns None when cache_dir is None."""
|
| 381 |
+
lw = LazyWeight(
|
| 382 |
+
source=mock_tensor,
|
| 383 |
+
device=mock_mesh_device,
|
| 384 |
+
memory_config=mock_memory_config,
|
| 385 |
+
layout=ttnn.TILE_LAYOUT,
|
| 386 |
+
)
|
| 387 |
+
result = lw._get_cache_fill_path(cache_dir=None, weight_name="test")
|
| 388 |
+
assert result is None
|
| 389 |
+
|
| 390 |
+
def test_returns_none_when_weight_name_none(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 391 |
+
"""Test that _get_cache_fill_path returns None when weight_name is None."""
|
| 392 |
+
lw = LazyWeight(
|
| 393 |
+
source=mock_tensor,
|
| 394 |
+
device=mock_mesh_device,
|
| 395 |
+
memory_config=mock_memory_config,
|
| 396 |
+
layout=ttnn.TILE_LAYOUT,
|
| 397 |
+
)
|
| 398 |
+
result = lw._get_cache_fill_path(cache_dir=Path("/tmp/cache"), weight_name=None)
|
| 399 |
+
assert result is None
|
| 400 |
+
|
| 401 |
+
def test_returns_path_with_fingerprint(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 402 |
+
"""Test that _get_cache_fill_path returns correct path with fingerprint."""
|
| 403 |
+
lw = LazyWeight(
|
| 404 |
+
source=mock_tensor,
|
| 405 |
+
device=mock_mesh_device,
|
| 406 |
+
memory_config=mock_memory_config,
|
| 407 |
+
layout=ttnn.TILE_LAYOUT,
|
| 408 |
+
)
|
| 409 |
+
cache_dir = Path("/tmp/cache")
|
| 410 |
+
result = lw._get_cache_fill_path(cache_dir=cache_dir, weight_name="my_weight")
|
| 411 |
+
assert result is not None
|
| 412 |
+
assert result.parent == cache_dir
|
| 413 |
+
assert result.name.startswith("my_weight_")
|
| 414 |
+
assert result.suffix == ".tensorbin"
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
# ============================================================================
|
| 418 |
+
# Test padded_shape Property
|
| 419 |
+
# ============================================================================
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
class TestPaddedShape:
|
| 423 |
+
"""Tests for LazyWeight.padded_shape property."""
|
| 424 |
+
|
| 425 |
+
def test_padded_shape_raises_when_not_resolved(self, mock_tensor, mock_mesh_device):
|
| 426 |
+
"""Test that padded_shape raises assertion when not resolved."""
|
| 427 |
+
lw = LazyWeight(
|
| 428 |
+
source=mock_tensor,
|
| 429 |
+
device=mock_mesh_device,
|
| 430 |
+
# Missing memory_config and layout
|
| 431 |
+
)
|
| 432 |
+
with pytest.raises(AssertionError, match="LazyWeight must be resolved"):
|
| 433 |
+
_ = lw.padded_shape
|
| 434 |
+
|
| 435 |
+
def test_padded_shape_raises_when_device_none(self, mock_tensor, mock_memory_config, mock_mesh_mapper_config):
|
| 436 |
+
"""Test that padded_shape raises ValueError when device is None."""
|
| 437 |
+
lw = LazyWeight(
|
| 438 |
+
source=mock_tensor,
|
| 439 |
+
device=None,
|
| 440 |
+
memory_config=mock_memory_config,
|
| 441 |
+
mesh_mapper_config=mock_mesh_mapper_config,
|
| 442 |
+
layout=ttnn.TILE_LAYOUT,
|
| 443 |
+
)
|
| 444 |
+
# First it will fail is_resolved check
|
| 445 |
+
with pytest.raises(AssertionError, match="LazyWeight must be resolved"):
|
| 446 |
+
_ = lw.padded_shape
|
| 447 |
+
|
| 448 |
+
def test_padded_shape_no_padding_when_replicated(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 449 |
+
"""Test that padded_shape returns original shape when replicated (mesh_mapper_config is None)."""
|
| 450 |
+
# Need to set mesh_mapper_config to something to pass is_resolved - use a mock
|
| 451 |
+
lw = LazyWeight(
|
| 452 |
+
source=mock_tensor,
|
| 453 |
+
device=mock_mesh_device,
|
| 454 |
+
memory_config=mock_memory_config,
|
| 455 |
+
layout=ttnn.TILE_LAYOUT,
|
| 456 |
+
mesh_mapper_config=None,
|
| 457 |
+
)
|
| 458 |
+
# Override is_resolved to return True for this test
|
| 459 |
+
with patch.object(lw, "is_resolved", return_value=True):
|
| 460 |
+
result = lw.padded_shape
|
| 461 |
+
assert result == tuple(mock_tensor.shape)
|
| 462 |
+
|
| 463 |
+
def test_padded_shape_no_padding_single_device(self, mock_tensor, mock_memory_config, mock_mesh_mapper_config):
|
| 464 |
+
"""Test that padded_shape returns original shape for single device."""
|
| 465 |
+
single_device = MagicMock(spec=ttnn.MeshDevice)
|
| 466 |
+
single_device.get_num_devices.return_value = 1
|
| 467 |
+
|
| 468 |
+
lw = LazyWeight(
|
| 469 |
+
source=mock_tensor,
|
| 470 |
+
device=single_device,
|
| 471 |
+
memory_config=mock_memory_config,
|
| 472 |
+
mesh_mapper_config=mock_mesh_mapper_config,
|
| 473 |
+
layout=ttnn.TILE_LAYOUT,
|
| 474 |
+
)
|
| 475 |
+
result = lw.padded_shape
|
| 476 |
+
assert result == tuple(mock_tensor.shape)
|
| 477 |
+
|
| 478 |
+
def test_padded_shape_pads_for_sharding(self, mock_memory_config):
|
| 479 |
+
"""Test that padded_shape correctly pads for sharding across multiple devices."""
|
| 480 |
+
# Create tensor with shape that needs padding: dim -1 is 100
|
| 481 |
+
# With 8 devices, shard_dim = 100/8 = 12.5, needs to be tile aligned (32)
|
| 482 |
+
# padded_shard = ceil(12.5/32)*32 = 32
|
| 483 |
+
# padded_hidden = 32 * 8 = 256
|
| 484 |
+
tensor = torch.randn(1, 32, 100)
|
| 485 |
+
device = MagicMock(spec=ttnn.MeshDevice)
|
| 486 |
+
device.get_num_devices.return_value = 8
|
| 487 |
+
|
| 488 |
+
config = ttnn.MeshMapperConfig(
|
| 489 |
+
placements=[ttnn.PlacementShard(-1)],
|
| 490 |
+
mesh_shape_override=ttnn.MeshShape([8]),
|
| 491 |
+
)
|
| 492 |
+
|
| 493 |
+
lw = LazyWeight(
|
| 494 |
+
source=tensor,
|
| 495 |
+
device=device,
|
| 496 |
+
memory_config=mock_memory_config,
|
| 497 |
+
mesh_mapper_config=config,
|
| 498 |
+
layout=ttnn.TILE_LAYOUT,
|
| 499 |
+
)
|
| 500 |
+
result = lw.padded_shape
|
| 501 |
+
# Original: (1, 32, 100), padded dim -1 should be 256 (32*8 where 32 is tile-aligned shard)
|
| 502 |
+
assert result == (1, 32, 256)
|
| 503 |
+
|
| 504 |
+
def test_padded_shape_raises_valueerror_when_device_none_but_resolved(
|
| 505 |
+
self, mock_tensor, mock_memory_config, mock_mesh_mapper_config
|
| 506 |
+
):
|
| 507 |
+
"""Test that padded_shape raises ValueError when device is None but is_resolved mocked True."""
|
| 508 |
+
lw = LazyWeight(
|
| 509 |
+
source=mock_tensor,
|
| 510 |
+
device=None,
|
| 511 |
+
memory_config=mock_memory_config,
|
| 512 |
+
mesh_mapper_config=mock_mesh_mapper_config,
|
| 513 |
+
layout=ttnn.TILE_LAYOUT,
|
| 514 |
+
)
|
| 515 |
+
# Override is_resolved to return True to reach the ValueError branch
|
| 516 |
+
with patch.object(lw, "is_resolved", return_value=True):
|
| 517 |
+
with pytest.raises(ValueError, match="device must be set to compute padded_shape"):
|
| 518 |
+
_ = lw.padded_shape
|
| 519 |
+
|
| 520 |
+
|
| 521 |
+
# ============================================================================
|
| 522 |
+
# Test get_device_weight Method
|
| 523 |
+
# ============================================================================
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
class TestGetDeviceWeight:
|
| 527 |
+
"""Tests for LazyWeight.get_device_weight() method."""
|
| 528 |
+
|
| 529 |
+
def test_raises_when_device_none(self, mock_tensor, mock_memory_config):
|
| 530 |
+
"""Test that get_device_weight raises when device is None."""
|
| 531 |
+
lw = LazyWeight(
|
| 532 |
+
source=mock_tensor,
|
| 533 |
+
device=None,
|
| 534 |
+
memory_config=mock_memory_config,
|
| 535 |
+
layout=ttnn.TILE_LAYOUT,
|
| 536 |
+
)
|
| 537 |
+
with pytest.raises(ValueError, match="device must be provided"):
|
| 538 |
+
lw.get_device_weight()
|
| 539 |
+
|
| 540 |
+
def test_raises_when_layout_none(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 541 |
+
"""Test that get_device_weight raises when layout is None."""
|
| 542 |
+
lw = LazyWeight(
|
| 543 |
+
source=mock_tensor,
|
| 544 |
+
device=mock_mesh_device,
|
| 545 |
+
memory_config=mock_memory_config,
|
| 546 |
+
layout=None,
|
| 547 |
+
)
|
| 548 |
+
with pytest.raises(ValueError, match="layout must be provided"):
|
| 549 |
+
lw.get_device_weight()
|
| 550 |
+
|
| 551 |
+
def test_raises_when_memory_config_none(self, mock_tensor, mock_mesh_device):
|
| 552 |
+
"""Test that get_device_weight raises when memory_config is None."""
|
| 553 |
+
lw = LazyWeight(
|
| 554 |
+
source=mock_tensor,
|
| 555 |
+
device=mock_mesh_device,
|
| 556 |
+
memory_config=None,
|
| 557 |
+
layout=ttnn.TILE_LAYOUT,
|
| 558 |
+
)
|
| 559 |
+
with pytest.raises(ValueError, match="memory_config must be provided"):
|
| 560 |
+
lw.get_device_weight()
|
| 561 |
+
|
| 562 |
+
def test_returns_cached_value_on_second_call(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 563 |
+
"""Test that get_device_weight returns cached _value on second call."""
|
| 564 |
+
mock_ttnn_tensor = MagicMock()
|
| 565 |
+
lw = LazyWeight(
|
| 566 |
+
source=mock_tensor,
|
| 567 |
+
device=mock_mesh_device,
|
| 568 |
+
memory_config=mock_memory_config,
|
| 569 |
+
layout=ttnn.TILE_LAYOUT,
|
| 570 |
+
)
|
| 571 |
+
# Manually set _value to simulate previous call
|
| 572 |
+
lw._value = mock_ttnn_tensor
|
| 573 |
+
|
| 574 |
+
result = lw.get_device_weight()
|
| 575 |
+
assert result is mock_ttnn_tensor
|
| 576 |
+
|
| 577 |
+
def test_get_device_weight_replicated_flow(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 578 |
+
"""Test get_device_weight with replicated tensor (no sharding) - mocked."""
|
| 579 |
+
mock_ttnn_tensor = MagicMock()
|
| 580 |
+
mock_mapper = MagicMock()
|
| 581 |
+
|
| 582 |
+
with patch("models.common.modules.lazy_weight.ttnn") as mock_ttnn:
|
| 583 |
+
mock_ttnn.replicate_tensor_to_mesh_mapper.return_value = mock_mapper
|
| 584 |
+
mock_ttnn.from_torch.return_value = mock_ttnn_tensor
|
| 585 |
+
|
| 586 |
+
lw = LazyWeight(
|
| 587 |
+
source=mock_tensor,
|
| 588 |
+
device=mock_mesh_device,
|
| 589 |
+
memory_config=mock_memory_config,
|
| 590 |
+
layout=ttnn.TILE_LAYOUT,
|
| 591 |
+
mesh_mapper_config=None, # Replicated
|
| 592 |
+
)
|
| 593 |
+
|
| 594 |
+
result = lw.get_device_weight()
|
| 595 |
+
|
| 596 |
+
# Verify replicate_tensor_to_mesh_mapper was called
|
| 597 |
+
mock_ttnn.replicate_tensor_to_mesh_mapper.assert_called_once_with(mock_mesh_device)
|
| 598 |
+
assert result is mock_ttnn_tensor
|
| 599 |
+
|
| 600 |
+
def test_get_device_weight_sharded_flow(
|
| 601 |
+
self, mock_tensor, mock_mesh_device, mock_memory_config, mock_mesh_mapper_config
|
| 602 |
+
):
|
| 603 |
+
"""Test get_device_weight with sharded tensor - mocked."""
|
| 604 |
+
mock_ttnn_tensor = MagicMock()
|
| 605 |
+
mock_mapper = MagicMock()
|
| 606 |
+
|
| 607 |
+
with patch("models.common.modules.lazy_weight.ttnn") as mock_ttnn:
|
| 608 |
+
mock_ttnn.create_mesh_mapper.return_value = mock_mapper
|
| 609 |
+
mock_ttnn.from_torch.return_value = mock_ttnn_tensor
|
| 610 |
+
|
| 611 |
+
lw = LazyWeight(
|
| 612 |
+
source=mock_tensor,
|
| 613 |
+
device=mock_mesh_device,
|
| 614 |
+
memory_config=mock_memory_config,
|
| 615 |
+
layout=ttnn.TILE_LAYOUT,
|
| 616 |
+
mesh_mapper_config=mock_mesh_mapper_config,
|
| 617 |
+
)
|
| 618 |
+
|
| 619 |
+
result = lw.get_device_weight()
|
| 620 |
+
|
| 621 |
+
# Verify create_mesh_mapper was called with the config
|
| 622 |
+
mock_ttnn.create_mesh_mapper.assert_called_once_with(mock_mesh_device, mock_mesh_mapper_config)
|
| 623 |
+
assert result is mock_ttnn_tensor
|
| 624 |
+
|
| 625 |
+
def test_get_device_weight_with_cache_dir_weight_name(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 626 |
+
"""Test get_device_weight extracts cache_dir and weight_name from tuple."""
|
| 627 |
+
mock_ttnn_tensor = MagicMock()
|
| 628 |
+
mock_mapper = MagicMock()
|
| 629 |
+
|
| 630 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 631 |
+
cache_dir = Path(tmpdir)
|
| 632 |
+
|
| 633 |
+
with patch("models.common.modules.lazy_weight.ttnn") as mock_ttnn:
|
| 634 |
+
mock_ttnn.replicate_tensor_to_mesh_mapper.return_value = mock_mapper
|
| 635 |
+
mock_ttnn.from_torch.return_value = mock_ttnn_tensor
|
| 636 |
+
mock_ttnn.StorageType.HOST = ttnn.StorageType.HOST
|
| 637 |
+
mock_ttnn_tensor.storage_type.return_value = ttnn.StorageType.HOST
|
| 638 |
+
|
| 639 |
+
lw = LazyWeight(
|
| 640 |
+
source=mock_tensor,
|
| 641 |
+
cache_dir_weight_name=(cache_dir, "my_weight"),
|
| 642 |
+
device=mock_mesh_device,
|
| 643 |
+
memory_config=mock_memory_config,
|
| 644 |
+
layout=ttnn.TILE_LAYOUT,
|
| 645 |
+
mesh_mapper_config=None,
|
| 646 |
+
)
|
| 647 |
+
|
| 648 |
+
result = lw.get_device_weight()
|
| 649 |
+
assert result is not None
|
| 650 |
+
|
| 651 |
+
def test_get_device_weight_cache_hit(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 652 |
+
"""Test get_device_weight loads from cache when file exists."""
|
| 653 |
+
mock_ttnn_tensor = MagicMock()
|
| 654 |
+
|
| 655 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 656 |
+
cache_dir = Path(tmpdir)
|
| 657 |
+
|
| 658 |
+
# Create LazyWeight and compute cache path WITHOUT mocking
|
| 659 |
+
lw = LazyWeight(
|
| 660 |
+
source=mock_tensor,
|
| 661 |
+
cache_dir_weight_name=(cache_dir, "cached_weight"),
|
| 662 |
+
device=mock_mesh_device,
|
| 663 |
+
memory_config=mock_memory_config,
|
| 664 |
+
layout=ttnn.TILE_LAYOUT,
|
| 665 |
+
mesh_mapper_config=None,
|
| 666 |
+
)
|
| 667 |
+
|
| 668 |
+
# Get expected cache path and create fake cache file
|
| 669 |
+
cache_path = lw._get_cache_fill_path(cache_dir, "cached_weight")
|
| 670 |
+
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
| 671 |
+
cache_path.touch()
|
| 672 |
+
|
| 673 |
+
# Now patch ttnn.load_tensor for the get_device_weight call
|
| 674 |
+
with patch("models.common.modules.lazy_weight.ttnn.load_tensor") as mock_load:
|
| 675 |
+
mock_load.return_value = mock_ttnn_tensor
|
| 676 |
+
|
| 677 |
+
result = lw.get_device_weight()
|
| 678 |
+
|
| 679 |
+
# Verify load_tensor was called (cache hit)
|
| 680 |
+
mock_load.assert_called_once()
|
| 681 |
+
assert result is mock_ttnn_tensor
|
| 682 |
+
|
| 683 |
+
|
| 684 |
+
# ============================================================================
|
| 685 |
+
# Test _auto_pad_for_sharded_tiles Function
|
| 686 |
+
# ============================================================================
|
| 687 |
+
|
| 688 |
+
|
| 689 |
+
class TestAutoPadForSharding:
|
| 690 |
+
"""Tests for _auto_pad_for_sharded_tiles() function."""
|
| 691 |
+
|
| 692 |
+
def test_no_padding_when_shapes_match(self, mock_tensor):
|
| 693 |
+
"""Test that no padding is applied when shapes match."""
|
| 694 |
+
result = _auto_pad_for_sharded_tiles(mock_tensor, tuple(mock_tensor.shape))
|
| 695 |
+
assert result is mock_tensor
|
| 696 |
+
|
| 697 |
+
def test_padding_applied_when_shapes_differ(self):
|
| 698 |
+
"""Test that padding is applied when shapes differ."""
|
| 699 |
+
tensor = torch.randn(1, 32, 100)
|
| 700 |
+
padded_shape = (1, 32, 128)
|
| 701 |
+
|
| 702 |
+
result = _auto_pad_for_sharded_tiles(tensor, padded_shape)
|
| 703 |
+
assert result.shape == padded_shape
|
| 704 |
+
# Original data should be preserved
|
| 705 |
+
assert torch.equal(result[:, :, :100], tensor)
|
| 706 |
+
|
| 707 |
+
def test_padding_with_custom_pad_value(self):
|
| 708 |
+
"""Test padding with custom pad value."""
|
| 709 |
+
tensor = torch.zeros(1, 32, 100)
|
| 710 |
+
padded_shape = (1, 32, 128)
|
| 711 |
+
|
| 712 |
+
result = _auto_pad_for_sharded_tiles(tensor, padded_shape, pad_value=1.0)
|
| 713 |
+
assert result.shape == padded_shape
|
| 714 |
+
# Padded region should have pad_value
|
| 715 |
+
assert torch.allclose(result[:, :, 100:], torch.ones(1, 32, 28))
|
| 716 |
+
|
| 717 |
+
|
| 718 |
+
# ============================================================================
|
| 719 |
+
# Test _from_torch_and_dump Function
|
| 720 |
+
# ============================================================================
|
| 721 |
+
|
| 722 |
+
|
| 723 |
+
class TestFromTorchAndDump:
|
| 724 |
+
"""Tests for _from_torch_and_dump() function."""
|
| 725 |
+
|
| 726 |
+
def test_no_cache_when_cache_file_none(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 727 |
+
"""Test that no caching occurs when cache_file_name is None."""
|
| 728 |
+
mock_ttnn_tensor = MagicMock()
|
| 729 |
+
mock_mapper = MagicMock()
|
| 730 |
+
|
| 731 |
+
with patch("models.common.modules.lazy_weight.ttnn") as mock_ttnn:
|
| 732 |
+
mock_ttnn.from_torch.return_value = mock_ttnn_tensor
|
| 733 |
+
|
| 734 |
+
result = _from_torch_and_dump(
|
| 735 |
+
tensor=mock_tensor,
|
| 736 |
+
device=mock_mesh_device,
|
| 737 |
+
dtype=ttnn.bfloat16,
|
| 738 |
+
layout=ttnn.TILE_LAYOUT,
|
| 739 |
+
memory_config=mock_memory_config,
|
| 740 |
+
mesh_mapper=mock_mapper,
|
| 741 |
+
is_replicated=False,
|
| 742 |
+
pad_value=0.0,
|
| 743 |
+
cache_file_name=None,
|
| 744 |
+
)
|
| 745 |
+
|
| 746 |
+
mock_ttnn.from_torch.assert_called_once()
|
| 747 |
+
assert result is mock_ttnn_tensor
|
| 748 |
+
|
| 749 |
+
def test_cache_dump_when_cache_file_provided(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 750 |
+
"""Test that tensor is cached when cache_file_name is provided."""
|
| 751 |
+
mock_ttnn_tensor = MagicMock()
|
| 752 |
+
mock_ttnn_tensor.storage_type.return_value = ttnn.StorageType.HOST
|
| 753 |
+
|
| 754 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 755 |
+
cache_file = Path(tmpdir) / "test_cache.tensorbin"
|
| 756 |
+
|
| 757 |
+
with patch("models.common.modules.lazy_weight.ttnn") as mock_ttnn:
|
| 758 |
+
mock_ttnn.from_torch.return_value = mock_ttnn_tensor
|
| 759 |
+
mock_ttnn.StorageType.HOST = ttnn.StorageType.HOST
|
| 760 |
+
|
| 761 |
+
_from_torch_and_dump(
|
| 762 |
+
tensor=mock_tensor,
|
| 763 |
+
device=mock_mesh_device,
|
| 764 |
+
dtype=ttnn.bfloat16,
|
| 765 |
+
layout=ttnn.TILE_LAYOUT,
|
| 766 |
+
memory_config=mock_memory_config,
|
| 767 |
+
mesh_mapper=None,
|
| 768 |
+
is_replicated=True,
|
| 769 |
+
pad_value=0.0,
|
| 770 |
+
cache_file_name=cache_file,
|
| 771 |
+
)
|
| 772 |
+
|
| 773 |
+
# Verify from_torch was called with device=None for caching
|
| 774 |
+
call_kwargs = mock_ttnn.from_torch.call_args.kwargs
|
| 775 |
+
assert call_kwargs.get("device") is None
|
| 776 |
+
|
| 777 |
+
# Verify dump_tensor_flatbuffer was called
|
| 778 |
+
mock_ttnn._ttnn.tensor.dump_tensor_flatbuffer.assert_called_once()
|
| 779 |
+
|
| 780 |
+
def test_replicated_uses_none_mesh_mapper(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 781 |
+
"""Test that replicated tensors use None mesh_mapper in from_torch."""
|
| 782 |
+
mock_ttnn_tensor = MagicMock()
|
| 783 |
+
mock_mapper = MagicMock()
|
| 784 |
+
|
| 785 |
+
with patch("models.common.modules.lazy_weight.ttnn") as mock_ttnn:
|
| 786 |
+
mock_ttnn.from_torch.return_value = mock_ttnn_tensor
|
| 787 |
+
|
| 788 |
+
_from_torch_and_dump(
|
| 789 |
+
tensor=mock_tensor,
|
| 790 |
+
device=mock_mesh_device,
|
| 791 |
+
dtype=ttnn.bfloat16,
|
| 792 |
+
layout=ttnn.TILE_LAYOUT,
|
| 793 |
+
memory_config=mock_memory_config,
|
| 794 |
+
mesh_mapper=mock_mapper,
|
| 795 |
+
is_replicated=True,
|
| 796 |
+
pad_value=0.0,
|
| 797 |
+
cache_file_name=None,
|
| 798 |
+
)
|
| 799 |
+
|
| 800 |
+
call_kwargs = mock_ttnn.from_torch.call_args.kwargs
|
| 801 |
+
assert call_kwargs.get("mesh_mapper") is None
|
| 802 |
+
|
| 803 |
+
def test_sharded_uses_mesh_mapper(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 804 |
+
"""Test that sharded tensors use the provided mesh_mapper."""
|
| 805 |
+
mock_ttnn_tensor = MagicMock()
|
| 806 |
+
mock_mapper = MagicMock()
|
| 807 |
+
|
| 808 |
+
with patch("models.common.modules.lazy_weight.ttnn") as mock_ttnn:
|
| 809 |
+
mock_ttnn.from_torch.return_value = mock_ttnn_tensor
|
| 810 |
+
|
| 811 |
+
_from_torch_and_dump(
|
| 812 |
+
tensor=mock_tensor,
|
| 813 |
+
device=mock_mesh_device,
|
| 814 |
+
dtype=ttnn.bfloat16,
|
| 815 |
+
layout=ttnn.TILE_LAYOUT,
|
| 816 |
+
memory_config=mock_memory_config,
|
| 817 |
+
mesh_mapper=mock_mapper,
|
| 818 |
+
is_replicated=False,
|
| 819 |
+
pad_value=0.0,
|
| 820 |
+
cache_file_name=None,
|
| 821 |
+
)
|
| 822 |
+
|
| 823 |
+
call_kwargs = mock_ttnn.from_torch.call_args.kwargs
|
| 824 |
+
assert call_kwargs.get("mesh_mapper") is mock_mapper
|
| 825 |
+
|
| 826 |
+
|
| 827 |
+
# ============================================================================
|
| 828 |
+
# Test resolve_lazy_weight Function
|
| 829 |
+
# ============================================================================
|
| 830 |
+
|
| 831 |
+
|
| 832 |
+
class TestResolveLazyWeight:
|
| 833 |
+
"""Tests for resolve_lazy_weight() function."""
|
| 834 |
+
|
| 835 |
+
def test_resolves_none_fields(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 836 |
+
"""Test that resolve_lazy_weight fills in None fields."""
|
| 837 |
+
lw = LazyWeight(source=mock_tensor)
|
| 838 |
+
|
| 839 |
+
resolved = resolve_lazy_weight(
|
| 840 |
+
lw,
|
| 841 |
+
device=mock_mesh_device,
|
| 842 |
+
memory_config=mock_memory_config,
|
| 843 |
+
layout=ttnn.TILE_LAYOUT,
|
| 844 |
+
)
|
| 845 |
+
|
| 846 |
+
assert resolved.device is mock_mesh_device
|
| 847 |
+
assert resolved.memory_config is mock_memory_config
|
| 848 |
+
assert resolved.layout == ttnn.TILE_LAYOUT
|
| 849 |
+
|
| 850 |
+
def test_does_not_override_existing_fields(self, mock_tensor, mock_mesh_device, mock_memory_config):
|
| 851 |
+
"""Test that resolve_lazy_weight does not override non-None fields."""
|
| 852 |
+
original_device = MagicMock(spec=ttnn.MeshDevice)
|
| 853 |
+
lw = LazyWeight(
|
| 854 |
+
source=mock_tensor,
|
| 855 |
+
device=original_device,
|
| 856 |
+
dtype=ttnn.bfloat4_b,
|
| 857 |
+
)
|
| 858 |
+
|
| 859 |
+
resolved = resolve_lazy_weight(
|
| 860 |
+
lw,
|
| 861 |
+
device=mock_mesh_device,
|
| 862 |
+
dtype=ttnn.bfloat16,
|
| 863 |
+
memory_config=mock_memory_config,
|
| 864 |
+
layout=ttnn.TILE_LAYOUT,
|
| 865 |
+
)
|
| 866 |
+
|
| 867 |
+
# Original non-None fields should be preserved
|
| 868 |
+
assert resolved.device is original_device
|
| 869 |
+
assert resolved.dtype == ttnn.bfloat4_b
|
| 870 |
+
# None fields should be filled
|
| 871 |
+
assert resolved.memory_config is mock_memory_config
|
| 872 |
+
assert resolved.layout == ttnn.TILE_LAYOUT
|
| 873 |
+
|
| 874 |
+
def test_returns_new_instance(self, mock_tensor, mock_mesh_device):
|
| 875 |
+
"""Test that resolve_lazy_weight returns a new LazyWeight instance."""
|
| 876 |
+
lw = LazyWeight(source=mock_tensor)
|
| 877 |
+
resolved = resolve_lazy_weight(lw, device=mock_mesh_device)
|
| 878 |
+
|
| 879 |
+
assert resolved is not lw
|
| 880 |
+
assert resolved.device is mock_mesh_device
|
| 881 |
+
assert lw.device is None # Original unchanged
|
| 882 |
+
|
| 883 |
+
def test_with_no_overrides(self, mock_tensor):
|
| 884 |
+
"""Test resolve_lazy_weight with no kwargs returns equivalent instance."""
|
| 885 |
+
lw = LazyWeight(source=mock_tensor, dtype=ttnn.bfloat16)
|
| 886 |
+
resolved = resolve_lazy_weight(lw)
|
| 887 |
+
|
| 888 |
+
assert resolved is not lw
|
| 889 |
+
assert resolved.source is lw.source
|
| 890 |
+
assert resolved.dtype == lw.dtype
|
| 891 |
+
|
| 892 |
+
|
| 893 |
+
# ============================================================================
|
| 894 |
+
# Integration Tests (require device - use parametrized fixture)
|
| 895 |
+
# ============================================================================
|
| 896 |
+
|
| 897 |
+
|
| 898 |
+
@pytest.mark.parametrize(
|
| 899 |
+
"ttnn_mesh_device",
|
| 900 |
+
[
|
| 901 |
+
(1, 1),
|
| 902 |
+
],
|
| 903 |
+
ids=[
|
| 904 |
+
"1x1",
|
| 905 |
+
],
|
| 906 |
+
indirect=True,
|
| 907 |
+
)
|
| 908 |
+
class TestLazyWeightIntegration:
|
| 909 |
+
"""Integration tests for LazyWeight that require actual TTNN device."""
|
| 910 |
+
|
| 911 |
+
def test_get_device_weight_replicated(self, ttnn_mesh_device: ttnn.MeshDevice):
|
| 912 |
+
"""Test get_device_weight with replicated tensor (no mesh_mapper_config)."""
|
| 913 |
+
tensor = torch.randn(1, 1, 32, 64, dtype=torch.bfloat16)
|
| 914 |
+
|
| 915 |
+
lw = LazyWeight(
|
| 916 |
+
source=tensor,
|
| 917 |
+
device=ttnn_mesh_device,
|
| 918 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 919 |
+
layout=ttnn.TILE_LAYOUT,
|
| 920 |
+
mesh_mapper_config=None, # Replicated
|
| 921 |
+
)
|
| 922 |
+
|
| 923 |
+
result = lw.get_device_weight()
|
| 924 |
+
assert result is not None
|
| 925 |
+
# Subsequent calls should return cached value
|
| 926 |
+
result2 = lw.get_device_weight()
|
| 927 |
+
assert result2 is result
|
| 928 |
+
|
| 929 |
+
def test_get_device_weight_with_cache(self, ttnn_mesh_device: ttnn.MeshDevice):
|
| 930 |
+
"""Test get_device_weight with caching enabled."""
|
| 931 |
+
tensor = torch.randn(1, 1, 32, 64, dtype=torch.bfloat16)
|
| 932 |
+
|
| 933 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 934 |
+
cache_dir = Path(tmpdir)
|
| 935 |
+
|
| 936 |
+
lw = LazyWeight(
|
| 937 |
+
source=tensor,
|
| 938 |
+
cache_dir_weight_name=(cache_dir, "test_weight"),
|
| 939 |
+
device=ttnn_mesh_device,
|
| 940 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 941 |
+
layout=ttnn.TILE_LAYOUT,
|
| 942 |
+
mesh_mapper_config=None,
|
| 943 |
+
)
|
| 944 |
+
|
| 945 |
+
# First call should create cache
|
| 946 |
+
result1 = lw.get_device_weight()
|
| 947 |
+
assert result1 is not None
|
| 948 |
+
|
| 949 |
+
# Verify cache file was created
|
| 950 |
+
cache_files = list(cache_dir.glob("*.tensorbin"))
|
| 951 |
+
assert len(cache_files) == 1
|
| 952 |
+
assert cache_files[0].name.startswith("test_weight_")
|
| 953 |
+
|
| 954 |
+
def test_get_device_weight_cache_hit(self, ttnn_mesh_device: ttnn.MeshDevice):
|
| 955 |
+
"""Test get_device_weight loads from cache on second instance."""
|
| 956 |
+
tensor = torch.randn(1, 1, 32, 64, dtype=torch.bfloat16)
|
| 957 |
+
|
| 958 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 959 |
+
cache_dir = Path(tmpdir)
|
| 960 |
+
|
| 961 |
+
# First LazyWeight creates cache
|
| 962 |
+
lw1 = LazyWeight(
|
| 963 |
+
source=tensor,
|
| 964 |
+
cache_dir_weight_name=(cache_dir, "test_weight"),
|
| 965 |
+
device=ttnn_mesh_device,
|
| 966 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 967 |
+
layout=ttnn.TILE_LAYOUT,
|
| 968 |
+
mesh_mapper_config=None,
|
| 969 |
+
)
|
| 970 |
+
lw1.get_device_weight()
|
| 971 |
+
|
| 972 |
+
# Second LazyWeight should load from cache
|
| 973 |
+
lw2 = LazyWeight(
|
| 974 |
+
source=tensor,
|
| 975 |
+
cache_dir_weight_name=(cache_dir, "test_weight"),
|
| 976 |
+
device=ttnn_mesh_device,
|
| 977 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 978 |
+
layout=ttnn.TILE_LAYOUT,
|
| 979 |
+
mesh_mapper_config=None,
|
| 980 |
+
)
|
| 981 |
+
result2 = lw2.get_device_weight()
|
| 982 |
+
|
| 983 |
+
assert result2 is not None
|
| 984 |
+
|
| 985 |
+
|
| 986 |
+
if __name__ == "__main__":
|
| 987 |
+
# Run unit tests that don't require device
|
| 988 |
+
pytest.main([__file__, "-v", "-k", "not Integration"])
|
code/models/common/tests/test_metrics.py
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
Unit tests for TTNN-native metric functions - NO MOCKS.
|
| 6 |
+
Tests numerical correctness of metrics computed with actual TTNN ops.
|
| 7 |
+
|
| 8 |
+
Run with: pytest test_ttnn_metrics_numerical.py -v
|
| 9 |
+
Requires: TTNN hardware/installation, pytest
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import pytest
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
import ttnn
|
| 16 |
+
|
| 17 |
+
# Import metric functions from the validation framework
|
| 18 |
+
from models.common.metrics import comp_allclose, compute_max_abs_error, compute_mean_abs_error, compute_pcc
|
| 19 |
+
|
| 20 |
+
pytestmark = [
|
| 21 |
+
pytest.mark.parametrize(
|
| 22 |
+
"ttnn_mesh_device",
|
| 23 |
+
[
|
| 24 |
+
(1, 1),
|
| 25 |
+
# todo)) currently device metrics are only supported on 1x1 mesh device, which is experimental feature.
|
| 26 |
+
# we could add support for more mesh shapes later if there is demand.
|
| 27 |
+
# (1, 2),
|
| 28 |
+
# (1, 8),
|
| 29 |
+
# (2, 4),
|
| 30 |
+
],
|
| 31 |
+
ids=[
|
| 32 |
+
"1x1",
|
| 33 |
+
# "1x2",
|
| 34 |
+
# "1x8",
|
| 35 |
+
# "2x4",
|
| 36 |
+
],
|
| 37 |
+
indirect=True,
|
| 38 |
+
),
|
| 39 |
+
pytest.mark.parametrize(
|
| 40 |
+
"layout,dtype",
|
| 41 |
+
[
|
| 42 |
+
(ttnn.ROW_MAJOR_LAYOUT, ttnn.bfloat16),
|
| 43 |
+
(ttnn.TILE_LAYOUT, ttnn.bfloat16),
|
| 44 |
+
(ttnn.TILE_LAYOUT, ttnn.bfloat8_b), # quantized dtypes only supported with TILE_LAYOUT
|
| 45 |
+
(ttnn.TILE_LAYOUT, ttnn.bfloat4_b), # quantized dtypes only supported with TILE_LAYOUT
|
| 46 |
+
],
|
| 47 |
+
ids=["row_major_bf16", "tile_bf16", "tile_bf8b", "tile_bf4b"],
|
| 48 |
+
),
|
| 49 |
+
]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _quantize_like_ttnn(x: torch.Tensor, device: ttnn.MeshDevice, dtype, layout):
|
| 53 |
+
"""Round-trip through TTNN to obtain torch tensor quantized like given dtype.
|
| 54 |
+
|
| 55 |
+
For bf16, returns input unchanged. For bf8b/bf4b, uses TILE_LAYOUT only.
|
| 56 |
+
"""
|
| 57 |
+
x_t = ttnn.from_torch(x, device=device, dtype=dtype, layout=layout)
|
| 58 |
+
return ttnn.to_torch(x_t)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# Test case definitions
|
| 62 |
+
# Format: (name, tensor_a_fn, tensor_b_fn, max_spec, mean_spec, cosine_spec)
|
| 63 |
+
# Each spec is a tuple: (expected_value, tolerance) or None to skip expected check
|
| 64 |
+
TEST_CASES = [
|
| 65 |
+
# Identical tensors
|
| 66 |
+
pytest.param(
|
| 67 |
+
"identical_random",
|
| 68 |
+
lambda: torch.randn(32, 64, dtype=torch.bfloat16),
|
| 69 |
+
lambda t: t.clone(),
|
| 70 |
+
(0.0, 1e-6), # max: expect ~0, tight tolerance
|
| 71 |
+
(0.0, 1e-6), # mean: expect ~0, tight tolerance
|
| 72 |
+
id="identical_random",
|
| 73 |
+
),
|
| 74 |
+
# Known differences
|
| 75 |
+
pytest.param(
|
| 76 |
+
"known_diff_0.5",
|
| 77 |
+
lambda: torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=torch.bfloat16),
|
| 78 |
+
lambda t: t + torch.tensor([[0.0, 0.0, 0.5], [0.0, 0.0, 0.0]], dtype=torch.bfloat16),
|
| 79 |
+
(0.5, 0.02), # max: expect 0.5
|
| 80 |
+
None, # mean: skip expected check, just compare TTNN vs PyTorch
|
| 81 |
+
id="known_diff_max_0.5",
|
| 82 |
+
),
|
| 83 |
+
pytest.param(
|
| 84 |
+
"uniform_diff_0.5",
|
| 85 |
+
lambda: torch.tensor([[1.0, 2.0], [3.0, 4.0]], dtype=torch.bfloat16),
|
| 86 |
+
lambda t: t + 1,
|
| 87 |
+
(1, 0.02), # max: expect 0.5
|
| 88 |
+
(1, 0.02), # mean: expect 0.5 (all elements differ by same amount)
|
| 89 |
+
id="uniform_diff_0.5",
|
| 90 |
+
),
|
| 91 |
+
# Orthogonal vectors
|
| 92 |
+
pytest.param(
|
| 93 |
+
"orthogonal",
|
| 94 |
+
lambda: torch.tensor([[1.0, 0.0]], dtype=torch.bfloat16),
|
| 95 |
+
lambda t: torch.tensor([[0.0, 1.0]], dtype=torch.bfloat16),
|
| 96 |
+
None, # max: skip expected check
|
| 97 |
+
None, # mean: skip expected check
|
| 98 |
+
id="orthogonal_vectors",
|
| 99 |
+
),
|
| 100 |
+
# Opposite vectors
|
| 101 |
+
pytest.param(
|
| 102 |
+
"opposite",
|
| 103 |
+
lambda: torch.ones(1, 16, dtype=torch.bfloat16),
|
| 104 |
+
lambda t: -torch.ones(1, 16, dtype=torch.bfloat16),
|
| 105 |
+
None, # max: skip expected check
|
| 106 |
+
None, # mean: skip expected check
|
| 107 |
+
id="opposite_vectors",
|
| 108 |
+
),
|
| 109 |
+
# Large tensors
|
| 110 |
+
pytest.param(
|
| 111 |
+
"large_128x256",
|
| 112 |
+
lambda: torch.randn(128, 256, dtype=torch.bfloat16),
|
| 113 |
+
lambda t: t + 0.1,
|
| 114 |
+
None, # max: skip expected check
|
| 115 |
+
None, # mean: skip expected check
|
| 116 |
+
id="large_128x256",
|
| 117 |
+
),
|
| 118 |
+
# Edge cases - all zeros
|
| 119 |
+
pytest.param(
|
| 120 |
+
"all_zeros",
|
| 121 |
+
lambda: torch.zeros(16, 16, dtype=torch.bfloat16),
|
| 122 |
+
lambda t: t.clone(),
|
| 123 |
+
(0.0, 1e-6), # max: expect 0
|
| 124 |
+
(0.0, 1e-6), # mean: expect 0
|
| 125 |
+
id="edge_all_zeros",
|
| 126 |
+
),
|
| 127 |
+
# Edge cases - all ones
|
| 128 |
+
pytest.param(
|
| 129 |
+
"all_ones",
|
| 130 |
+
lambda: torch.ones(16, 16, dtype=torch.bfloat16),
|
| 131 |
+
lambda t: t.clone(),
|
| 132 |
+
(0.0, 1e-6), # max: expect 0
|
| 133 |
+
(0.0, 1e-6), # mean: expect 0
|
| 134 |
+
id="edge_all_ones",
|
| 135 |
+
),
|
| 136 |
+
]
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
@pytest.mark.parametrize("name,tensor_a_fn,tensor_b_fn,max_spec,mean_spec", TEST_CASES)
|
| 140 |
+
def test_abs_metrics_vs_pytorch(ttnn_mesh_device, layout, dtype, name, tensor_a_fn, tensor_b_fn, max_spec, mean_spec):
|
| 141 |
+
"""
|
| 142 |
+
Unified test for all metrics against PyTorch ground truth.
|
| 143 |
+
Tests various tensor configurations and verifies TTNN metrics match PyTorch.
|
| 144 |
+
Each metric spec is (expected_value, tolerance) or None.
|
| 145 |
+
"""
|
| 146 |
+
# Generate tensors
|
| 147 |
+
torch_a = tensor_a_fn()
|
| 148 |
+
torch_b = tensor_b_fn(torch_a) if callable(tensor_b_fn) else tensor_b_fn
|
| 149 |
+
|
| 150 |
+
# Compute PyTorch ground truth (use TTNN-like quantized tensors for quantized dtypes)
|
| 151 |
+
torch_a_q = _quantize_like_ttnn(torch_a, ttnn_mesh_device, dtype, layout)
|
| 152 |
+
torch_b_q = _quantize_like_ttnn(torch_b, ttnn_mesh_device, dtype, layout)
|
| 153 |
+
max_error_torch = (torch_a_q - torch_b_q).abs().max().item()
|
| 154 |
+
mean_error_torch = (torch_a_q - torch_b_q).abs().mean().item()
|
| 155 |
+
|
| 156 |
+
# Convert to TTNN
|
| 157 |
+
ttnn_a = ttnn.from_torch(torch_a, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 158 |
+
ttnn_b = ttnn.from_torch(torch_b, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 159 |
+
|
| 160 |
+
# Compute with TTNN (ttnn vs ttnn)
|
| 161 |
+
max_error_ttnn = compute_max_abs_error(ttnn_a, ttnn_b)
|
| 162 |
+
mean_error_ttnn = compute_mean_abs_error(ttnn_a, ttnn_b)
|
| 163 |
+
|
| 164 |
+
# Mixed-mode checks (torch vs ttnn and ttnn vs torch)
|
| 165 |
+
# Use quantized torch views for mixed checks to fairly compare quantized dtypes
|
| 166 |
+
max_error_mixed_torch_ttnn = compute_max_abs_error(torch_a_q, ttnn_b)
|
| 167 |
+
mean_error_mixed_torch_ttnn = compute_mean_abs_error(torch_a_q, ttnn_b)
|
| 168 |
+
max_error_mixed_ttnn_torch = compute_max_abs_error(ttnn_a, torch_b_q)
|
| 169 |
+
mean_error_mixed_ttnn_torch = compute_mean_abs_error(ttnn_a, torch_b_q)
|
| 170 |
+
|
| 171 |
+
# Default tolerance for TTNN vs PyTorch comparison (bf16 precision)
|
| 172 |
+
default_tolerance = 0.02
|
| 173 |
+
|
| 174 |
+
# Verify max_abs_error
|
| 175 |
+
if max_spec is not None:
|
| 176 |
+
expected_max, tolerance_max = max_spec
|
| 177 |
+
assert (
|
| 178 |
+
abs(max_error_ttnn - expected_max) < tolerance_max
|
| 179 |
+
), f"max_abs_error: expected {expected_max}, got {max_error_ttnn}"
|
| 180 |
+
# Check TTNN matches PyTorch
|
| 181 |
+
assert (
|
| 182 |
+
abs(max_error_ttnn - max_error_torch) < tolerance_max
|
| 183 |
+
), f"max_abs_error TTNN vs PyTorch: {max_error_ttnn} vs {max_error_torch}"
|
| 184 |
+
# Mixed variants also match expected and PyTorch
|
| 185 |
+
assert (
|
| 186 |
+
abs(max_error_mixed_torch_ttnn - expected_max) < tolerance_max
|
| 187 |
+
), f"max_abs_error (torch,ttnn): expected {expected_max}, got {max_error_mixed_torch_ttnn}"
|
| 188 |
+
assert (
|
| 189 |
+
abs(max_error_mixed_ttnn_torch - expected_max) < tolerance_max
|
| 190 |
+
), f"max_abs_error (ttnn,torch): expected {expected_max}, got {max_error_mixed_ttnn_torch}"
|
| 191 |
+
assert (
|
| 192 |
+
abs(max_error_mixed_torch_ttnn - max_error_torch) < tolerance_max
|
| 193 |
+
), f"max_abs_error (torch,ttnn) vs PyTorch: {max_error_mixed_torch_ttnn} vs {max_error_torch}"
|
| 194 |
+
assert (
|
| 195 |
+
abs(max_error_mixed_ttnn_torch - max_error_torch) < tolerance_max
|
| 196 |
+
), f"max_abs_error (ttnn,torch) vs PyTorch: {max_error_mixed_ttnn_torch} vs {max_error_torch}"
|
| 197 |
+
else:
|
| 198 |
+
# No expected value, just check TTNN matches PyTorch with default tolerance
|
| 199 |
+
assert (
|
| 200 |
+
abs(max_error_ttnn - max_error_torch) < default_tolerance
|
| 201 |
+
), f"max_abs_error TTNN vs PyTorch: {max_error_ttnn} vs {max_error_torch}"
|
| 202 |
+
assert (
|
| 203 |
+
abs(max_error_mixed_torch_ttnn - max_error_torch) < default_tolerance
|
| 204 |
+
), f"max_abs_error (torch,ttnn) vs PyTorch: {max_error_mixed_torch_ttnn} vs {max_error_torch}"
|
| 205 |
+
assert (
|
| 206 |
+
abs(max_error_mixed_ttnn_torch - max_error_torch) < default_tolerance
|
| 207 |
+
), f"max_abs_error (ttnn,torch) vs PyTorch: {max_error_mixed_ttnn_torch} vs {max_error_torch}"
|
| 208 |
+
|
| 209 |
+
# Verify mean_abs_error
|
| 210 |
+
if mean_spec is not None:
|
| 211 |
+
expected_mean, tolerance_mean = mean_spec
|
| 212 |
+
assert (
|
| 213 |
+
abs(mean_error_ttnn - expected_mean) < tolerance_mean
|
| 214 |
+
), f"mean_abs_error: expected {expected_mean}, got {mean_error_ttnn}"
|
| 215 |
+
# Check TTNN matches PyTorch
|
| 216 |
+
assert (
|
| 217 |
+
abs(mean_error_ttnn - mean_error_torch) < tolerance_mean
|
| 218 |
+
), f"mean_abs_error TTNN vs PyTorch: {mean_error_ttnn} vs {mean_error_torch}"
|
| 219 |
+
# Mixed variants also match expected and PyTorch
|
| 220 |
+
assert (
|
| 221 |
+
abs(mean_error_mixed_torch_ttnn - expected_mean) < tolerance_mean
|
| 222 |
+
), f"mean_abs_error (torch,ttnn): expected {expected_mean}, got {mean_error_mixed_torch_ttnn}"
|
| 223 |
+
assert (
|
| 224 |
+
abs(mean_error_mixed_ttnn_torch - expected_mean) < tolerance_mean
|
| 225 |
+
), f"mean_abs_error (ttnn,torch): expected {expected_mean}, got {mean_error_mixed_ttnn_torch}"
|
| 226 |
+
assert (
|
| 227 |
+
abs(mean_error_mixed_torch_ttnn - mean_error_torch) < tolerance_mean
|
| 228 |
+
), f"mean_abs_error (torch,ttnn) vs PyTorch: {mean_error_mixed_torch_ttnn} vs {mean_error_torch}"
|
| 229 |
+
assert (
|
| 230 |
+
abs(mean_error_mixed_ttnn_torch - mean_error_torch) < tolerance_mean
|
| 231 |
+
), f"mean_abs_error (ttnn,torch) vs PyTorch: {mean_error_mixed_ttnn_torch} vs {mean_error_torch}"
|
| 232 |
+
else:
|
| 233 |
+
# No expected value, just check TTNN matches PyTorch with default tolerance
|
| 234 |
+
assert (
|
| 235 |
+
abs(mean_error_ttnn - mean_error_torch) < default_tolerance
|
| 236 |
+
), f"mean_abs_error TTNN vs PyTorch: {mean_error_ttnn} vs {mean_error_torch}"
|
| 237 |
+
assert (
|
| 238 |
+
abs(mean_error_mixed_torch_ttnn - mean_error_torch) < default_tolerance
|
| 239 |
+
), f"mean_abs_error (torch,ttnn) vs PyTorch: {mean_error_mixed_torch_ttnn} vs {mean_error_torch}"
|
| 240 |
+
assert (
|
| 241 |
+
abs(mean_error_mixed_ttnn_torch - mean_error_torch) < default_tolerance
|
| 242 |
+
), f"mean_abs_error (ttnn,torch) vs PyTorch: {mean_error_mixed_ttnn_torch} vs {mean_error_torch}"
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
# check pcc computed on device against known good pcc values on host
|
| 246 |
+
def test_pcc_ttnn_native(ttnn_mesh_device, layout, dtype):
|
| 247 |
+
"""
|
| 248 |
+
Test TTNN-native PCC computation with actual TTNN tensors on device.
|
| 249 |
+
|
| 250 |
+
Verifies that:
|
| 251 |
+
1. PCC computes correctly on-device using TTNN ops
|
| 252 |
+
2. Results match the robust CPU/numpy implementation
|
| 253 |
+
3. Handles various correlation patterns (perfect, high, negative)
|
| 254 |
+
"""
|
| 255 |
+
print("\nTest: TTNN-native PCC computation")
|
| 256 |
+
|
| 257 |
+
# Test case 1: Perfect positive correlation
|
| 258 |
+
torch.manual_seed(42)
|
| 259 |
+
a_torch = torch.randn(32, 32).bfloat16()
|
| 260 |
+
b_torch = a_torch.clone()
|
| 261 |
+
|
| 262 |
+
a_ttnn = ttnn.from_torch(a_torch, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 263 |
+
b_ttnn = ttnn.from_torch(b_torch, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 264 |
+
|
| 265 |
+
pcc_ttnn = compute_pcc(a_ttnn, b_ttnn)
|
| 266 |
+
a_q = _quantize_like_ttnn(a_torch, ttnn_mesh_device, dtype, layout)
|
| 267 |
+
b_q = _quantize_like_ttnn(b_torch, ttnn_mesh_device, dtype, layout)
|
| 268 |
+
pcc_torch = compute_pcc(a_q, b_q)
|
| 269 |
+
# Mixed-mode PCC (ttnn vs torch and torch vs ttnn)
|
| 270 |
+
pcc_mixed_1 = compute_pcc(a_ttnn, b_q)
|
| 271 |
+
pcc_mixed_2 = compute_pcc(a_q, b_ttnn)
|
| 272 |
+
|
| 273 |
+
print(f" Perfect correlation - TTNN: {pcc_ttnn:.6f}, PyTorch: {pcc_torch:.6f}")
|
| 274 |
+
assert pcc_ttnn >= 0.999, f"Perfect correlation should be ~1.0, got {pcc_ttnn}"
|
| 275 |
+
assert abs(pcc_ttnn - pcc_torch) < 0.01, f"TTNN vs PyTorch mismatch: {pcc_ttnn} vs {pcc_torch}"
|
| 276 |
+
# Mixed should match PyTorch within same tolerance
|
| 277 |
+
assert abs(pcc_mixed_1 - pcc_torch) < 0.01, f"PCC (ttnn,torch) vs PyTorch mismatch: {pcc_mixed_1} vs {pcc_torch}"
|
| 278 |
+
assert abs(pcc_mixed_2 - pcc_torch) < 0.01, f"PCC (torch,ttnn) vs PyTorch mismatch: {pcc_mixed_2} vs {pcc_torch}"
|
| 279 |
+
|
| 280 |
+
# Test case 2: High correlation with small noise
|
| 281 |
+
torch.manual_seed(42)
|
| 282 |
+
a_torch = torch.randn(32, 64).bfloat16()
|
| 283 |
+
b_torch = a_torch + torch.randn(32, 64).bfloat16() * 0.01
|
| 284 |
+
|
| 285 |
+
a_ttnn = ttnn.from_torch(a_torch, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 286 |
+
b_ttnn = ttnn.from_torch(b_torch, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 287 |
+
|
| 288 |
+
pcc_ttnn = compute_pcc(a_ttnn, b_ttnn)
|
| 289 |
+
a_q = _quantize_like_ttnn(a_torch, ttnn_mesh_device, dtype, layout)
|
| 290 |
+
b_q = _quantize_like_ttnn(b_torch, ttnn_mesh_device, dtype, layout)
|
| 291 |
+
pcc_torch = compute_pcc(a_q, b_q)
|
| 292 |
+
pcc_mixed_1 = compute_pcc(a_ttnn, b_q)
|
| 293 |
+
pcc_mixed_2 = compute_pcc(a_q, b_ttnn)
|
| 294 |
+
|
| 295 |
+
print(f" High correlation - TTNN: {pcc_ttnn:.6f}, PyTorch: {pcc_torch:.6f}")
|
| 296 |
+
assert pcc_ttnn >= 0.95, f"High correlation should be >= 0.95, got {pcc_ttnn}"
|
| 297 |
+
# Allow more tolerance here due to bfloat16 and noise
|
| 298 |
+
assert abs(pcc_ttnn - pcc_torch) < 0.05, f"TTNN vs PyTorch mismatch: {pcc_ttnn} vs {pcc_torch}"
|
| 299 |
+
assert abs(pcc_mixed_1 - pcc_torch) < 0.05, f"PCC (ttnn,torch) vs PyTorch mismatch: {pcc_mixed_1} vs {pcc_torch}"
|
| 300 |
+
assert abs(pcc_mixed_2 - pcc_torch) < 0.05, f"PCC (torch,ttnn) vs PyTorch mismatch: {pcc_mixed_2} vs {pcc_torch}"
|
| 301 |
+
|
| 302 |
+
# Test case 3: Negative correlation
|
| 303 |
+
torch.manual_seed(42)
|
| 304 |
+
a_torch = torch.randn(32, 32).bfloat16()
|
| 305 |
+
b_torch = -a_torch + torch.randn(32, 32).bfloat16() * 0.1
|
| 306 |
+
|
| 307 |
+
a_ttnn = ttnn.from_torch(a_torch, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 308 |
+
b_ttnn = ttnn.from_torch(b_torch, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 309 |
+
|
| 310 |
+
pcc_ttnn = compute_pcc(a_ttnn, b_ttnn)
|
| 311 |
+
a_q = _quantize_like_ttnn(a_torch, ttnn_mesh_device, dtype, layout)
|
| 312 |
+
b_q = _quantize_like_ttnn(b_torch, ttnn_mesh_device, dtype, layout)
|
| 313 |
+
pcc_torch = compute_pcc(a_q, b_q)
|
| 314 |
+
pcc_mixed_1 = compute_pcc(a_ttnn, b_q)
|
| 315 |
+
pcc_mixed_2 = compute_pcc(a_q, b_ttnn)
|
| 316 |
+
|
| 317 |
+
print(f" Negative correlation - TTNN: {pcc_ttnn:.6f}, PyTorch: {pcc_torch:.6f}")
|
| 318 |
+
assert pcc_ttnn < -0.8, f"Negative correlation should be < -0.8, got {pcc_ttnn}"
|
| 319 |
+
assert abs(pcc_ttnn - pcc_torch) < 0.1, f"TTNN vs PyTorch mismatch: {pcc_ttnn} vs {pcc_torch}"
|
| 320 |
+
assert abs(pcc_mixed_1 - pcc_torch) < 0.1, f"PCC (ttnn,torch) vs PyTorch mismatch: {pcc_mixed_1} vs {pcc_torch}"
|
| 321 |
+
assert abs(pcc_mixed_2 - pcc_torch) < 0.1, f"PCC (torch,ttnn) vs PyTorch mismatch: {pcc_mixed_2} vs {pcc_torch}"
|
| 322 |
+
|
| 323 |
+
# Test case 4: Larger tensor (128x256)
|
| 324 |
+
torch.manual_seed(42)
|
| 325 |
+
a_torch = torch.randn(128, 256).bfloat16()
|
| 326 |
+
b_torch = a_torch + torch.randn(128, 256).bfloat16() * 0.05
|
| 327 |
+
|
| 328 |
+
a_ttnn = ttnn.from_torch(a_torch, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 329 |
+
b_ttnn = ttnn.from_torch(b_torch, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 330 |
+
|
| 331 |
+
pcc_ttnn = compute_pcc(a_ttnn, b_ttnn)
|
| 332 |
+
a_q = _quantize_like_ttnn(a_torch, ttnn_mesh_device, dtype, layout)
|
| 333 |
+
b_q = _quantize_like_ttnn(b_torch, ttnn_mesh_device, dtype, layout)
|
| 334 |
+
pcc_torch = compute_pcc(a_q, b_q)
|
| 335 |
+
pcc_mixed_1 = compute_pcc(a_ttnn, b_q)
|
| 336 |
+
pcc_mixed_2 = compute_pcc(a_q, b_ttnn)
|
| 337 |
+
|
| 338 |
+
print(f" Large tensor - TTNN: {pcc_ttnn:.6f}, PyTorch: {pcc_torch:.6f}")
|
| 339 |
+
assert pcc_ttnn >= 0.90, f"Large tensor PCC should be >= 0.90, got {pcc_ttnn}"
|
| 340 |
+
assert abs(pcc_ttnn - pcc_torch) < 0.1, f"TTNN vs PyTorch mismatch: {pcc_ttnn} vs {pcc_torch}"
|
| 341 |
+
assert abs(pcc_mixed_1 - pcc_torch) < 0.1, f"PCC (ttnn,torch) vs PyTorch mismatch: {pcc_mixed_1} vs {pcc_torch}"
|
| 342 |
+
assert abs(pcc_mixed_2 - pcc_torch) < 0.1, f"PCC (torch,ttnn) vs PyTorch mismatch: {pcc_mixed_2} vs {pcc_torch}"
|
| 343 |
+
|
| 344 |
+
# Test case 5: Large noise - should produce low PCC (< 1.0)
|
| 345 |
+
torch.manual_seed(42)
|
| 346 |
+
a_torch = torch.randn(32, 32).bfloat16()
|
| 347 |
+
# Add significant noise (50% of signal strength)
|
| 348 |
+
b_torch = a_torch + torch.randn(32, 32).bfloat16() * 0.5
|
| 349 |
+
|
| 350 |
+
a_ttnn = ttnn.from_torch(a_torch, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 351 |
+
b_ttnn = ttnn.from_torch(b_torch, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 352 |
+
|
| 353 |
+
pcc_ttnn = compute_pcc(a_ttnn, b_ttnn)
|
| 354 |
+
a_q = _quantize_like_ttnn(a_torch, ttnn_mesh_device, dtype, layout)
|
| 355 |
+
b_q = _quantize_like_ttnn(b_torch, ttnn_mesh_device, dtype, layout)
|
| 356 |
+
pcc_torch = compute_pcc(a_q, b_q)
|
| 357 |
+
pcc_mixed_1 = compute_pcc(a_ttnn, b_q)
|
| 358 |
+
pcc_mixed_2 = compute_pcc(a_q, b_ttnn)
|
| 359 |
+
|
| 360 |
+
print(f" Large noise (0.5x) - TTNN: {pcc_ttnn:.6f}, PyTorch: {pcc_torch:.6f}")
|
| 361 |
+
assert pcc_ttnn < 0.99, f"Large noise should reduce PCC below 0.99, got {pcc_ttnn}"
|
| 362 |
+
assert pcc_ttnn > 0.50, f"PCC should still show some correlation (>0.50), got {pcc_ttnn}"
|
| 363 |
+
assert abs(pcc_ttnn - pcc_torch) < 0.1, f"TTNN vs PyTorch mismatch: {pcc_ttnn} vs {pcc_torch}"
|
| 364 |
+
assert abs(pcc_mixed_1 - pcc_torch) < 0.1, f"PCC (ttnn,torch) vs PyTorch mismatch: {pcc_mixed_1} vs {pcc_torch}"
|
| 365 |
+
assert abs(pcc_mixed_2 - pcc_torch) < 0.1, f"PCC (torch,ttnn) vs PyTorch mismatch: {pcc_mixed_2} vs {pcc_torch}"
|
| 366 |
+
|
| 367 |
+
# Test case 6: Very large noise - should produce very low PCC
|
| 368 |
+
torch.manual_seed(42)
|
| 369 |
+
a_torch = torch.randn(32, 32).bfloat16()
|
| 370 |
+
# Add massive noise (2x signal strength) - correlation should be weak
|
| 371 |
+
b_torch = a_torch + torch.randn(32, 32).bfloat16() * 2.0
|
| 372 |
+
|
| 373 |
+
a_ttnn = ttnn.from_torch(a_torch, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 374 |
+
b_ttnn = ttnn.from_torch(b_torch, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 375 |
+
|
| 376 |
+
pcc_ttnn = compute_pcc(a_ttnn, b_ttnn)
|
| 377 |
+
a_q = _quantize_like_ttnn(a_torch, ttnn_mesh_device, dtype, layout)
|
| 378 |
+
b_q = _quantize_like_ttnn(b_torch, ttnn_mesh_device, dtype, layout)
|
| 379 |
+
pcc_torch = compute_pcc(a_q, b_q)
|
| 380 |
+
pcc_mixed_1 = compute_pcc(a_ttnn, b_q)
|
| 381 |
+
pcc_mixed_2 = compute_pcc(a_q, b_ttnn)
|
| 382 |
+
|
| 383 |
+
print(f" Very large noise (2.0x) - TTNN: {pcc_ttnn:.6f}, PyTorch: {pcc_torch:.6f}")
|
| 384 |
+
assert pcc_ttnn < 0.80, f"Very large noise should reduce PCC below 0.80, got {pcc_ttnn}"
|
| 385 |
+
assert abs(pcc_ttnn - pcc_torch) < 0.15, f"TTNN vs PyTorch mismatch: {pcc_ttnn} vs {pcc_torch}"
|
| 386 |
+
assert abs(pcc_mixed_1 - pcc_torch) < 0.15, f"PCC (ttnn,torch) vs PyTorch mismatch: {pcc_mixed_1} vs {pcc_torch}"
|
| 387 |
+
assert abs(pcc_mixed_2 - pcc_torch) < 0.15, f"PCC (torch,ttnn) vs PyTorch mismatch: {pcc_mixed_2} vs {pcc_torch}"
|
| 388 |
+
|
| 389 |
+
print(" ✓ TTNN-native PCC correctly detects varying correlation strengths!")
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
def test_pcc_constant_tensors(ttnn_mesh_device, layout, dtype):
|
| 393 |
+
"""
|
| 394 |
+
Test that TTNN-native PCC correctly handles constant tensors.
|
| 395 |
+
|
| 396 |
+
Constant tensors are an edge case where variance is zero. The implementation
|
| 397 |
+
should detect this and return 1.0 if both constants are equal, 0.0 otherwise.
|
| 398 |
+
"""
|
| 399 |
+
print("\nTest: PCC with constant tensors")
|
| 400 |
+
|
| 401 |
+
# Test case 1: Same constant value
|
| 402 |
+
a_torch = torch.ones(32, 32).bfloat16() * 5.0
|
| 403 |
+
b_torch = torch.ones(32, 32).bfloat16() * 5.0
|
| 404 |
+
|
| 405 |
+
a_ttnn = ttnn.from_torch(a_torch, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 406 |
+
b_ttnn = ttnn.from_torch(b_torch, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 407 |
+
|
| 408 |
+
pcc_ttnn = compute_pcc(a_ttnn, b_ttnn)
|
| 409 |
+
a_q = _quantize_like_ttnn(a_torch, ttnn_mesh_device, dtype, layout)
|
| 410 |
+
b_q = _quantize_like_ttnn(b_torch, ttnn_mesh_device, dtype, layout)
|
| 411 |
+
pcc_torch = compute_pcc(a_q, b_q)
|
| 412 |
+
# Mixed-mode PCC computation
|
| 413 |
+
pcc_mixed_1 = compute_pcc(a_ttnn, b_q)
|
| 414 |
+
pcc_mixed_2 = compute_pcc(a_q, b_ttnn)
|
| 415 |
+
|
| 416 |
+
print(f" Same constant (5.0) - TTNN: {pcc_ttnn:.6f}, PyTorch: {pcc_torch:.6f}")
|
| 417 |
+
assert pcc_ttnn == 1.0, f"Same constant tensors should have PCC=1.0, got {pcc_ttnn}"
|
| 418 |
+
assert pcc_torch == 1.0, f"PyTorch should also return 1.0, got {pcc_torch}"
|
| 419 |
+
assert pcc_mixed_1 == pcc_torch, f"PCC (ttnn,torch) should equal PyTorch: {pcc_mixed_1} vs {pcc_torch}"
|
| 420 |
+
assert pcc_mixed_2 == pcc_torch, f"PCC (torch,ttnn) should equal PyTorch: {pcc_mixed_2} vs {pcc_torch}"
|
| 421 |
+
|
| 422 |
+
# Test case 2: Different constant values
|
| 423 |
+
a_torch = torch.ones(32, 32).bfloat16() * 5.0
|
| 424 |
+
b_torch = torch.ones(32, 32).bfloat16() * 3.0
|
| 425 |
+
|
| 426 |
+
a_ttnn = ttnn.from_torch(a_torch, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 427 |
+
b_ttnn = ttnn.from_torch(b_torch, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 428 |
+
|
| 429 |
+
pcc_ttnn = compute_pcc(a_ttnn, b_ttnn)
|
| 430 |
+
a_q = _quantize_like_ttnn(a_torch, ttnn_mesh_device, dtype, layout)
|
| 431 |
+
b_q = _quantize_like_ttnn(b_torch, ttnn_mesh_device, dtype, layout)
|
| 432 |
+
pcc_torch = compute_pcc(a_q, b_q)
|
| 433 |
+
pcc_mixed_1 = compute_pcc(a_ttnn, b_q)
|
| 434 |
+
pcc_mixed_2 = compute_pcc(a_q, b_ttnn)
|
| 435 |
+
|
| 436 |
+
print(f" Different constants (5.0 vs 3.0) - TTNN: {pcc_ttnn:.6f}, PyTorch: {pcc_torch:.6f}")
|
| 437 |
+
assert pcc_ttnn == 0.0, f"Different constant tensors should have PCC=0.0, got {pcc_ttnn}"
|
| 438 |
+
# PyTorch returns True/False which becomes 1.0/0.0
|
| 439 |
+
assert pcc_torch in [0.0, 1.0], f"PyTorch should return 0.0 or 1.0, got {pcc_torch}"
|
| 440 |
+
assert pcc_mixed_1 == pcc_torch, f"PCC (ttnn,torch) should equal PyTorch: {pcc_mixed_1} vs {pcc_torch}"
|
| 441 |
+
assert pcc_mixed_2 == pcc_torch, f"PCC (torch,ttnn) should equal PyTorch: {pcc_mixed_2} vs {pcc_torch}"
|
| 442 |
+
|
| 443 |
+
# Test case 3: All zeros
|
| 444 |
+
a_torch = torch.zeros(32, 32).bfloat16()
|
| 445 |
+
b_torch = torch.zeros(32, 32).bfloat16()
|
| 446 |
+
|
| 447 |
+
a_ttnn = ttnn.from_torch(a_torch, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 448 |
+
b_ttnn = ttnn.from_torch(b_torch, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 449 |
+
|
| 450 |
+
pcc_ttnn = compute_pcc(a_ttnn, b_ttnn)
|
| 451 |
+
a_q = _quantize_like_ttnn(a_torch, ttnn_mesh_device, dtype, layout)
|
| 452 |
+
b_q = _quantize_like_ttnn(b_torch, ttnn_mesh_device, dtype, layout)
|
| 453 |
+
pcc_torch = compute_pcc(a_q, b_q)
|
| 454 |
+
pcc_mixed_1 = compute_pcc(a_ttnn, b_q)
|
| 455 |
+
pcc_mixed_2 = compute_pcc(a_q, b_ttnn)
|
| 456 |
+
|
| 457 |
+
print(f" All zeros - TTNN: {pcc_ttnn:.6f}, PyTorch: {pcc_torch:.6f}")
|
| 458 |
+
assert pcc_ttnn == 1.0, f"Zero tensors should have PCC=1.0, got {pcc_ttnn}"
|
| 459 |
+
assert pcc_torch == 1.0, f"PyTorch should also return 1.0, got {pcc_torch}"
|
| 460 |
+
assert pcc_mixed_1 == pcc_torch, f"PCC (ttnn,torch) should equal PyTorch: {pcc_mixed_1} vs {pcc_torch}"
|
| 461 |
+
assert pcc_mixed_2 == pcc_torch, f"PCC (torch,ttnn) should equal PyTorch: {pcc_mixed_2} vs {pcc_torch}"
|
| 462 |
+
|
| 463 |
+
print(" ✓ TTNN-native PCC correctly handles constant tensors!")
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
def test_pcc_all_nan_and_mixed_nan(ttnn_mesh_device, layout, dtype):
|
| 467 |
+
"""
|
| 468 |
+
TTNN-native PCC should mirror CPU semantics for NaN cases:
|
| 469 |
+
- both all-NaN -> 1.0
|
| 470 |
+
- mixed NaN presence -> 0.0
|
| 471 |
+
"""
|
| 472 |
+
print("\nTest: PCC NaN edge cases")
|
| 473 |
+
|
| 474 |
+
a_nan = torch.full((32, 32), float("nan"), dtype=torch.float32)
|
| 475 |
+
b_nan = torch.full((32, 32), float("nan"), dtype=torch.float32)
|
| 476 |
+
a_num = torch.zeros(32, 32, dtype=torch.float32)
|
| 477 |
+
|
| 478 |
+
a_nan_t = ttnn.from_torch(a_nan, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 479 |
+
b_nan_t = ttnn.from_torch(b_nan, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 480 |
+
a_num_t = ttnn.from_torch(a_num, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 481 |
+
|
| 482 |
+
# Both all-NaN -> 1.0
|
| 483 |
+
pcc_ttnn = compute_pcc(a_nan_t, b_nan_t)
|
| 484 |
+
a_nan_q = _quantize_like_ttnn(a_nan, ttnn_mesh_device, dtype, layout)
|
| 485 |
+
b_nan_q = _quantize_like_ttnn(b_nan, ttnn_mesh_device, dtype, layout)
|
| 486 |
+
pcc_cpu = compute_pcc(a_nan_q, b_nan_q)
|
| 487 |
+
# Mixed-mode PCC computation
|
| 488 |
+
pcc_mixed_1 = compute_pcc(a_nan_t, b_nan_q)
|
| 489 |
+
pcc_mixed_2 = compute_pcc(a_nan_q, b_nan_t)
|
| 490 |
+
print(f" both NaN - TTNN: {pcc_ttnn}, CPU: {pcc_cpu}")
|
| 491 |
+
assert pcc_ttnn == 1.0
|
| 492 |
+
assert pcc_cpu == 1.0
|
| 493 |
+
assert pcc_mixed_1 == 1.0
|
| 494 |
+
assert pcc_mixed_2 == 1.0
|
| 495 |
+
|
| 496 |
+
# Mixed NaN presence -> 0.0
|
| 497 |
+
pcc_ttnn = compute_pcc(a_nan_t, a_num_t)
|
| 498 |
+
a_num_q = _quantize_like_ttnn(a_num, ttnn_mesh_device, dtype, layout)
|
| 499 |
+
pcc_cpu = compute_pcc(a_nan_q, a_num_q)
|
| 500 |
+
# Mixed-mode PCC computation
|
| 501 |
+
pcc_mixed_1 = compute_pcc(a_nan_t, a_num_q)
|
| 502 |
+
pcc_mixed_2 = compute_pcc(a_nan_q, a_num_t)
|
| 503 |
+
print(f" mixed NaN - TTNN: {pcc_ttnn}, CPU: {pcc_cpu}")
|
| 504 |
+
assert pcc_ttnn == 0.0
|
| 505 |
+
assert pcc_cpu == 0.0
|
| 506 |
+
assert pcc_mixed_1 == 0.0
|
| 507 |
+
assert pcc_mixed_2 == 0.0
|
| 508 |
+
|
| 509 |
+
print(" ✓ TTNN-native PCC correctly handles NaN cases!")
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
def test_pcc_zero_vs_nonzero(ttnn_mesh_device, layout, dtype):
|
| 513 |
+
"""
|
| 514 |
+
One tensor all-zero and the other non-zero -> PCC = 0.0 on both TTNN and CPU.
|
| 515 |
+
"""
|
| 516 |
+
print("\nTest: PCC zero vs non-zero")
|
| 517 |
+
|
| 518 |
+
a_zero = torch.zeros(32, 32, dtype=torch.float32)
|
| 519 |
+
b_nonzero = torch.ones(32, 32, dtype=torch.float32)
|
| 520 |
+
|
| 521 |
+
a_zero_t = ttnn.from_torch(a_zero, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 522 |
+
b_nonzero_t = ttnn.from_torch(b_nonzero, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 523 |
+
|
| 524 |
+
pcc_ttnn = compute_pcc(a_zero_t, b_nonzero_t)
|
| 525 |
+
a_zero_q = _quantize_like_ttnn(a_zero, ttnn_mesh_device, dtype, layout)
|
| 526 |
+
b_nonzero_q = _quantize_like_ttnn(b_nonzero, ttnn_mesh_device, dtype, layout)
|
| 527 |
+
pcc_cpu = compute_pcc(a_zero_q, b_nonzero_q)
|
| 528 |
+
# Mixed-mode PCC computation
|
| 529 |
+
pcc_mixed_1 = compute_pcc(a_zero_t, b_nonzero_q)
|
| 530 |
+
pcc_mixed_2 = compute_pcc(a_zero_q, b_nonzero_t)
|
| 531 |
+
print(f" zero vs non-zero - TTNN: {pcc_ttnn}, CPU: {pcc_cpu}")
|
| 532 |
+
assert pcc_ttnn == 0.0
|
| 533 |
+
assert pcc_cpu == 0.0
|
| 534 |
+
assert pcc_mixed_1 == 0.0
|
| 535 |
+
assert pcc_mixed_2 == 0.0
|
| 536 |
+
print(" ✓ TTNN-native PCC correctly handles zero vs non-zero!")
|
| 537 |
+
|
| 538 |
+
|
| 539 |
+
def test_comp_allclose_ttnn_native(ttnn_mesh_device, layout, dtype):
|
| 540 |
+
"""TTNN-native tests for comp_allclose using on-device ops."""
|
| 541 |
+
|
| 542 |
+
# Exact equality
|
| 543 |
+
a_t = ttnn.from_torch(torch.randn(2, 4, dtype=torch.bfloat16), device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 544 |
+
b_t = a_t
|
| 545 |
+
passed, msg = comp_allclose(a_t, b_t)
|
| 546 |
+
assert passed, f"TTNN equality should pass. Got: {msg}"
|
| 547 |
+
|
| 548 |
+
# Fail with tight tolerance
|
| 549 |
+
a = torch.tensor([[1.0, 2.0], [3.0, 4.0]], dtype=torch.bfloat16)
|
| 550 |
+
b = a + 0.5
|
| 551 |
+
a_t = ttnn.from_torch(a, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 552 |
+
b_t = ttnn.from_torch(b, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 553 |
+
passed, msg = comp_allclose(a_t, b_t, rtol=1e-6, atol=1e-6)
|
| 554 |
+
assert not passed and "Allclose check failed" in msg
|
| 555 |
+
|
| 556 |
+
# Pass with relaxed tolerance
|
| 557 |
+
passed, msg = comp_allclose(a_t, b_t, rtol=0.2, atol=0.6)
|
| 558 |
+
assert passed, f"Expected pass with relaxed tolerance. Got: {msg}"
|
| 559 |
+
|
| 560 |
+
# NaN equal
|
| 561 |
+
a = torch.tensor([float("nan"), 1.0], dtype=torch.float32)
|
| 562 |
+
b = torch.tensor([float("nan"), 1.0], dtype=torch.float32)
|
| 563 |
+
a_t = ttnn.from_torch(a, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 564 |
+
b_t = ttnn.from_torch(b, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 565 |
+
passed, _ = comp_allclose(a_t, b_t)
|
| 566 |
+
assert passed, "TTNN: Both NaNs at same positions should pass"
|
| 567 |
+
|
| 568 |
+
# Inf same sign pass, different sign fail
|
| 569 |
+
a = torch.tensor([float("inf"), -float("inf"), 2.0], dtype=torch.float32)
|
| 570 |
+
b = torch.tensor([float("inf"), -float("inf"), 2.0], dtype=torch.float32)
|
| 571 |
+
a_t = ttnn.from_torch(a, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 572 |
+
b_t = ttnn.from_torch(b, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 573 |
+
passed, _ = comp_allclose(a_t, b_t)
|
| 574 |
+
assert passed
|
| 575 |
+
|
| 576 |
+
b = torch.tensor([float("inf"), float("inf"), 2.0], dtype=torch.float32)
|
| 577 |
+
b_t = ttnn.from_torch(b, device=ttnn_mesh_device, dtype=dtype, layout=layout)
|
| 578 |
+
passed, msg = comp_allclose(a_t, b_t)
|
| 579 |
+
assert not passed and "Allclose check failed" in msg
|
| 580 |
+
|
| 581 |
+
|
| 582 |
+
if __name__ == "__main__":
|
| 583 |
+
# Run with pytest for better output
|
| 584 |
+
pytest.main([__file__, "-v", "--tb=short"])
|
code/models/common/tests/test_module_input_validation.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from types import SimpleNamespace
|
| 5 |
+
|
| 6 |
+
from models.common.models import module_input_validation as validation
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class FakeTensor:
|
| 10 |
+
def __init__(self, memory_config="dram"):
|
| 11 |
+
self._memory_config = memory_config
|
| 12 |
+
self.memory_config_calls = 0
|
| 13 |
+
|
| 14 |
+
def is_allocated(self):
|
| 15 |
+
return True
|
| 16 |
+
|
| 17 |
+
def memory_config(self):
|
| 18 |
+
self.memory_config_calls += 1
|
| 19 |
+
return self._memory_config
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class FakeConfig:
|
| 23 |
+
prefill_input_memcfg = "dram"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class FakeModule:
|
| 27 |
+
config = FakeConfig()
|
| 28 |
+
|
| 29 |
+
def prefill_forward(self, x):
|
| 30 |
+
return x
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class FakeModel:
|
| 34 |
+
def __init__(self):
|
| 35 |
+
self.module = FakeModule()
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def iter_named_modules(model):
|
| 39 |
+
yield "module", model.module
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def test_validate_module_input_configs_reads_memory_config_when_active(monkeypatch):
|
| 43 |
+
monkeypatch.setattr(validation, "ttnn", SimpleNamespace(Tensor=FakeTensor))
|
| 44 |
+
model = FakeModel()
|
| 45 |
+
tensor = FakeTensor()
|
| 46 |
+
|
| 47 |
+
with validation.validate_module_input_configs(
|
| 48 |
+
model=model,
|
| 49 |
+
iter_named_modules=iter_named_modules,
|
| 50 |
+
mode="prefill",
|
| 51 |
+
):
|
| 52 |
+
assert model.module.prefill_forward(tensor) is tensor
|
| 53 |
+
|
| 54 |
+
assert tensor.memory_config_calls == 1
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def test_suspend_module_input_validation_bypasses_memory_config(monkeypatch):
|
| 58 |
+
monkeypatch.setattr(validation, "ttnn", SimpleNamespace(Tensor=FakeTensor))
|
| 59 |
+
model = FakeModel()
|
| 60 |
+
tensor = FakeTensor()
|
| 61 |
+
|
| 62 |
+
with validation.validate_module_input_configs(
|
| 63 |
+
model=model,
|
| 64 |
+
iter_named_modules=iter_named_modules,
|
| 65 |
+
mode="prefill",
|
| 66 |
+
):
|
| 67 |
+
with validation.suspend_module_input_validation():
|
| 68 |
+
assert model.module.prefill_forward(tensor) is tensor
|
| 69 |
+
|
| 70 |
+
assert tensor.memory_config_calls == 0
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def test_suspend_module_input_validation_restores_after_nested_contexts(monkeypatch):
|
| 74 |
+
monkeypatch.setattr(validation, "ttnn", SimpleNamespace(Tensor=FakeTensor))
|
| 75 |
+
model = FakeModel()
|
| 76 |
+
tensor = FakeTensor()
|
| 77 |
+
|
| 78 |
+
with validation.validate_module_input_configs(
|
| 79 |
+
model=model,
|
| 80 |
+
iter_named_modules=iter_named_modules,
|
| 81 |
+
mode="prefill",
|
| 82 |
+
):
|
| 83 |
+
with validation.suspend_module_input_validation():
|
| 84 |
+
with validation.suspend_module_input_validation():
|
| 85 |
+
model.module.prefill_forward(tensor)
|
| 86 |
+
model.module.prefill_forward(tensor)
|
| 87 |
+
model.module.prefill_forward(tensor)
|
| 88 |
+
|
| 89 |
+
model.module.prefill_forward(tensor)
|
| 90 |
+
|
| 91 |
+
assert tensor.memory_config_calls == 1
|
code/models/common/tests/test_sampling.py
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from types import SimpleNamespace
|
| 5 |
+
|
| 6 |
+
import pytest
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
|
| 10 |
+
import ttnn
|
| 11 |
+
from models.common.sampling import (
|
| 12 |
+
LogProbsCalculator,
|
| 13 |
+
SamplingParams,
|
| 14 |
+
SeedManager,
|
| 15 |
+
broadcast_sampling_params,
|
| 16 |
+
format_sampling_params,
|
| 17 |
+
)
|
| 18 |
+
from models.common.sampling.tt_log_probs import MAX_TOP_LOGPROBS, LogProbsResult
|
| 19 |
+
from models.common.utility_functions import comp_pcc
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# ---------------------------------------------------------------------------
|
| 23 |
+
# Helper: simulate per-device top-k gather (mirrors TTSampling behaviour)
|
| 24 |
+
# ---------------------------------------------------------------------------
|
| 25 |
+
def _simulate_gathered_topk(torch_logits, num_devices, top_k=32):
|
| 26 |
+
"""Simulate the per-device top-k + all-gather that TTSampling performs.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
torch_logits: Full logits tensor, shape (1, 1, B, V).
|
| 30 |
+
num_devices: Number of TP devices.
|
| 31 |
+
top_k: Per-device top-k count.
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
gathered_values: (1, 1, B, num_devices * top_k) raw logit values.
|
| 35 |
+
gathered_indices: (1, 1, B, num_devices * top_k) global vocab indices.
|
| 36 |
+
"""
|
| 37 |
+
V = torch_logits.shape[-1]
|
| 38 |
+
shard_size = V // num_devices
|
| 39 |
+
all_values = []
|
| 40 |
+
all_indices = []
|
| 41 |
+
for d in range(num_devices):
|
| 42 |
+
shard = torch_logits[:, :, :, d * shard_size : (d + 1) * shard_size]
|
| 43 |
+
vals, local_idx = torch.topk(shard, top_k, dim=-1)
|
| 44 |
+
global_idx = local_idx + d * shard_size
|
| 45 |
+
all_values.append(vals)
|
| 46 |
+
all_indices.append(global_idx)
|
| 47 |
+
gathered_values = torch.cat(all_values, dim=-1)
|
| 48 |
+
gathered_indices = torch.cat(all_indices, dim=-1)
|
| 49 |
+
return gathered_values, gathered_indices
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# ===========================================================================
|
| 53 |
+
# Top-K logprobs tests (TG Galaxy only)
|
| 54 |
+
# ===========================================================================
|
| 55 |
+
|
| 56 |
+
# Common TG Galaxy device parametrization for all new tests
|
| 57 |
+
TG_SHAPE = [1, 1, 32, 8 * 16032] # Llama on TG with 8-chip TP sharded vocab
|
| 58 |
+
TG_DEVICE_PARAMS = {
|
| 59 |
+
"fabric_config": ttnn.FabricConfig.FABRIC_1D_RING,
|
| 60 |
+
"dispatch_core_axis": ttnn.DispatchCoreAxis.COL,
|
| 61 |
+
}
|
| 62 |
+
TG_MESH_SHAPE = (8, 4)
|
| 63 |
+
TG_SUB_CORE_GRIDS = ttnn.CoreRangeSet(
|
| 64 |
+
[
|
| 65 |
+
ttnn.CoreRange(ttnn.CoreCoord(1, 0), ttnn.CoreCoord(3, 9)),
|
| 66 |
+
ttnn.CoreRange(ttnn.CoreCoord(5, 0), ttnn.CoreCoord(6, 9)),
|
| 67 |
+
]
|
| 68 |
+
)
|
| 69 |
+
TG_NUM_TP_DEVICES = 8 # TP dimension for Galaxy
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _make_host_only_seed_manager(max_batch_size=4):
|
| 73 |
+
return SeedManager(SimpleNamespace(_sampling_dp=1), max_batch_size=max_batch_size)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def test_seed_manager_seed_params_do_not_fallback_to_slot_zero():
|
| 77 |
+
seed_manager = _make_host_only_seed_manager()
|
| 78 |
+
|
| 79 |
+
assert seed_manager._seed_from_slot_params([11], 0) == 11
|
| 80 |
+
assert seed_manager._seed_from_slot_params([11], 1) is None
|
| 81 |
+
assert seed_manager._seed_from_slot_params(torch.tensor([22]), 1) is None
|
| 82 |
+
assert seed_manager._seed_from_slot_params(33, 3) == 33
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def test_seed_counter_position_alignment_skips_out_of_bounds_slots():
|
| 86 |
+
seed_manager = _make_host_only_seed_manager()
|
| 87 |
+
|
| 88 |
+
seed_manager.align_seed_counters_to_positions([101, None, 303], [0, 2], [5], offset=1)
|
| 89 |
+
|
| 90 |
+
assert seed_manager.seed_counters == [6, 0, 0, 0]
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def test_broadcast_sampling_params_preserves_none_list_fields():
|
| 94 |
+
params = SamplingParams(temperature=[1.0, 1.0], top_k=[1, 1], top_p=[1.0, 1.0], seed=[None, 42])
|
| 95 |
+
|
| 96 |
+
broadcast = broadcast_sampling_params(params, 0, slot_len=4)
|
| 97 |
+
|
| 98 |
+
assert broadcast.seed == [None, None, None, None]
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def test_format_sampling_params_uses_device_argmax_sentinel_for_greedy_rows():
|
| 102 |
+
params = format_sampling_params(
|
| 103 |
+
SamplingParams(temperature=0.0, top_k=32, top_p=0.95),
|
| 104 |
+
max_batch_size=32,
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
assert params.temperature[0] == 1.0
|
| 108 |
+
assert params.top_k[0] == 1
|
| 109 |
+
assert params.top_p[0] == 0.0
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _skip_if_not_galaxy(mesh_device):
|
| 113 |
+
"""Skip test if not running on TG Galaxy (32 devices)."""
|
| 114 |
+
if mesh_device.get_num_devices() != 32:
|
| 115 |
+
pytest.skip(f"Test requires TG Galaxy (32 devices), got {mesh_device.get_num_devices()}")
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def _push_topk_test_tensors_to_tg(torch_tensor, gathered_values, gathered_indices, mesh_device):
|
| 119 |
+
"""Push logits, topk values, and topk indices to a TG Galaxy mesh device."""
|
| 120 |
+
logits_tt = ttnn.from_torch(
|
| 121 |
+
torch_tensor,
|
| 122 |
+
device=mesh_device,
|
| 123 |
+
dtype=ttnn.bfloat16,
|
| 124 |
+
layout=ttnn.TILE_LAYOUT,
|
| 125 |
+
mesh_mapper=ttnn.ShardTensor2dMesh(mesh_device, dims=(-1, None), mesh_shape=list(mesh_device.shape)),
|
| 126 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 127 |
+
)
|
| 128 |
+
topk_values_tt = ttnn.from_torch(
|
| 129 |
+
gathered_values,
|
| 130 |
+
device=mesh_device,
|
| 131 |
+
dtype=ttnn.bfloat16,
|
| 132 |
+
layout=ttnn.TILE_LAYOUT,
|
| 133 |
+
mesh_mapper=ttnn.ReplicateTensorToMesh(mesh_device),
|
| 134 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 135 |
+
)
|
| 136 |
+
topk_indices_tt = ttnn.from_torch(
|
| 137 |
+
gathered_indices.to(torch.int32),
|
| 138 |
+
device=mesh_device,
|
| 139 |
+
dtype=ttnn.int32,
|
| 140 |
+
layout=ttnn.TILE_LAYOUT,
|
| 141 |
+
mesh_mapper=ttnn.ReplicateTensorToMesh(mesh_device),
|
| 142 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 143 |
+
)
|
| 144 |
+
return logits_tt, topk_values_tt, topk_indices_tt
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
@pytest.mark.parametrize(
|
| 148 |
+
"shape",
|
| 149 |
+
[
|
| 150 |
+
[1, 1, 32, 8 * 18992], # Qwen3 on T3K
|
| 151 |
+
],
|
| 152 |
+
)
|
| 153 |
+
@pytest.mark.parametrize(
|
| 154 |
+
"device_params",
|
| 155 |
+
[
|
| 156 |
+
({"fabric_config": ttnn.FabricConfig.FABRIC_1D}),
|
| 157 |
+
],
|
| 158 |
+
indirect=["device_params"],
|
| 159 |
+
ids=["fabric_linear"],
|
| 160 |
+
)
|
| 161 |
+
def test_log_probs_calculation(shape, mesh_device):
|
| 162 |
+
seed = 1234
|
| 163 |
+
torch.manual_seed(seed)
|
| 164 |
+
|
| 165 |
+
log_probs_calculator = LogProbsCalculator(mesh_device)
|
| 166 |
+
|
| 167 |
+
torch_tensor = torch.randn(shape)
|
| 168 |
+
# shuffle the tensor in last 2 dimensions
|
| 169 |
+
for i in range(shape[-2]):
|
| 170 |
+
torch_tensor[:, :, i, :] = torch_tensor[:, :, i, torch.randperm(shape[-1])]
|
| 171 |
+
|
| 172 |
+
argmax_tensor = torch.argmax(torch_tensor, dim=-1, keepdim=True)
|
| 173 |
+
indices_tensor = argmax_tensor.reshape(
|
| 174 |
+
argmax_tensor.shape[0], argmax_tensor.shape[1], argmax_tensor.shape[-1], argmax_tensor.shape[-2]
|
| 175 |
+
)
|
| 176 |
+
# Push inputs to device
|
| 177 |
+
logits_tensor = ttnn.from_torch(
|
| 178 |
+
torch_tensor,
|
| 179 |
+
device=mesh_device,
|
| 180 |
+
dtype=ttnn.bfloat16,
|
| 181 |
+
layout=ttnn.TILE_LAYOUT,
|
| 182 |
+
mesh_mapper=ttnn.ShardTensorToMesh(mesh_device, dim=-1),
|
| 183 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
ttnn_indices_tensor = ttnn.from_torch(
|
| 187 |
+
indices_tensor,
|
| 188 |
+
device=mesh_device,
|
| 189 |
+
dtype=ttnn.int32,
|
| 190 |
+
layout=ttnn.TILE_LAYOUT,
|
| 191 |
+
mesh_mapper=ttnn.ReplicateTensorToMesh(mesh_device),
|
| 192 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
log_probs_calculator.set_log_probs_mode(True)
|
| 196 |
+
tt_log_probs = log_probs_calculator.calculate_log_probs(logits_tensor, ttnn_indices_tensor)
|
| 197 |
+
log_probs_tt_host = ttnn.to_torch(tt_log_probs, mesh_composer=ttnn.ConcatMeshToTensor(mesh_device, dim=3))
|
| 198 |
+
log_probs_tt_host = log_probs_tt_host[:, :, :1, :32]
|
| 199 |
+
|
| 200 |
+
# Calculate log-probs for each user on each chip using torch
|
| 201 |
+
log_probs_torch = F.log_softmax(torch_tensor.float(), dim=-1)
|
| 202 |
+
log_probs_torch_argmax = torch.gather(log_probs_torch, dim=-1, index=argmax_tensor)
|
| 203 |
+
log_probs_torch_argmax = torch.reshape(log_probs_torch_argmax, (1, 1, 1, 32))
|
| 204 |
+
|
| 205 |
+
passing, pcc = comp_pcc(log_probs_torch_argmax, log_probs_tt_host, pcc=0.99)
|
| 206 |
+
print(f"pcc={pcc}")
|
| 207 |
+
|
| 208 |
+
assert passing, f"Assertion failed, PCC={pcc}"
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
@pytest.mark.parametrize(
|
| 212 |
+
"shape",
|
| 213 |
+
[
|
| 214 |
+
[1, 1, 32, 8 * 18992], # Qwen3 on T3K
|
| 215 |
+
],
|
| 216 |
+
)
|
| 217 |
+
@pytest.mark.parametrize(
|
| 218 |
+
"device_params",
|
| 219 |
+
[
|
| 220 |
+
({"fabric_config": ttnn.FabricConfig.FABRIC_1D}),
|
| 221 |
+
],
|
| 222 |
+
indirect=["device_params"],
|
| 223 |
+
ids=["fabric_linear"],
|
| 224 |
+
)
|
| 225 |
+
def test_log_probs_returns_none_when_disabled(shape, mesh_device):
|
| 226 |
+
"""Test that calculate_log_probs returns None when enable_log_probs is False."""
|
| 227 |
+
log_probs_calculator = LogProbsCalculator(mesh_device)
|
| 228 |
+
|
| 229 |
+
torch_tensor = torch.randn(shape)
|
| 230 |
+
argmax_tensor = torch.argmax(torch_tensor, dim=-1, keepdim=True)
|
| 231 |
+
indices_tensor = argmax_tensor.reshape(
|
| 232 |
+
argmax_tensor.shape[0], argmax_tensor.shape[1], argmax_tensor.shape[-1], argmax_tensor.shape[-2]
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
logits_tensor = ttnn.from_torch(
|
| 236 |
+
torch_tensor,
|
| 237 |
+
device=mesh_device,
|
| 238 |
+
dtype=ttnn.bfloat16,
|
| 239 |
+
layout=ttnn.TILE_LAYOUT,
|
| 240 |
+
mesh_mapper=ttnn.ShardTensorToMesh(mesh_device, dim=-1),
|
| 241 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 242 |
+
)
|
| 243 |
+
ttnn_indices_tensor = ttnn.from_torch(
|
| 244 |
+
indices_tensor,
|
| 245 |
+
device=mesh_device,
|
| 246 |
+
dtype=ttnn.int32,
|
| 247 |
+
layout=ttnn.TILE_LAYOUT,
|
| 248 |
+
mesh_mapper=ttnn.ReplicateTensorToMesh(mesh_device),
|
| 249 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
# Log probs disabled (default) - should return None
|
| 253 |
+
log_probs_calculator.set_log_probs_mode(False)
|
| 254 |
+
result = log_probs_calculator.calculate_log_probs(logits_tensor, ttnn_indices_tensor)
|
| 255 |
+
assert result is None, f"Expected None when log_probs disabled, got {type(result)}"
|
| 256 |
+
|
| 257 |
+
# Log probs enabled - should return a tensor (not None)
|
| 258 |
+
log_probs_calculator.set_log_probs_mode(True)
|
| 259 |
+
num_devices = mesh_device.get_num_devices()
|
| 260 |
+
result = log_probs_calculator.calculate_log_probs(logits_tensor, ttnn_indices_tensor)
|
| 261 |
+
if num_devices in (8, 32) and log_probs_calculator.num_devices_for_sharding >= 2:
|
| 262 |
+
assert result is not None, "Expected tensor when log_probs enabled on supported device"
|
| 263 |
+
else:
|
| 264 |
+
assert result is None, "Expected None on unsupported device count"
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
@pytest.mark.parametrize(
|
| 268 |
+
"shape",
|
| 269 |
+
[
|
| 270 |
+
[1, 1, 32, 8 * 16032], # llama on TG with 8 chips sharded vocab
|
| 271 |
+
],
|
| 272 |
+
)
|
| 273 |
+
@pytest.mark.parametrize(
|
| 274 |
+
"device_params",
|
| 275 |
+
[
|
| 276 |
+
(
|
| 277 |
+
{
|
| 278 |
+
"fabric_config": ttnn.FabricConfig.FABRIC_1D_RING,
|
| 279 |
+
"dispatch_core_axis": ttnn.DispatchCoreAxis.COL,
|
| 280 |
+
}
|
| 281 |
+
),
|
| 282 |
+
],
|
| 283 |
+
indirect=True,
|
| 284 |
+
ids=["fabric_linear"],
|
| 285 |
+
)
|
| 286 |
+
@pytest.mark.parametrize(
|
| 287 |
+
"mesh_device",
|
| 288 |
+
[
|
| 289 |
+
(8, 4),
|
| 290 |
+
],
|
| 291 |
+
indirect=True,
|
| 292 |
+
)
|
| 293 |
+
def test_log_probs_with_sub_core_grids_on_galaxy(shape, mesh_device):
|
| 294 |
+
seed = 1234
|
| 295 |
+
torch.manual_seed(seed)
|
| 296 |
+
|
| 297 |
+
sub_core_grids = ttnn.CoreRangeSet(
|
| 298 |
+
[
|
| 299 |
+
ttnn.CoreRange(ttnn.CoreCoord(1, 0), ttnn.CoreCoord(3, 9)),
|
| 300 |
+
ttnn.CoreRange(ttnn.CoreCoord(5, 0), ttnn.CoreCoord(6, 9)),
|
| 301 |
+
]
|
| 302 |
+
)
|
| 303 |
+
log_probs_calculator = LogProbsCalculator(mesh_device, sub_core_grids)
|
| 304 |
+
|
| 305 |
+
torch_tensor = torch.randn(shape)
|
| 306 |
+
# shuffle the tensor in last 2 dimensions
|
| 307 |
+
for i in range(shape[-2]):
|
| 308 |
+
torch_tensor[:, :, i, :] = torch_tensor[:, :, i, torch.randperm(shape[-1])]
|
| 309 |
+
|
| 310 |
+
argmax_tensor = torch.argmax(torch_tensor, dim=-1, keepdim=True)
|
| 311 |
+
indices_tensor = argmax_tensor.reshape(
|
| 312 |
+
argmax_tensor.shape[0], argmax_tensor.shape[1], argmax_tensor.shape[-1], argmax_tensor.shape[-2]
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
if mesh_device.get_num_devices() == 8:
|
| 316 |
+
mesh_mapper = ttnn.ShardTensorToMesh(mesh_device, dim=-1)
|
| 317 |
+
elif mesh_device.get_num_devices() == 32:
|
| 318 |
+
mesh_mapper = ttnn.ShardTensor2dMesh(mesh_device, dims=(-1, None), mesh_shape=list(mesh_device.shape))
|
| 319 |
+
else:
|
| 320 |
+
raise ValueError(f"Unsupported number of devices: {mesh_device.get_num_devices()}")
|
| 321 |
+
|
| 322 |
+
logits_tensor = ttnn.from_torch(
|
| 323 |
+
torch_tensor,
|
| 324 |
+
device=mesh_device,
|
| 325 |
+
dtype=ttnn.bfloat16,
|
| 326 |
+
layout=ttnn.TILE_LAYOUT,
|
| 327 |
+
mesh_mapper=mesh_mapper,
|
| 328 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
ttnn_indices_tensor = ttnn.from_torch(
|
| 332 |
+
indices_tensor,
|
| 333 |
+
device=mesh_device,
|
| 334 |
+
dtype=ttnn.int32,
|
| 335 |
+
layout=ttnn.TILE_LAYOUT,
|
| 336 |
+
mesh_mapper=ttnn.ReplicateTensorToMesh(mesh_device),
|
| 337 |
+
memory_config=ttnn.DRAM_MEMORY_CONFIG,
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
log_probs_calculator.set_log_probs_mode(True)
|
| 341 |
+
tt_log_probs = log_probs_calculator.calculate_log_probs(logits_tensor, ttnn_indices_tensor)
|
| 342 |
+
log_probs_tt_host = ttnn.to_torch(tt_log_probs, mesh_composer=ttnn.ConcatMeshToTensor(mesh_device, dim=3))
|
| 343 |
+
# slice from (1,1,32,256) -> (1,1,1,32)
|
| 344 |
+
log_probs_tt_host = log_probs_tt_host[:, :, :1, :32]
|
| 345 |
+
|
| 346 |
+
log_probs_torch = F.log_softmax(torch_tensor.float(), dim=-1)
|
| 347 |
+
log_probs_torch_argmax = torch.gather(log_probs_torch, dim=-1, index=argmax_tensor)
|
| 348 |
+
log_probs_torch_argmax = torch.reshape(log_probs_torch_argmax, (1, 1, 1, 32))
|
| 349 |
+
|
| 350 |
+
passing, pcc = comp_pcc(log_probs_torch_argmax, log_probs_tt_host, pcc=0.99)
|
| 351 |
+
print(f"pcc={pcc}")
|
| 352 |
+
|
| 353 |
+
assert passing, f"Assertion failed, PCC={pcc}"
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
# ===========================================================================
|
| 357 |
+
# New top-K logprobs tests (TG Galaxy only)
|
| 358 |
+
# ===========================================================================
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
@pytest.mark.parametrize("shape", [TG_SHAPE])
|
| 362 |
+
@pytest.mark.parametrize("device_params", [TG_DEVICE_PARAMS], indirect=True, ids=["tg"])
|
| 363 |
+
@pytest.mark.parametrize("mesh_device", [TG_MESH_SHAPE], indirect=True)
|
| 364 |
+
def test_top_k_log_probs_on_galaxy(shape, mesh_device):
|
| 365 |
+
"""Top-K logprobs PCC check on TG Galaxy (32-device 2D mesh)."""
|
| 366 |
+
_skip_if_not_galaxy(mesh_device)
|
| 367 |
+
torch.manual_seed(1234)
|
| 368 |
+
batch_size = shape[2]
|
| 369 |
+
|
| 370 |
+
calc = LogProbsCalculator(mesh_device, TG_SUB_CORE_GRIDS, batch_size=batch_size, use_topk_logprobs=True)
|
| 371 |
+
|
| 372 |
+
torch_tensor = torch.randn(shape)
|
| 373 |
+
for i in range(batch_size):
|
| 374 |
+
torch_tensor[:, :, i, :] = torch_tensor[:, :, i, torch.randperm(shape[-1])]
|
| 375 |
+
|
| 376 |
+
log_probs_torch = F.log_softmax(torch_tensor.to(torch.float16), dim=-1)
|
| 377 |
+
gathered_values, gathered_indices = _simulate_gathered_topk(torch_tensor, TG_NUM_TP_DEVICES)
|
| 378 |
+
argmax_tensor = torch.argmax(torch_tensor, dim=-1, keepdim=True)
|
| 379 |
+
|
| 380 |
+
logits_tt, topk_values_tt, topk_indices_tt = _push_topk_test_tensors_to_tg(
|
| 381 |
+
torch_tensor, gathered_values, gathered_indices, mesh_device
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
calc.set_log_probs_mode([True] * batch_size, num_logprobs=[5] * batch_size)
|
| 385 |
+
result = calc.calculate_topk_log_probs(logits_tt, topk_values_tt, topk_indices_tt)
|
| 386 |
+
|
| 387 |
+
assert result is not None, "Expected LogProbsResult, got None"
|
| 388 |
+
assert isinstance(result, LogProbsResult)
|
| 389 |
+
|
| 390 |
+
host_results = calc.transfer_logprobs_to_host(result, argmax_tensor.squeeze())
|
| 391 |
+
|
| 392 |
+
composer = calc._build_mesh_composer()
|
| 393 |
+
topk_logprobs_host = ttnn.to_torch(result.topk_logprobs, mesh_composer=composer)
|
| 394 |
+
topk_logprobs_host = topk_logprobs_host[0, 0, ...]
|
| 395 |
+
topk_indices_host = ttnn.to_torch(result.topk_indices, mesh_composer=composer)
|
| 396 |
+
topk_indices_host = topk_indices_host[0, 0, ...].long()
|
| 397 |
+
|
| 398 |
+
expected_logprobs = torch.gather(
|
| 399 |
+
log_probs_torch.squeeze(0).squeeze(0),
|
| 400 |
+
dim=-1,
|
| 401 |
+
index=topk_indices_host,
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
passing, pcc = comp_pcc(expected_logprobs, topk_logprobs_host, pcc=0.99)
|
| 405 |
+
print(f"Galaxy top-K logprobs PCC={pcc}")
|
| 406 |
+
assert passing, f"Galaxy top-K logprobs PCC failed: {pcc}"
|
| 407 |
+
|
| 408 |
+
for user_idx in range(batch_size):
|
| 409 |
+
r = host_results[user_idx]
|
| 410 |
+
assert r is not None
|
| 411 |
+
sampled_id = argmax_tensor[0, 0, user_idx, 0].item()
|
| 412 |
+
torch_lp = log_probs_torch[0, 0, user_idx, sampled_id].item()
|
| 413 |
+
assert abs(r["returned_token"]["logprob"] - torch_lp) < 0.05
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
@pytest.mark.parametrize("shape", [TG_SHAPE])
|
| 417 |
+
@pytest.mark.parametrize("device_params", [TG_DEVICE_PARAMS], indirect=True, ids=["tg"])
|
| 418 |
+
@pytest.mark.parametrize("mesh_device", [TG_MESH_SHAPE], indirect=True)
|
| 419 |
+
def test_top_k_log_probs_returns_none_when_not_needed(shape, mesh_device):
|
| 420 |
+
"""calculate_topk_log_probs returns None when disabled."""
|
| 421 |
+
_skip_if_not_galaxy(mesh_device)
|
| 422 |
+
batch_size = shape[2]
|
| 423 |
+
calc = LogProbsCalculator(mesh_device, TG_SUB_CORE_GRIDS, batch_size=batch_size, use_topk_logprobs=True)
|
| 424 |
+
|
| 425 |
+
torch_tensor = torch.randn(shape)
|
| 426 |
+
gathered_values, gathered_indices = _simulate_gathered_topk(torch_tensor, TG_NUM_TP_DEVICES)
|
| 427 |
+
argmax_tensor = torch.argmax(torch_tensor, dim=-1, keepdim=True)
|
| 428 |
+
|
| 429 |
+
logits_tt, topk_values_tt, topk_indices_tt = _push_topk_test_tensors_to_tg(
|
| 430 |
+
torch_tensor, gathered_values, gathered_indices, mesh_device
|
| 431 |
+
)
|
| 432 |
+
|
| 433 |
+
calc.set_log_probs_mode(False, num_logprobs=0)
|
| 434 |
+
result = calc.calculate_topk_log_probs(logits_tt, topk_values_tt, topk_indices_tt)
|
| 435 |
+
assert result is None, "Expected None when logprobs disabled"
|
| 436 |
+
|
| 437 |
+
calc.set_log_probs_mode(True, num_logprobs=0)
|
| 438 |
+
assert calc.topk_logprobs_needed # needed for sampled token logprob
|
| 439 |
+
result = calc.calculate_topk_log_probs(logits_tt, topk_values_tt, topk_indices_tt)
|
| 440 |
+
assert result is not None, "Expected LogProbsResult when logprobs enabled"
|
| 441 |
+
|
| 442 |
+
sampled_ids = argmax_tensor.squeeze()
|
| 443 |
+
host_results = calc.transfer_logprobs_to_host(result, sampled_ids)
|
| 444 |
+
assert len(host_results) == batch_size
|
| 445 |
+
for i in range(batch_size):
|
| 446 |
+
r = host_results[i]
|
| 447 |
+
assert r is not None
|
| 448 |
+
assert r["returned_token"]["token_idx"] == int(sampled_ids[i].item())
|
| 449 |
+
assert len(r["top_logprobs"]["token_indices"]) == 0
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
@pytest.mark.parametrize("shape", [TG_SHAPE])
|
| 453 |
+
@pytest.mark.parametrize("device_params", [TG_DEVICE_PARAMS], indirect=True, ids=["tg"])
|
| 454 |
+
@pytest.mark.parametrize("mesh_device", [TG_MESH_SHAPE], indirect=True)
|
| 455 |
+
def test_per_user_logprobs_enabled(shape, mesh_device):
|
| 456 |
+
"""Mixed per-user logprobs: only even users enabled."""
|
| 457 |
+
_skip_if_not_galaxy(mesh_device)
|
| 458 |
+
torch.manual_seed(42)
|
| 459 |
+
batch_size = shape[2]
|
| 460 |
+
|
| 461 |
+
calc = LogProbsCalculator(mesh_device, TG_SUB_CORE_GRIDS, batch_size=batch_size, use_topk_logprobs=True)
|
| 462 |
+
|
| 463 |
+
torch_tensor = torch.randn(shape)
|
| 464 |
+
for i in range(batch_size):
|
| 465 |
+
torch_tensor[:, :, i, :] = torch_tensor[:, :, i, torch.randperm(shape[-1])]
|
| 466 |
+
|
| 467 |
+
log_probs_torch = F.log_softmax(torch_tensor.to(torch.float16), dim=-1)
|
| 468 |
+
gathered_values, gathered_indices = _simulate_gathered_topk(torch_tensor, TG_NUM_TP_DEVICES)
|
| 469 |
+
argmax_tensor = torch.argmax(torch_tensor, dim=-1, keepdim=True)
|
| 470 |
+
|
| 471 |
+
logits_tt, topk_values_tt, topk_indices_tt = _push_topk_test_tensors_to_tg(
|
| 472 |
+
torch_tensor, gathered_values, gathered_indices, mesh_device
|
| 473 |
+
)
|
| 474 |
+
|
| 475 |
+
enable_log_probs = [i % 2 == 0 for i in range(batch_size)]
|
| 476 |
+
num_logprobs_list = [5 if i % 2 == 0 else 0 for i in range(batch_size)]
|
| 477 |
+
calc.set_log_probs_mode(enable_log_probs, num_logprobs=num_logprobs_list)
|
| 478 |
+
|
| 479 |
+
result = calc.calculate_topk_log_probs(logits_tt, topk_values_tt, topk_indices_tt)
|
| 480 |
+
assert result is not None
|
| 481 |
+
|
| 482 |
+
sampled_ids = argmax_tensor.squeeze()
|
| 483 |
+
host_results = calc.transfer_logprobs_to_host(result, sampled_ids)
|
| 484 |
+
|
| 485 |
+
for i in range(batch_size):
|
| 486 |
+
if enable_log_probs[i]:
|
| 487 |
+
assert host_results[i] is not None
|
| 488 |
+
sampled_id = int(sampled_ids[i].item())
|
| 489 |
+
torch_lp = log_probs_torch[0, 0, i, sampled_id].item()
|
| 490 |
+
assert abs(host_results[i]["returned_token"]["logprob"] - torch_lp) < 0.05
|
| 491 |
+
else:
|
| 492 |
+
assert host_results[i] is None
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
@pytest.mark.parametrize("shape", [TG_SHAPE])
|
| 496 |
+
@pytest.mark.parametrize("device_params", [TG_DEVICE_PARAMS], indirect=True, ids=["tg"])
|
| 497 |
+
@pytest.mark.parametrize("mesh_device", [TG_MESH_SHAPE], indirect=True)
|
| 498 |
+
def test_set_log_probs_mode_validation(shape, mesh_device):
|
| 499 |
+
"""Verify set_log_probs_mode internal state."""
|
| 500 |
+
_skip_if_not_galaxy(mesh_device)
|
| 501 |
+
batch_size = shape[2]
|
| 502 |
+
calc = LogProbsCalculator(mesh_device, TG_SUB_CORE_GRIDS, batch_size=batch_size, use_topk_logprobs=True)
|
| 503 |
+
|
| 504 |
+
calc.set_log_probs_mode(True)
|
| 505 |
+
assert calc.enable_log_probs is True
|
| 506 |
+
assert all(calc.logprobs_enabled)
|
| 507 |
+
assert calc.topk_logprobs_needed # needed for sampled token logprob
|
| 508 |
+
|
| 509 |
+
calc.set_log_probs_mode(True, num_logprobs=5)
|
| 510 |
+
assert calc.topk_logprobs_needed is True
|
| 511 |
+
assert all(n == 5 for n in calc.num_logprobs)
|
| 512 |
+
|
| 513 |
+
enable_list = [True, False, True] + [False] * (batch_size - 3)
|
| 514 |
+
num_lp_list = [10, 0, 3] + [0] * (batch_size - 3)
|
| 515 |
+
calc.set_log_probs_mode(enable_list, num_logprobs=num_lp_list)
|
| 516 |
+
assert calc.enable_log_probs is True
|
| 517 |
+
assert calc.topk_logprobs_needed is True
|
| 518 |
+
assert calc.logprobs_enabled == enable_list
|
| 519 |
+
assert calc.num_logprobs == num_lp_list
|
| 520 |
+
|
| 521 |
+
calc.set_log_probs_mode(False, num_logprobs=0)
|
| 522 |
+
assert calc.enable_log_probs is False
|
| 523 |
+
|
| 524 |
+
calc.set_log_probs_mode(True, num_logprobs=0)
|
| 525 |
+
assert calc.enable_log_probs is True
|
| 526 |
+
assert calc.topk_logprobs_needed # needed for sampled token logprob
|
| 527 |
+
|
| 528 |
+
calc.set_log_probs_mode(False, num_logprobs=0)
|
| 529 |
+
calc.set_log_probs_mode([True, True], num_logprobs=[10, 15], empty_slots=[2, 5])
|
| 530 |
+
assert calc.logprobs_enabled[2] is True
|
| 531 |
+
assert calc.logprobs_enabled[5] is True
|
| 532 |
+
assert calc.logprobs_enabled[0] is False
|
| 533 |
+
assert calc.num_logprobs[2] == 10
|
| 534 |
+
assert calc.num_logprobs[5] == 15
|
| 535 |
+
|
| 536 |
+
calc.set_log_probs_mode(False, num_logprobs=0)
|
| 537 |
+
calc.set_log_probs_mode(True, num_logprobs=7, empty_slots=[0, 3, 4])
|
| 538 |
+
assert all(calc.logprobs_enabled[i] for i in [0, 3, 4])
|
| 539 |
+
assert calc.logprobs_enabled[1] is False
|
| 540 |
+
|
| 541 |
+
calc.set_log_probs_mode([True], num_logprobs=[20], empty_slots=[1])
|
| 542 |
+
assert calc.logprobs_enabled[1] is True
|
| 543 |
+
assert calc.num_logprobs[0] == 7
|
| 544 |
+
assert calc.num_logprobs[1] == 20
|
| 545 |
+
|
| 546 |
+
|
| 547 |
+
@pytest.mark.parametrize("shape", [TG_SHAPE])
|
| 548 |
+
@pytest.mark.parametrize("device_params", [TG_DEVICE_PARAMS], indirect=True, ids=["tg"])
|
| 549 |
+
@pytest.mark.parametrize("mesh_device", [TG_MESH_SHAPE], indirect=True)
|
| 550 |
+
def test_top_k_logprobs_pcc_torch_vs_tt(shape, mesh_device):
|
| 551 |
+
"""Compare host (PyTorch bfloat16) vs device (bfloat16) logprobs for full batch."""
|
| 552 |
+
_skip_if_not_galaxy(mesh_device)
|
| 553 |
+
torch.manual_seed(9999)
|
| 554 |
+
batch_size = shape[2]
|
| 555 |
+
requested_logprobs = MAX_TOP_LOGPROBS
|
| 556 |
+
|
| 557 |
+
calc = LogProbsCalculator(mesh_device, TG_SUB_CORE_GRIDS, batch_size=batch_size, use_topk_logprobs=True)
|
| 558 |
+
|
| 559 |
+
torch_tensor = torch.randn(shape).to(torch.bfloat16)
|
| 560 |
+
for i in range(batch_size):
|
| 561 |
+
torch_tensor[:, :, i, :] = torch_tensor[:, :, i, torch.randperm(shape[-1])]
|
| 562 |
+
|
| 563 |
+
log_probs_torch = F.log_softmax(torch_tensor, dim=-1, dtype=torch.bfloat16)
|
| 564 |
+
gathered_values, gathered_indices = _simulate_gathered_topk(torch_tensor, TG_NUM_TP_DEVICES)
|
| 565 |
+
argmax_tensor = torch.argmax(torch_tensor, dim=-1, keepdim=True)
|
| 566 |
+
|
| 567 |
+
logits_tt, topk_values_tt, topk_indices_tt = _push_topk_test_tensors_to_tg(
|
| 568 |
+
torch_tensor, gathered_values, gathered_indices, mesh_device
|
| 569 |
+
)
|
| 570 |
+
|
| 571 |
+
calc.set_log_probs_mode([True] * batch_size, num_logprobs=[requested_logprobs] * batch_size)
|
| 572 |
+
|
| 573 |
+
result = calc.calculate_topk_log_probs(logits_tt, topk_values_tt, topk_indices_tt)
|
| 574 |
+
assert result is not None
|
| 575 |
+
|
| 576 |
+
sampled_ids = argmax_tensor.squeeze()
|
| 577 |
+
host_results = calc.transfer_logprobs_to_host(result, sampled_ids)
|
| 578 |
+
|
| 579 |
+
for user in range(batch_size):
|
| 580 |
+
r = host_results[user]
|
| 581 |
+
assert r is not None
|
| 582 |
+
|
| 583 |
+
device_sampled_lp = r["returned_token"]["logprob"]
|
| 584 |
+
token_idx = r["returned_token"]["token_idx"]
|
| 585 |
+
torch_sampled_lp = log_probs_torch[0, 0, user, token_idx].item()
|
| 586 |
+
assert abs(device_sampled_lp - torch_sampled_lp) < 0.05
|
| 587 |
+
|
| 588 |
+
top_indices = r["top_logprobs"]["token_indices"]
|
| 589 |
+
top_lps_device = torch.tensor(r["top_logprobs"]["logprobs"], dtype=torch.float32)
|
| 590 |
+
assert len(top_indices) == requested_logprobs
|
| 591 |
+
|
| 592 |
+
top_lps_torch = log_probs_torch[0, 0, user, top_indices].float()
|
| 593 |
+
passing, pcc = comp_pcc(top_lps_torch.unsqueeze(0), top_lps_device.unsqueeze(0), pcc=0.98)
|
| 594 |
+
assert passing, (
|
| 595 |
+
f"User {user} top-{requested_logprobs} logprobs PCC failed: {pcc}\n"
|
| 596 |
+
f" device: {top_lps_device[:5].tolist()}...\n"
|
| 597 |
+
f" torch: {top_lps_torch[:5].tolist()}..."
|
| 598 |
+
)
|
code/models/common/tests/test_utils.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from models.common.tests.utils import stable_model_seed
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def test_stable_model_seed_deterministic() -> None:
|
| 8 |
+
assert stable_model_seed("llama-3") == stable_model_seed("llama-3")
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_stable_model_seed_distinct() -> None:
|
| 12 |
+
assert stable_model_seed("llama-3") != stable_model_seed("mistral-7b")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_stable_model_seed_uint32_range() -> None:
|
| 16 |
+
seed = stable_model_seed("llama-3")
|
| 17 |
+
assert 0 <= seed < 2**32
|
code/models/common/tests/test_validation_tools.py
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
These tests demonstrate how to use @validate_against to compare TTNN implementations
|
| 6 |
+
against reference PyTorch implementations with automatic metrics collection.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import pytest
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
import ttnn
|
| 13 |
+
from models.common.auto_compose import to_torch_auto_compose
|
| 14 |
+
from models.common.metrics import compute_pcc_host
|
| 15 |
+
from models.common.validation_tools import (
|
| 16 |
+
Metric,
|
| 17 |
+
MetricSpec,
|
| 18 |
+
clear_validation_results,
|
| 19 |
+
compare_to_torch,
|
| 20 |
+
compare_to_ttnn,
|
| 21 |
+
enable_validation,
|
| 22 |
+
get_validation_registry,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
# [INFO] the purpose of this test is to validate the validation framework itself,
|
| 26 |
+
# which does not care about the mesh shape or tensor layout; we have other test files on those topics.
|
| 27 |
+
pytestmark = [
|
| 28 |
+
pytest.mark.parametrize(
|
| 29 |
+
"ttnn_mesh_device",
|
| 30 |
+
[
|
| 31 |
+
(1, 1),
|
| 32 |
+
],
|
| 33 |
+
ids=[
|
| 34 |
+
"1x1",
|
| 35 |
+
],
|
| 36 |
+
indirect=True,
|
| 37 |
+
),
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
# ============================================================================
|
| 41 |
+
# Example 1: Validating RMSNorm against PyTorch reference
|
| 42 |
+
# ============================================================================
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def torch_rms_norm(x, weight, eps=1e-6):
|
| 46 |
+
"""Reference PyTorch implementation of RMS normalization"""
|
| 47 |
+
variance = x.pow(2).mean(-1, keepdim=True)
|
| 48 |
+
x = x * torch.rsqrt(variance + eps)
|
| 49 |
+
return weight * x
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class HostValidatedRMSNorm:
|
| 53 |
+
"""RMS Normalization with validation decorator using old input_map pattern"""
|
| 54 |
+
|
| 55 |
+
def __init__(self, weight: torch.Tensor, eps: float, device):
|
| 56 |
+
self.eps = eps
|
| 57 |
+
self.weight = ttnn.from_torch(
|
| 58 |
+
weight.unsqueeze(0).unsqueeze(0), device=device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
@compare_to_torch(
|
| 62 |
+
reference_fn=torch_rms_norm,
|
| 63 |
+
input_to_torch=lambda self, x: (
|
| 64 |
+
# [INFO] produce input args to torch_rms_norm as a tuple
|
| 65 |
+
(to_torch_auto_compose(x), to_torch_auto_compose(self.weight)),
|
| 66 |
+
# [INFO] produce input kwargs to torch_rms_norm as a dict
|
| 67 |
+
{"eps": self.eps},
|
| 68 |
+
),
|
| 69 |
+
metric_tolerances={
|
| 70 |
+
Metric.MAX_ABS_ERROR: 1e-2,
|
| 71 |
+
Metric.MEAN_ABS_ERROR: 1e-3,
|
| 72 |
+
"pcc": 0.99, # can use enum or their string values
|
| 73 |
+
},
|
| 74 |
+
enabled=True,
|
| 75 |
+
)
|
| 76 |
+
def __call__(self, x):
|
| 77 |
+
# x shape: [1, seq_len, hidden_size]
|
| 78 |
+
x_squared = ttnn.mul(x, x)
|
| 79 |
+
mean_x_squared = ttnn.mean(x_squared, dim=-1, keepdim=True)
|
| 80 |
+
rms = ttnn.sqrt(ttnn.add(mean_x_squared, self.eps))
|
| 81 |
+
x_normed = ttnn.mul(x, ttnn.reciprocal(rms))
|
| 82 |
+
return ttnn.mul(x_normed, self.weight)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class DeviceValidatedRMSNorm:
|
| 86 |
+
"""RMS Normalization - ultra-clean pattern: NO conversions needed!"""
|
| 87 |
+
|
| 88 |
+
def __init__(self, weight: torch.Tensor, eps: float, device):
|
| 89 |
+
self.eps = eps
|
| 90 |
+
self.weight_torch = weight # Keep for reference
|
| 91 |
+
self.weight = ttnn.from_torch(
|
| 92 |
+
weight.unsqueeze(0).unsqueeze(0), device=device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT
|
| 93 |
+
)
|
| 94 |
+
self.device = device
|
| 95 |
+
|
| 96 |
+
def _reference_impl(self, x):
|
| 97 |
+
"""Reference implementation - mocking a TTNN reference implementation for testing"""
|
| 98 |
+
# Convert TTNN to torch for reference computation
|
| 99 |
+
x_torch = ttnn.to_torch(x).squeeze(0)
|
| 100 |
+
result_torch = torch_rms_norm(x_torch, self.weight_torch, self.eps)
|
| 101 |
+
# Convert back to TTNN to match __call__ output type
|
| 102 |
+
return ttnn.from_torch(
|
| 103 |
+
result_torch.unsqueeze(0), device=self.device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
# [INFO] this decorator is useful when the reference function is a TTNN-native function.
|
| 107 |
+
# currently, it is experimental and requires the reference function has same-ordered
|
| 108 |
+
# inputs as the decorated function.
|
| 109 |
+
@compare_to_ttnn(
|
| 110 |
+
reference_fn=lambda self, x: self._reference_impl(x),
|
| 111 |
+
# [INFO] passing `metric_tolerances` is optional; if not provided, the default tolerances will be used:
|
| 112 |
+
# metric_tolerances={
|
| 113 |
+
# Metric.MAX_ABS_ERROR: 1e-2,
|
| 114 |
+
# Metric.PCC: 0.99,
|
| 115 |
+
# },
|
| 116 |
+
)
|
| 117 |
+
def __call__(self, x):
|
| 118 |
+
# x shape: [1, seq_len, hidden_size]
|
| 119 |
+
x_squared = ttnn.mul(x, x)
|
| 120 |
+
mean_x_squared = ttnn.mean(x_squared, dim=-1, keepdim=True)
|
| 121 |
+
rms = ttnn.sqrt(ttnn.add(mean_x_squared, self.eps))
|
| 122 |
+
x_normed = ttnn.mul(x, ttnn.reciprocal(rms))
|
| 123 |
+
return ttnn.mul(x_normed, self.weight)
|
| 124 |
+
|
| 125 |
+
@compare_to_ttnn(
|
| 126 |
+
reference_fn=lambda self, x: self._reference_impl(x),
|
| 127 |
+
)
|
| 128 |
+
def _call_torch__(self, x):
|
| 129 |
+
# copied __call__ code below and converted to torch tensor to mock a function under test that returns a torch tensor
|
| 130 |
+
# x shape: [1, seq_len, hidden_size]
|
| 131 |
+
x_squared = ttnn.mul(x, x)
|
| 132 |
+
mean_x_squared = ttnn.mean(x_squared, dim=-1, keepdim=True)
|
| 133 |
+
rms = ttnn.sqrt(ttnn.add(mean_x_squared, self.eps))
|
| 134 |
+
x_normed = ttnn.mul(x, ttnn.reciprocal(rms))
|
| 135 |
+
return to_torch_auto_compose(ttnn.mul(x_normed, self.weight))
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def test_validation_rmsnorm_host_and_device(ttnn_mesh_device: ttnn.MeshDevice):
|
| 139 |
+
registry = get_validation_registry()
|
| 140 |
+
|
| 141 |
+
hidden_size = 64
|
| 142 |
+
batch_size = 1
|
| 143 |
+
seq_len = 8
|
| 144 |
+
|
| 145 |
+
weight = torch.randn(hidden_size, dtype=torch.bfloat16)
|
| 146 |
+
|
| 147 |
+
# Device-validated RMSNorm
|
| 148 |
+
rms_device = DeviceValidatedRMSNorm(weight, eps=1e-6, device=ttnn_mesh_device)
|
| 149 |
+
x = torch.randn(batch_size, seq_len, hidden_size, dtype=torch.bfloat16)
|
| 150 |
+
x_tt = ttnn.from_torch(x.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 151 |
+
_ = rms_device(x_tt)
|
| 152 |
+
|
| 153 |
+
_ = rms_device._call_torch__(x_tt)
|
| 154 |
+
|
| 155 |
+
# Host-validated RMSNorm
|
| 156 |
+
rms_host = HostValidatedRMSNorm(weight, eps=1e-6, device=ttnn_mesh_device)
|
| 157 |
+
x2 = torch.randn(batch_size, seq_len, hidden_size, dtype=torch.bfloat16)
|
| 158 |
+
x2_tt = ttnn.from_torch(x2.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 159 |
+
_ = rms_host(x2_tt)
|
| 160 |
+
|
| 161 |
+
assert len(registry.results) >= 3
|
| 162 |
+
# Expect all validations to pass
|
| 163 |
+
assert registry.results[0].metrics[Metric.MAX_ABS_ERROR].passed
|
| 164 |
+
assert registry.results[1].metrics[Metric.MAX_ABS_ERROR].passed
|
| 165 |
+
assert registry.results[2].metrics[Metric.MAX_ABS_ERROR].passed
|
| 166 |
+
assert registry.results[2].metrics[Metric.MEAN_ABS_ERROR].passed
|
| 167 |
+
assert registry.results[0].metrics[Metric.PCC].passed
|
| 168 |
+
assert registry.results[1].metrics[Metric.PCC].passed
|
| 169 |
+
assert registry.results[2].metrics[Metric.PCC].passed
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
# ============================================================================
|
| 173 |
+
# Example 2: Validating matrix multiplication
|
| 174 |
+
# ============================================================================
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
@compare_to_torch(
|
| 178 |
+
reference_fn=torch.matmul,
|
| 179 |
+
# [INFO] when reference function accepts inputs in the same order as the decorated function,
|
| 180 |
+
# we can omit input_to_torch; the mapping will be inferred automatically.
|
| 181 |
+
metric_tolerances={
|
| 182 |
+
Metric.MAX_ABS_ERROR: 1.5e-1,
|
| 183 |
+
Metric.PCC: 0.99,
|
| 184 |
+
},
|
| 185 |
+
)
|
| 186 |
+
def ttnn_matmul(a, b):
|
| 187 |
+
"""TTNN matrix multiplication with validation"""
|
| 188 |
+
return ttnn.matmul(a, b)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
# make a test case to show how to directly use auto_compose to convert ttnn to torch
|
| 192 |
+
@compare_to_torch(
|
| 193 |
+
reference_fn=torch.matmul,
|
| 194 |
+
# [INFO] this is a simple example of input remapping.
|
| 195 |
+
input_to_torch=lambda a, b: (to_torch_auto_compose(b), to_torch_auto_compose(a)),
|
| 196 |
+
metric_tolerances={
|
| 197 |
+
Metric.MAX_ABS_ERROR: 1.5e-1,
|
| 198 |
+
Metric.PCC: 0.99,
|
| 199 |
+
},
|
| 200 |
+
)
|
| 201 |
+
def ttnn_matmul_reverse(a, b):
|
| 202 |
+
"""TTNN matrix multiplication with validation"""
|
| 203 |
+
return ttnn.matmul(b, a)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def test_validation_matmul(ttnn_mesh_device: ttnn.MeshDevice):
|
| 207 |
+
registry = get_validation_registry()
|
| 208 |
+
|
| 209 |
+
m, n, k = 16, 24, 12
|
| 210 |
+
a = torch.randn(1, m, k, dtype=torch.bfloat16)
|
| 211 |
+
b = torch.randn(1, k, n, dtype=torch.bfloat16)
|
| 212 |
+
|
| 213 |
+
a_tt = ttnn.from_torch(a.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 214 |
+
b_tt = ttnn.from_torch(b.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 215 |
+
|
| 216 |
+
_ = ttnn_matmul(a_tt, b_tt)
|
| 217 |
+
_ = ttnn_matmul_reverse(b_tt, a_tt)
|
| 218 |
+
|
| 219 |
+
# Expect two validations recorded and both passed
|
| 220 |
+
assert len(registry.results) >= 2
|
| 221 |
+
assert registry.results[-1].passed
|
| 222 |
+
assert registry.results[-2].passed
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
# ============================================================================
|
| 226 |
+
# Example 3: Custom metrics and complex mappings
|
| 227 |
+
# ============================================================================
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def custom_attention_reference(q, k, v, scale):
|
| 231 |
+
"""Reference attention computation"""
|
| 232 |
+
scores = torch.matmul(q, k.transpose(-2, -1)) * scale
|
| 233 |
+
attn_weights = torch.nn.functional.softmax(scores, dim=-1)
|
| 234 |
+
return torch.matmul(attn_weights, v)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
@compare_to_torch(
|
| 238 |
+
reference_fn=custom_attention_reference,
|
| 239 |
+
# [INFO]{ when reference function accepts inputs in the same order as the decorated function,
|
| 240 |
+
# we can omit input_to_torch; it will be inferred automatically as if the following code were written:
|
| 241 |
+
# input_to_torch=lambda q, k, v, scale: (
|
| 242 |
+
# to_torch_auto_compose(q),
|
| 243 |
+
# to_torch_auto_compose(k),
|
| 244 |
+
# to_torch_auto_compose(v),
|
| 245 |
+
# scale,
|
| 246 |
+
# ),
|
| 247 |
+
# [INFO]}
|
| 248 |
+
metric_tolerances={
|
| 249 |
+
Metric.MAX_ABS_ERROR: 0.1,
|
| 250 |
+
Metric.MEAN_ABS_ERROR: 0.02,
|
| 251 |
+
Metric.PCC: 0.99,
|
| 252 |
+
},
|
| 253 |
+
)
|
| 254 |
+
def ttnn_attention(q, k, v, scale):
|
| 255 |
+
"""Simplified attention with validation"""
|
| 256 |
+
scores = ttnn.matmul(q, ttnn.transpose(k, -2, -1))
|
| 257 |
+
scores = ttnn.mul(scores, scale)
|
| 258 |
+
attn_weights = ttnn.softmax(scores, dim=-1)
|
| 259 |
+
return ttnn.matmul(attn_weights, v)
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def test_validation_attention(ttnn_mesh_device: ttnn.MeshDevice):
|
| 263 |
+
m, n, dk, dv = 8, 8, 16, 16
|
| 264 |
+
q = torch.randn(1, m, dk, dtype=torch.bfloat16)
|
| 265 |
+
k = torch.randn(1, n, dk, dtype=torch.bfloat16)
|
| 266 |
+
v = torch.randn(1, n, dv, dtype=torch.bfloat16)
|
| 267 |
+
|
| 268 |
+
q_tt = ttnn.from_torch(q.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 269 |
+
k_tt = ttnn.from_torch(k.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 270 |
+
v_tt = ttnn.from_torch(v.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 271 |
+
|
| 272 |
+
registry = get_validation_registry()
|
| 273 |
+
before = len(registry.results)
|
| 274 |
+
scale = 1.0 / (dk**0.5)
|
| 275 |
+
_ = ttnn_attention(q_tt, k_tt, v_tt, scale)
|
| 276 |
+
assert len(registry.results) == before + 1
|
| 277 |
+
test_result = registry.results[-1]
|
| 278 |
+
# expect the test to pass max_abs_error and pcc checks
|
| 279 |
+
assert test_result.metrics[Metric.MAX_ABS_ERROR].passed
|
| 280 |
+
assert test_result.metrics[Metric.PCC].passed
|
| 281 |
+
assert test_result.metrics[Metric.MEAN_ABS_ERROR].passed
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
# ============================================================================
|
| 285 |
+
# Example 4: Validating from_torch checkpoint
|
| 286 |
+
# ============================================================================
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
@compare_to_torch(
|
| 290 |
+
reference_fn=lambda tensor, device: tensor,
|
| 291 |
+
output_to_torch=to_torch_auto_compose,
|
| 292 |
+
metric_tolerances={
|
| 293 |
+
Metric.MAX_ABS_ERROR: 0.015,
|
| 294 |
+
Metric.MEAN_ABS_ERROR: 0.01,
|
| 295 |
+
Metric.PCC: 0.99,
|
| 296 |
+
},
|
| 297 |
+
)
|
| 298 |
+
def from_torch_checkpoint(tensor: torch.Tensor, device: ttnn.MeshDevice):
|
| 299 |
+
"""Return TTNN tensor created via from_torch from a checkpoint tensor."""
|
| 300 |
+
return ttnn.from_torch(tensor, device=device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def test_validation_checkpoint_from_torch(ttnn_mesh_device: ttnn.MeshDevice):
|
| 304 |
+
registry = get_validation_registry()
|
| 305 |
+
before = len(registry.results)
|
| 306 |
+
|
| 307 |
+
# Simulated checkpoint tensor (e.g., a weight matrix)
|
| 308 |
+
rows, cols = 32, 128
|
| 309 |
+
weight = torch.randn(rows, cols, dtype=torch.float32)
|
| 310 |
+
|
| 311 |
+
# Validate a direct from_torch call via the decorated function
|
| 312 |
+
_ = from_torch_checkpoint(weight, ttnn_mesh_device)
|
| 313 |
+
|
| 314 |
+
# Ensure a result was recorded and it passed
|
| 315 |
+
assert len(registry.results) == before + 1
|
| 316 |
+
assert registry.results[-1].passed
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
# ============================================================================
|
| 320 |
+
# Example 5: Validating with MetricSpec
|
| 321 |
+
# ============================================================================
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
@compare_to_torch(
|
| 325 |
+
reference_fn=torch.matmul,
|
| 326 |
+
metric_tolerances={
|
| 327 |
+
"pcc_host": MetricSpec(tolerance=0.99, higher_is_better=True, compute_fn=compute_pcc_host),
|
| 328 |
+
},
|
| 329 |
+
)
|
| 330 |
+
def ttnn_matmul_metric_spec(a, b):
|
| 331 |
+
return ttnn.matmul(a, b)
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def test_validation_matmul_metric_spec(ttnn_mesh_device: ttnn.MeshDevice):
|
| 335 |
+
registry = get_validation_registry()
|
| 336 |
+
before = len(registry.results)
|
| 337 |
+
|
| 338 |
+
m, n, k = 8, 10, 6
|
| 339 |
+
a = torch.randn(1, m, k, dtype=torch.bfloat16)
|
| 340 |
+
b = torch.randn(1, k, n, dtype=torch.bfloat16)
|
| 341 |
+
|
| 342 |
+
a_tt = ttnn.from_torch(a.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 343 |
+
b_tt = ttnn.from_torch(b.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 344 |
+
|
| 345 |
+
_ = ttnn_matmul_metric_spec(a_tt, b_tt)
|
| 346 |
+
|
| 347 |
+
assert len(registry.results) == before + 1
|
| 348 |
+
assert registry.results[-1].passed
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
# ============================================================================
|
| 352 |
+
# Example 6: Validating with non-decorator use of compare_to_torch
|
| 353 |
+
# between a class instance and a reference class instance!
|
| 354 |
+
# ============================================================================
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def test_validation_non_decorator_class_vs_class_torch(ttnn_mesh_device: ttnn.MeshDevice):
|
| 358 |
+
"""Validate a callable class against a reference class using non-decorator style."""
|
| 359 |
+
registry = get_validation_registry()
|
| 360 |
+
before = len(registry.results)
|
| 361 |
+
|
| 362 |
+
# Simple linear layer implemented in TTNN (__call__) vs Torch reference (forward)
|
| 363 |
+
m, n, k = 8, 10, 6
|
| 364 |
+
x = torch.randn(1, m, k, dtype=torch.bfloat16)
|
| 365 |
+
w = torch.randn(1, k, n, dtype=torch.bfloat16)
|
| 366 |
+
|
| 367 |
+
class TTLinear:
|
| 368 |
+
def __init__(self, weight: torch.Tensor, device: ttnn.MeshDevice):
|
| 369 |
+
# Weight expected as [1, k, n]; add device batch dim for TTNN tensor
|
| 370 |
+
self.weight = ttnn.from_torch(
|
| 371 |
+
weight.unsqueeze(0), device=device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT
|
| 372 |
+
)
|
| 373 |
+
|
| 374 |
+
def __call__(self, inp):
|
| 375 |
+
return ttnn.matmul(inp, self.weight)
|
| 376 |
+
|
| 377 |
+
class TorchLinearRef:
|
| 378 |
+
def __init__(self, weight: torch.Tensor):
|
| 379 |
+
self.weight = weight
|
| 380 |
+
|
| 381 |
+
def forward(self, inp: torch.Tensor):
|
| 382 |
+
return torch.matmul(inp, self.weight)
|
| 383 |
+
|
| 384 |
+
# Instantiate both implementations
|
| 385 |
+
layer = TTLinear(w, ttnn_mesh_device)
|
| 386 |
+
ref_layer = TorchLinearRef(w)
|
| 387 |
+
|
| 388 |
+
# Convert input to TTNN tensor (add device batch dim)
|
| 389 |
+
x_tt = ttnn.from_torch(x.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 390 |
+
|
| 391 |
+
# Non-decorator usage: wrap the unbound __call__ so we can pass (self, x)
|
| 392 |
+
validated_call = compare_to_torch(
|
| 393 |
+
reference_fn=lambda self, inp: ref_layer.forward(inp),
|
| 394 |
+
metric_tolerances={
|
| 395 |
+
Metric.MAX_ABS_ERROR: 1.5e-1,
|
| 396 |
+
Metric.PCC: 0.99,
|
| 397 |
+
},
|
| 398 |
+
)(TTLinear.__call__)
|
| 399 |
+
|
| 400 |
+
_ = validated_call(layer, x_tt)
|
| 401 |
+
|
| 402 |
+
assert len(registry.results) == before + 1
|
| 403 |
+
assert registry.results[-1].passed
|
| 404 |
+
|
| 405 |
+
|
| 406 |
+
# ============================================================================
|
| 407 |
+
# Example 7: Validating with non-decorator use of compare_to_ttnn
|
| 408 |
+
# between a class instance (return torch tensor) and a reference class instance
|
| 409 |
+
# (return TTNN tensor)
|
| 410 |
+
# NOTE: This use of compare_to_ttnn could come in handy in situations where a module instance
|
| 411 |
+
# within torch implementation is being replaced by a TTNN module instance and
|
| 412 |
+
# we want to check the output of the TTNN module instance against the output of
|
| 413 |
+
# the torch module instance during end2end testing.
|
| 414 |
+
# ============================================================================
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
def test_validation_non_decorator_class_vs_class_ttnn(ttnn_mesh_device: ttnn.MeshDevice):
|
| 418 |
+
"""Validate a callable TTNN class against a TTNN reference class using non-decorator style."""
|
| 419 |
+
registry = get_validation_registry()
|
| 420 |
+
before = len(registry.results)
|
| 421 |
+
|
| 422 |
+
# Simple linear layer implemented in TTNN (__call__) vs TTNN reference (forward)
|
| 423 |
+
m, n, k = 8, 10, 6
|
| 424 |
+
x = torch.randn(1, m, k, dtype=torch.bfloat16)
|
| 425 |
+
w = torch.randn(1, k, n, dtype=torch.bfloat16)
|
| 426 |
+
|
| 427 |
+
class TTLinear:
|
| 428 |
+
def __init__(self, weight: torch.Tensor, device: ttnn.MeshDevice):
|
| 429 |
+
# Weight expected as [1, k, n]; add device batch dim for TTNN tensor
|
| 430 |
+
self.weight = ttnn.from_torch(
|
| 431 |
+
weight.unsqueeze(0), device=device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
def __call__(self, inp):
|
| 435 |
+
return ttnn.matmul(inp, self.weight)
|
| 436 |
+
|
| 437 |
+
class TorchLinearRef:
|
| 438 |
+
def __init__(self, weight: torch.Tensor, device: ttnn.MeshDevice):
|
| 439 |
+
self.weight = weight
|
| 440 |
+
|
| 441 |
+
def forward(self, inp):
|
| 442 |
+
return torch.matmul(inp, self.weight)
|
| 443 |
+
|
| 444 |
+
# Instantiate both implementations
|
| 445 |
+
layer = TTLinear(w, ttnn_mesh_device)
|
| 446 |
+
ref_layer = TorchLinearRef(w, ttnn_mesh_device)
|
| 447 |
+
|
| 448 |
+
# # Convert input to TTNN tensor (add device batch dim)
|
| 449 |
+
# x_tt = ttnn.from_torch(x.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 450 |
+
|
| 451 |
+
# Non-decorator usage: wrap the unbound __call__ so we can pass (self, x)
|
| 452 |
+
validated_call = compare_to_ttnn(
|
| 453 |
+
reference_fn=lambda inp: layer(inp),
|
| 454 |
+
input_to_ttnn=lambda self, inp: (
|
| 455 |
+
ttnn.from_torch(inp, device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT),
|
| 456 |
+
),
|
| 457 |
+
metric_tolerances={
|
| 458 |
+
Metric.MAX_ABS_ERROR: 1.5e-1,
|
| 459 |
+
Metric.PCC: 0.99,
|
| 460 |
+
},
|
| 461 |
+
)(TorchLinearRef.forward)
|
| 462 |
+
|
| 463 |
+
ttnn.SetDefaultDevice(ttnn_mesh_device)
|
| 464 |
+
_ = validated_call(ref_layer, x.unsqueeze(0))
|
| 465 |
+
|
| 466 |
+
assert len(registry.results) == before + 1
|
| 467 |
+
assert registry.results[-1].passed
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
def test_return_reference_output_torch(ttnn_mesh_device: ttnn.MeshDevice):
|
| 471 |
+
"""Demonstrate return_reference_output=True returns the reference (torch) output.
|
| 472 |
+
|
| 473 |
+
The decorator computes torch.matmul on host for reference, then returns that
|
| 474 |
+
reference result converted back to a TTNN tensor distributed like the impl output.
|
| 475 |
+
"""
|
| 476 |
+
registry = get_validation_registry()
|
| 477 |
+
before = len(registry.results)
|
| 478 |
+
|
| 479 |
+
m, n, k = 8, 10, 6
|
| 480 |
+
a = torch.randn(1, m, k, dtype=torch.bfloat16)
|
| 481 |
+
b = torch.randn(1, k, n, dtype=torch.bfloat16)
|
| 482 |
+
a_tt = ttnn.from_torch(a.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 483 |
+
b_tt = ttnn.from_torch(b.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 484 |
+
|
| 485 |
+
ref_host = torch.ones(m, n, dtype=torch.bfloat16)
|
| 486 |
+
|
| 487 |
+
@compare_to_torch(
|
| 488 |
+
# mock a reference function that returns a torch tensor with the same shape as the decorated function output
|
| 489 |
+
reference_fn=lambda a, b: ref_host,
|
| 490 |
+
metric_tolerances={
|
| 491 |
+
Metric.MAX_ABS_ERROR: 1, # outrageous tolerance to confirm the mock
|
| 492 |
+
Metric.PCC: 0.99,
|
| 493 |
+
},
|
| 494 |
+
return_reference_output=True,
|
| 495 |
+
)
|
| 496 |
+
def _impl_matmul(a, b):
|
| 497 |
+
return ttnn.matmul(a, b)
|
| 498 |
+
|
| 499 |
+
# Call impl; returned value should be the reference result (distributed as impl output)
|
| 500 |
+
out_tt = _impl_matmul(a_tt, b_tt)
|
| 501 |
+
|
| 502 |
+
# Registry records one validation
|
| 503 |
+
assert len(registry.results) == before + 1
|
| 504 |
+
assert not registry.results[-1].metrics[Metric.MAX_ABS_ERROR].passed
|
| 505 |
+
assert registry.results[-1].metrics[Metric.PCC].passed
|
| 506 |
+
|
| 507 |
+
# Convert both outputs to host and verify numerical equivalence
|
| 508 |
+
out_host = to_torch_auto_compose(out_tt)
|
| 509 |
+
assert torch.allclose(out_host, ref_host)
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
# ============================================================================
|
| 513 |
+
# Additional test functions
|
| 514 |
+
# ============================================================================
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
def test_validation_enable_disable(ttnn_mesh_device: ttnn.MeshDevice):
|
| 518 |
+
a = torch.randn(1, 8, 8, dtype=torch.bfloat16)
|
| 519 |
+
b = torch.randn(1, 8, 8, dtype=torch.bfloat16)
|
| 520 |
+
a_tt = ttnn.from_torch(a.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 521 |
+
b_tt = ttnn.from_torch(b.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 522 |
+
|
| 523 |
+
registry = get_validation_registry()
|
| 524 |
+
recorded_after_enable = len(registry.results)
|
| 525 |
+
# Disable validation: should not record
|
| 526 |
+
enable_validation(False)
|
| 527 |
+
_ = ttnn_matmul(a_tt, b_tt)
|
| 528 |
+
assert len(registry.results) == recorded_after_enable
|
| 529 |
+
|
| 530 |
+
# Re-enable for subsequent tests
|
| 531 |
+
enable_validation(True)
|
| 532 |
+
|
| 533 |
+
|
| 534 |
+
def test_validation_non_decorator_host(ttnn_mesh_device: ttnn.MeshDevice):
|
| 535 |
+
registry = get_validation_registry()
|
| 536 |
+
before = len(registry.results)
|
| 537 |
+
|
| 538 |
+
m, n, k = 8, 10, 6
|
| 539 |
+
a = torch.randn(1, m, k, dtype=torch.bfloat16)
|
| 540 |
+
b = torch.randn(1, k, n, dtype=torch.bfloat16)
|
| 541 |
+
a_tt = ttnn.from_torch(a.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 542 |
+
b_tt = ttnn.from_torch(b.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 543 |
+
|
| 544 |
+
def _matmul(a, b):
|
| 545 |
+
return ttnn.matmul(a, b)
|
| 546 |
+
|
| 547 |
+
validated_matmul = compare_to_torch(
|
| 548 |
+
reference_fn=torch.matmul,
|
| 549 |
+
metric_tolerances={
|
| 550 |
+
Metric.MAX_ABS_ERROR: 1.5e-1,
|
| 551 |
+
Metric.PCC: 0.99,
|
| 552 |
+
},
|
| 553 |
+
)(_matmul)
|
| 554 |
+
|
| 555 |
+
_ = validated_matmul(a_tt, b_tt)
|
| 556 |
+
|
| 557 |
+
assert len(registry.results) == before + 1
|
| 558 |
+
assert registry.results[-1].passed
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
def test_validation_raises_on_reference_exception(ttnn_mesh_device: ttnn.MeshDevice):
|
| 562 |
+
"""When raise_exceptions=True, reference exceptions should propagate and not record results."""
|
| 563 |
+
registry = get_validation_registry()
|
| 564 |
+
before = len(registry.results)
|
| 565 |
+
|
| 566 |
+
a = torch.randn(1, 8, 8, dtype=torch.bfloat16)
|
| 567 |
+
b = torch.randn(1, 8, 8, dtype=torch.bfloat16)
|
| 568 |
+
a_tt = ttnn.from_torch(a.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 569 |
+
b_tt = ttnn.from_torch(b.unsqueeze(0), device=ttnn_mesh_device, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT)
|
| 570 |
+
|
| 571 |
+
def _ref_raises(a, b):
|
| 572 |
+
pass
|
| 573 |
+
|
| 574 |
+
# [INFO] make a mismatched signature on reference function to force the reference function to raise an exception!
|
| 575 |
+
@compare_to_torch(reference_fn=lambda a, b, c: _ref_raises(a, b), raise_exceptions=True)
|
| 576 |
+
def _matmul(a, b):
|
| 577 |
+
return ttnn.matmul(a, b)
|
| 578 |
+
|
| 579 |
+
with pytest.raises(TypeError) as e:
|
| 580 |
+
_ = _matmul(a_tt, b_tt)
|
| 581 |
+
assert "missing 1 required positional argument: 'c'" in str(e.value)
|
| 582 |
+
assert len(registry.results) == before + 1
|
| 583 |
+
|
| 584 |
+
# [INFO] make a mismatched signature on output_to_torch to force the reference function to raise an exception!
|
| 585 |
+
@compare_to_torch(reference_fn=lambda a, b: ..., output_to_torch=lambda x, y: ..., raise_exceptions=True)
|
| 586 |
+
def _matmul_too(a, b):
|
| 587 |
+
return ttnn.matmul(a, b)
|
| 588 |
+
|
| 589 |
+
with pytest.raises(TypeError) as e:
|
| 590 |
+
_ = _matmul_too(a_tt, b_tt)
|
| 591 |
+
assert "missing 1 required positional argument: 'y'" in str(e.value)
|
| 592 |
+
assert len(registry.results) == before + 2
|
| 593 |
+
|
| 594 |
+
# [INFO] make a mismatched signature on input_to_torch to force the reference function to raise an exception!
|
| 595 |
+
@compare_to_torch(reference_fn=lambda a, b: ..., input_to_torch=lambda x: ..., raise_exceptions=True)
|
| 596 |
+
def _matmul_three(a, b):
|
| 597 |
+
return ttnn.matmul(a, b)
|
| 598 |
+
|
| 599 |
+
with pytest.raises(TypeError) as e:
|
| 600 |
+
_ = _matmul_three(a_tt, b_tt)
|
| 601 |
+
assert "takes 1 positional argument but 2 were given" in str(e.value)
|
| 602 |
+
assert len(registry.results) == before + 3
|
| 603 |
+
|
| 604 |
+
|
| 605 |
+
@pytest.fixture(scope="module", autouse=True)
|
| 606 |
+
def _print_validation_report_after_module(request):
|
| 607 |
+
# Runs once after all tests in this module finish
|
| 608 |
+
yield
|
| 609 |
+
registry = get_validation_registry()
|
| 610 |
+
reporter = request.config.pluginmanager.get_plugin("terminalreporter")
|
| 611 |
+
reporter.write_line("Printing validation report after yield")
|
| 612 |
+
registry.print_report(verbose=True)
|
| 613 |
+
|
| 614 |
+
|
| 615 |
+
@pytest.fixture(scope="module", autouse=True)
|
| 616 |
+
def _clear_validation_results_before_module():
|
| 617 |
+
clear_validation_results()
|
| 618 |
+
|
| 619 |
+
|
| 620 |
+
# todo)) add an example where the reference is a file name that we can load output tensors from!
|
| 621 |
+
# idea: validating against tensors -- give us a file name and we can compare our tensor under test against that!
|
| 622 |
+
# - a filename for the tensor
|
code/models/common/tests/utils.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""Shared helpers for models/common tests."""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import zlib
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def stable_model_seed(model_name: str) -> int:
|
| 12 |
+
"""Stable 32-bit seed derived from model name.
|
| 13 |
+
|
| 14 |
+
Python's built-in hash is randomized per process, which breaks reproducibility
|
| 15 |
+
across runs and can mismatch on-disk cached weights. Use CRC32 instead.
|
| 16 |
+
|
| 17 |
+
NOTE: Avoid a single hardcoded global seed (e.g., 1234) for all models; a
|
| 18 |
+
per-model stable seed keeps caches distinct and reduces correlated RNG paths.
|
| 19 |
+
"""
|
| 20 |
+
return zlib.crc32(model_name.encode("utf-8")) & 0xFFFFFFFF
|
code/models/common/utility_functions.py
ADDED
|
@@ -0,0 +1,1216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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):
|
| 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
|
| 504 |
+
if torch.any(golden.bool()) != torch.any(calculated.bool()):
|
| 505 |
+
logger.error("One tensor is all zero")
|
| 506 |
+
return False, 0.0
|
| 507 |
+
|
| 508 |
+
# For now, mask all infs and nans so that we check the rest... TODO
|
| 509 |
+
# Skip this for integer types which don't have NaN/Inf values
|
| 510 |
+
if golden.dtype.is_floating_point:
|
| 511 |
+
# Check if dtype is FP8 - they don't support isinf/isneginf/masked_fill operations
|
| 512 |
+
is_fp8 = golden.dtype in [torch.float8_e4m3fn, torch.float8_e5m2]
|
| 513 |
+
|
| 514 |
+
if is_fp8:
|
| 515 |
+
# Convert FP8 to float32 for comparison since FP8 doesn't support many operations
|
| 516 |
+
golden = golden.to(torch.float32)
|
| 517 |
+
calculated = calculated.to(torch.float32)
|
| 518 |
+
|
| 519 |
+
golden = golden.clone()
|
| 520 |
+
calculated = calculated.clone()
|
| 521 |
+
|
| 522 |
+
# Mask NaN and inf values
|
| 523 |
+
golden[
|
| 524 |
+
torch.logical_or(
|
| 525 |
+
torch.isnan(golden),
|
| 526 |
+
torch.logical_or(torch.isinf(golden), torch.isneginf(golden)),
|
| 527 |
+
)
|
| 528 |
+
] = 0
|
| 529 |
+
calculated[
|
| 530 |
+
torch.logical_or(
|
| 531 |
+
torch.isnan(calculated),
|
| 532 |
+
torch.logical_or(torch.isinf(calculated), torch.isneginf(calculated)),
|
| 533 |
+
)
|
| 534 |
+
] = 0
|
| 535 |
+
|
| 536 |
+
if torch.equal(golden, calculated):
|
| 537 |
+
return True, 1.0
|
| 538 |
+
|
| 539 |
+
if golden.dtype == torch.bfloat16:
|
| 540 |
+
golden = golden.type(torch.float32)
|
| 541 |
+
calculated = calculated.type(torch.float32)
|
| 542 |
+
cal_pcc = np.min(
|
| 543 |
+
np.ma.corrcoef(
|
| 544 |
+
np.ma.masked_invalid(torch.squeeze(golden).detach().numpy()).flatten(),
|
| 545 |
+
np.ma.masked_invalid(torch.squeeze(calculated).detach().numpy()).flatten(),
|
| 546 |
+
)
|
| 547 |
+
)
|
| 548 |
+
|
| 549 |
+
if isinstance(cal_pcc, np.ma.core.MaskedConstant):
|
| 550 |
+
return True, 1.0
|
| 551 |
+
|
| 552 |
+
return cal_pcc >= pcc, cal_pcc
|
| 553 |
+
|
| 554 |
+
|
| 555 |
+
def ulp(x: Union[ttnn.Tensor, torch.Tensor]) -> Union[ttnn.Tensor, torch.Tensor]:
|
| 556 |
+
"Return Unit of Least Precision for each element of a given tensor"
|
| 557 |
+
|
| 558 |
+
received_ttnn_input = False
|
| 559 |
+
if isinstance(x, ttnn.Tensor):
|
| 560 |
+
x = ttnn.to_torch(x)
|
| 561 |
+
received_ttnn_input = True
|
| 562 |
+
|
| 563 |
+
# Notes:
|
| 564 |
+
# - This should be identical to the definition of ULP by Goldberg
|
| 565 |
+
# "What every computer scientist should know about floating-point arithmetic"
|
| 566 |
+
# https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
|
| 567 |
+
# - We use torch.abs(x) to ensure symmetry ULP(-x) == ULP(x)
|
| 568 |
+
# - For x powers of 2, x + ULP(x) is not closest number but second closest (previous number is 2x closer)
|
| 569 |
+
# However, this avoids rounding-to-nearest-tie-to-even issues on addition (i.e. x + ULP(x) != x)
|
| 570 |
+
abs_x = torch.abs(x)
|
| 571 |
+
next = torch.nextafter(
|
| 572 |
+
abs_x, torch.tensor(math.inf, dtype=x.dtype)
|
| 573 |
+
) # 1 ULP ~ Difference between two consecutive floating point numbers
|
| 574 |
+
ulp_value = next - abs_x
|
| 575 |
+
|
| 576 |
+
# Special case: if abs_x == torch.finfo(x.dtype).max, then next == math.inf, which leads to ULP(x) == inf rather than finite number
|
| 577 |
+
# We fix this problem by manually calculating ULP at max value, and masking tensor when input == max
|
| 578 |
+
dtype_max = torch.finfo(x.dtype).max
|
| 579 |
+
max_epsilon = dtype_max - torch.nextafter(
|
| 580 |
+
torch.tensor(dtype_max, dtype=x.dtype), torch.tensor(-math.inf, dtype=x.dtype)
|
| 581 |
+
)
|
| 582 |
+
ulp_value = torch.where(abs_x == dtype_max, max_epsilon, ulp_value)
|
| 583 |
+
|
| 584 |
+
if received_ttnn_input: # Ensures that type(input) == type(output)
|
| 585 |
+
ulp_value = ttnn.from_torch(ulp_value)
|
| 586 |
+
|
| 587 |
+
return ulp_value
|
| 588 |
+
|
| 589 |
+
|
| 590 |
+
def comp_ulp(golden, calculated, ulp_threshold, allow_nonfinite=False):
|
| 591 |
+
"""
|
| 592 |
+
Compute absolute error between two tensors in Units of Least Precision (ULP)
|
| 593 |
+
"""
|
| 594 |
+
|
| 595 |
+
# If both tensors are empty, then we can return True
|
| 596 |
+
if torch.numel(golden) == 0 and torch.numel(calculated) == 0:
|
| 597 |
+
return True, "Both tensors are empty"
|
| 598 |
+
|
| 599 |
+
if not allow_nonfinite and not torch.all(torch.isfinite(calculated)):
|
| 600 |
+
return False, "Calculated tensor contains non-finite values"
|
| 601 |
+
|
| 602 |
+
if not _comp_nonfinite(golden, calculated):
|
| 603 |
+
return False, "Tensors are not finite at the same positions"
|
| 604 |
+
# nonfinite elements can interfere with ULP error calculation
|
| 605 |
+
# To avoid this, replace nan, +inf, -inf with 0
|
| 606 |
+
# (we have already checked that both tensors have the same nonfinite elements)
|
| 607 |
+
mask_finite = ~torch.isfinite(golden)
|
| 608 |
+
golden = golden.clone()
|
| 609 |
+
calculated = calculated.clone()
|
| 610 |
+
golden[mask_finite] = 0
|
| 611 |
+
calculated[mask_finite] = 0
|
| 612 |
+
|
| 613 |
+
# ULP is measured according to the golden tensor
|
| 614 |
+
# In most cases, data type of golden tensor should be the same as calculated tensor.
|
| 615 |
+
# However, in some cases, we may want to measure < 1 ULP differences, which requires golden tensor
|
| 616 |
+
# to have higher precision than calculated tensor.
|
| 617 |
+
# If we passed golden tensor to ulp() as is, we would get ULP of higher precision.
|
| 618 |
+
# e.g. ulp of float32 rather bfloat16 calculation, which would give us a wrong value.
|
| 619 |
+
ulp_value = ulp(golden.type(calculated.dtype))
|
| 620 |
+
|
| 621 |
+
if golden.dtype != calculated.dtype: # Note: assumes that golden has higher precision than calculated tensor
|
| 622 |
+
calculated = calculated.type(golden.dtype)
|
| 623 |
+
ulp_value = ulp_value.type(golden.dtype) # Convert ULP to higher precision (for sub-1 ULP measurements)
|
| 624 |
+
|
| 625 |
+
ulp_tensor = torch.abs(calculated - golden) / ulp_value
|
| 626 |
+
ulp_delta = torch.max(ulp_tensor)
|
| 627 |
+
within_threshold = ulp_delta <= ulp_threshold
|
| 628 |
+
message = f"Max ULP Delta: {ulp_delta}"
|
| 629 |
+
if not within_threshold:
|
| 630 |
+
ulp_index = torch.argmax(ulp_tensor)
|
| 631 |
+
ulp_index_tuple = tuple(int(idx) for idx in torch.unravel_index(ulp_index, golden.shape))
|
| 632 |
+
message += f" @ {list(ulp_index_tuple)} = |{calculated[ulp_index_tuple]} - {golden[ulp_index_tuple]}| / {ulp_value[ulp_index_tuple]}"
|
| 633 |
+
return (within_threshold, message)
|
| 634 |
+
|
| 635 |
+
|
| 636 |
+
def calculate_detailed_ulp_stats(expected, actual):
|
| 637 |
+
"""
|
| 638 |
+
Calculate detailed ULP statistics for analysis.
|
| 639 |
+
|
| 640 |
+
Returns:
|
| 641 |
+
dict: Dictionary with ULP statistics including max, mean, std, and percentiles
|
| 642 |
+
"""
|
| 643 |
+
if isinstance(actual, ttnn.Tensor):
|
| 644 |
+
actual = ttnn.to_torch(actual)
|
| 645 |
+
if isinstance(expected, ttnn.Tensor):
|
| 646 |
+
expected = ttnn.to_torch(expected)
|
| 647 |
+
|
| 648 |
+
# Convert to bfloat16 if not already
|
| 649 |
+
expected = expected.to(torch.bfloat16)
|
| 650 |
+
actual = actual.to(torch.bfloat16)
|
| 651 |
+
|
| 652 |
+
# Handle special cases
|
| 653 |
+
if torch.allclose(expected, actual, rtol=0, atol=0, equal_nan=True):
|
| 654 |
+
return {
|
| 655 |
+
"max_ulp": 0.0,
|
| 656 |
+
"mean_ulp": 0.0,
|
| 657 |
+
"median_ulp": 0.0,
|
| 658 |
+
"std_ulp": 0.0,
|
| 659 |
+
"p95_ulp": 0.0,
|
| 660 |
+
"p99_ulp": 0.0,
|
| 661 |
+
"perfect_matches": 1.0,
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
# Convert bfloat16 to uint16 representation for bit manipulation
|
| 665 |
+
expected_bits = expected.view(torch.int16).to(torch.int32)
|
| 666 |
+
actual_bits = actual.view(torch.int16).to(torch.int32)
|
| 667 |
+
|
| 668 |
+
# Handle sign differences
|
| 669 |
+
expected_sign = expected_bits < 0
|
| 670 |
+
actual_sign = actual_bits < 0
|
| 671 |
+
same_sign = expected_sign == actual_sign
|
| 672 |
+
|
| 673 |
+
# Calculate ULP differences
|
| 674 |
+
expected_abs_bits = torch.where(expected_sign, -expected_bits, expected_bits)
|
| 675 |
+
actual_abs_bits = torch.where(actual_sign, -actual_bits, actual_bits)
|
| 676 |
+
|
| 677 |
+
ulp_diff = torch.where(same_sign, torch.abs(expected_bits - actual_bits), expected_abs_bits + actual_abs_bits)
|
| 678 |
+
|
| 679 |
+
# Handle non-finite values
|
| 680 |
+
expected_finite = torch.isfinite(expected)
|
| 681 |
+
actual_finite = torch.isfinite(actual)
|
| 682 |
+
both_finite = expected_finite & actual_finite
|
| 683 |
+
|
| 684 |
+
ulp_diff = torch.where(both_finite, ulp_diff, torch.tensor(float("inf")))
|
| 685 |
+
|
| 686 |
+
# Handle same non-finite values
|
| 687 |
+
both_nan = torch.isnan(expected) & torch.isnan(actual)
|
| 688 |
+
both_posinf = torch.isposinf(expected) & torch.isposinf(actual)
|
| 689 |
+
both_neginf = torch.isneginf(expected) & torch.isneginf(actual)
|
| 690 |
+
same_nonfinite = both_nan | both_posinf | both_neginf
|
| 691 |
+
|
| 692 |
+
ulp_diff = torch.where(same_nonfinite, torch.tensor(0.0), ulp_diff)
|
| 693 |
+
|
| 694 |
+
# Calculate statistics only on finite ULP differences
|
| 695 |
+
finite_ulp = ulp_diff[torch.isfinite(ulp_diff)]
|
| 696 |
+
|
| 697 |
+
if len(finite_ulp) == 0:
|
| 698 |
+
return {
|
| 699 |
+
"max_ulp": float("inf"),
|
| 700 |
+
"mean_ulp": float("inf"),
|
| 701 |
+
"median_ulp": float("inf"),
|
| 702 |
+
"std_ulp": float("inf"),
|
| 703 |
+
"p95_ulp": float("inf"),
|
| 704 |
+
"p99_ulp": float("inf"),
|
| 705 |
+
"perfect_matches": 0.0,
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
finite_ulp_float = finite_ulp.float()
|
| 709 |
+
perfect_matches = (finite_ulp == 0).float().mean().item()
|
| 710 |
+
|
| 711 |
+
return {
|
| 712 |
+
"max_ulp": torch.max(finite_ulp).item(),
|
| 713 |
+
"mean_ulp": torch.mean(finite_ulp_float).item(),
|
| 714 |
+
"median_ulp": torch.median(finite_ulp_float).item(),
|
| 715 |
+
"std_ulp": torch.std(finite_ulp_float).item(),
|
| 716 |
+
"p95_ulp": torch.quantile(finite_ulp_float, 0.95).item(),
|
| 717 |
+
"p99_ulp": torch.quantile(finite_ulp_float, 0.99).item(),
|
| 718 |
+
"perfect_matches": perfect_matches,
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
|
| 722 |
+
def comp_allclose_and_pcc(golden, calculated, rtol=1e-05, atol=1e-08, pcc=0.99):
|
| 723 |
+
# 0-volume tensors are special because they don't have elements, so we can't compute PCC, etc.
|
| 724 |
+
# If one of the tensors is a 0-volume tensor, simply call torch.equal to check if they are equal
|
| 725 |
+
# (i.e. that both are 0-volume tensors and they have equal shapes).
|
| 726 |
+
if golden.numel() == 0 or calculated.numel() == 0:
|
| 727 |
+
return torch.equal(golden, calculated), f"{golden} != {calculated}"
|
| 728 |
+
|
| 729 |
+
if golden.dtype != calculated.dtype:
|
| 730 |
+
calculated = calculated.type(golden.dtype)
|
| 731 |
+
|
| 732 |
+
passing = True
|
| 733 |
+
output = ""
|
| 734 |
+
passing_allclose, output_allclose = comp_allclose(golden, calculated, rtol, atol)
|
| 735 |
+
passing &= passing_allclose
|
| 736 |
+
output += output_allclose
|
| 737 |
+
if torch.numel(golden) != 1:
|
| 738 |
+
passing_pcc, output_pcc = comp_pcc(golden, calculated, pcc)
|
| 739 |
+
passing &= passing_pcc
|
| 740 |
+
output += f", pcc={output_pcc}"
|
| 741 |
+
|
| 742 |
+
return passing, output
|
| 743 |
+
|
| 744 |
+
|
| 745 |
+
def comp_equal(golden, calculated):
|
| 746 |
+
if golden.dtype != calculated.dtype:
|
| 747 |
+
calculated = calculated.type(golden.dtype)
|
| 748 |
+
|
| 749 |
+
# If either tensor is zero-volume, broadcasting can still yield an empty delta and
|
| 750 |
+
# crash torch.max(); defer entirely to torch.equal (False on shape mismatch).
|
| 751 |
+
if golden.numel() == 0 or calculated.numel() == 0:
|
| 752 |
+
return torch.equal(golden, calculated), f"{golden} != {calculated}"
|
| 753 |
+
|
| 754 |
+
atol_delta = torch.max(torch.abs(golden - calculated)).item()
|
| 755 |
+
rtol_delta = torch.max(torch.abs(golden - calculated) / torch.abs(calculated)).item()
|
| 756 |
+
return (
|
| 757 |
+
torch.equal(golden, calculated),
|
| 758 |
+
f"Max ATOL Delta: {atol_delta}, Max RTOL Delta: {rtol_delta}",
|
| 759 |
+
)
|
| 760 |
+
|
| 761 |
+
|
| 762 |
+
def get_oom_of_float(float_lst):
|
| 763 |
+
"""
|
| 764 |
+
Given a list of floats, returns a list of the order or magnitudes
|
| 765 |
+
of the floats. Useful when you want to make sure that even if your
|
| 766 |
+
tt outputs don't match pytorch all that well, they are at least
|
| 767 |
+
on the same order of magnitude
|
| 768 |
+
"""
|
| 769 |
+
ooms = []
|
| 770 |
+
for el in float_lst:
|
| 771 |
+
str_el = str(el)
|
| 772 |
+
if "e" in str_el:
|
| 773 |
+
oom = int(str_el.split("e")[1])
|
| 774 |
+
elif str_el[:2] == "0.":
|
| 775 |
+
str_el = str_el.split(".")[1]
|
| 776 |
+
|
| 777 |
+
oom = -1
|
| 778 |
+
for e in str_el:
|
| 779 |
+
if e != "0":
|
| 780 |
+
break
|
| 781 |
+
oom -= 1
|
| 782 |
+
else:
|
| 783 |
+
oom = len(str_el.split(".")[0])
|
| 784 |
+
|
| 785 |
+
ooms.append(oom)
|
| 786 |
+
|
| 787 |
+
return ooms
|
| 788 |
+
|
| 789 |
+
|
| 790 |
+
def print_diff_argmax(a, b, annotation=""):
|
| 791 |
+
"""
|
| 792 |
+
Prints out the value of both tensors at a point where the absolute difference is the largest.
|
| 793 |
+
"""
|
| 794 |
+
absdiff = (a - b).abs()
|
| 795 |
+
argmax = absdiff.argmax().item()
|
| 796 |
+
diff = absdiff.reshape(-1)[argmax]
|
| 797 |
+
rela = a.abs() / (torch.max(a.abs(), b.abs()))
|
| 798 |
+
relb = b.abs() / (torch.max(a.abs(), b.abs()))
|
| 799 |
+
HT = a.shape[-2] // 32
|
| 800 |
+
WT = a.shape[-1] // 32
|
| 801 |
+
hwt = argmax // 1024
|
| 802 |
+
wt = hwt % WT
|
| 803 |
+
ht = hwt // WT
|
| 804 |
+
h = (argmax % 1024) // 32
|
| 805 |
+
w = (argmax % 1024) % 32
|
| 806 |
+
print(
|
| 807 |
+
"Abs diff=",
|
| 808 |
+
diff,
|
| 809 |
+
" at ",
|
| 810 |
+
argmax,
|
| 811 |
+
" --- ",
|
| 812 |
+
annotation,
|
| 813 |
+
"HTWT=",
|
| 814 |
+
ht,
|
| 815 |
+
wt,
|
| 816 |
+
"HW=",
|
| 817 |
+
h,
|
| 818 |
+
w,
|
| 819 |
+
)
|
| 820 |
+
print(" (a=", a.reshape(-1)[argmax].item(), ")")
|
| 821 |
+
print(" (b=", b.reshape(-1)[argmax].item(), ")")
|
| 822 |
+
print(" Rel a=", rela.reshape(-1)[argmax], " at ", argmax)
|
| 823 |
+
print(" Rel b=", relb.reshape(-1)[argmax], " at ", argmax)
|
| 824 |
+
return diff.item()
|
| 825 |
+
|
| 826 |
+
|
| 827 |
+
def print_diff_tt_pyt(a, b, annotation=""):
|
| 828 |
+
# first convert a pytorch tensor argument b to tt
|
| 829 |
+
padded_b = pad_weight(b)
|
| 830 |
+
pyt_a = tt2torch(a) # untilizes also
|
| 831 |
+
return print_diff_argmax(pyt_a, padded_b, annotation)
|
| 832 |
+
|
| 833 |
+
|
| 834 |
+
def ttP(x, count=4, offset=0, stride=1):
|
| 835 |
+
if type(x) == torch.Tensor:
|
| 836 |
+
t1 = x.reshape(-1)
|
| 837 |
+
else:
|
| 838 |
+
tt_out = x.cpu()
|
| 839 |
+
torch_out = untilize(tt_out.to_torch())
|
| 840 |
+
t1 = torch_out.reshape(-1)
|
| 841 |
+
print("Tensor vals: (", end="")
|
| 842 |
+
for j in range(offset, offset + count * stride, stride):
|
| 843 |
+
print(t1[j].item(), " ", end="")
|
| 844 |
+
print(")")
|
| 845 |
+
|
| 846 |
+
|
| 847 |
+
### Conv related helpers ###
|
| 848 |
+
def read_conv_act_into_mm_act_block(
|
| 849 |
+
conv_act,
|
| 850 |
+
act_address_map_index,
|
| 851 |
+
address_map,
|
| 852 |
+
address_map_this_block_size,
|
| 853 |
+
act_block_h,
|
| 854 |
+
act_block_w,
|
| 855 |
+
):
|
| 856 |
+
mm_act_block_shape = [1, 1, act_block_h * 32, act_block_w * 32]
|
| 857 |
+
mm_act_block_size = act_block_h * act_block_w * 1024
|
| 858 |
+
mm_act_block = torch.zeros(mm_act_block_size, dtype=torch.bfloat16).float()
|
| 859 |
+
for i in range(0, address_map_this_block_size, 4):
|
| 860 |
+
src_address = address_map[act_address_map_index]
|
| 861 |
+
dst_address = address_map[act_address_map_index + 1]
|
| 862 |
+
read_size = address_map[act_address_map_index + 2]
|
| 863 |
+
pad = address_map[act_address_map_index + 3]
|
| 864 |
+
for s in range(read_size):
|
| 865 |
+
assert dst_address + s < mm_act_block_size
|
| 866 |
+
if pad:
|
| 867 |
+
mm_act_block[dst_address + s] = 0
|
| 868 |
+
else:
|
| 869 |
+
assert src_address + s < len(conv_act)
|
| 870 |
+
mm_act_block[dst_address + s] = conv_act[src_address + s]
|
| 871 |
+
act_address_map_index += 4
|
| 872 |
+
return (mm_act_block.reshape(mm_act_block_shape), act_address_map_index)
|
| 873 |
+
|
| 874 |
+
|
| 875 |
+
def read_conv_weight_into_mm_weight_block(
|
| 876 |
+
conv_weight,
|
| 877 |
+
weight_address_map_index,
|
| 878 |
+
weight_address_map,
|
| 879 |
+
weight_address_map_this_block_size,
|
| 880 |
+
weight_block_h,
|
| 881 |
+
weight_block_w,
|
| 882 |
+
):
|
| 883 |
+
mm_weight_block_shape = [1, 1, weight_block_h * 32, weight_block_w * 32]
|
| 884 |
+
mm_weight_block_size = weight_block_h * weight_block_w * 1024
|
| 885 |
+
mm_weight_block = torch.zeros(mm_weight_block_size, dtype=torch.bfloat16).float()
|
| 886 |
+
for i in range(0, weight_address_map_this_block_size, 4):
|
| 887 |
+
src_address = weight_address_map[weight_address_map_index]
|
| 888 |
+
dst_address = weight_address_map[weight_address_map_index + 1]
|
| 889 |
+
read_size = weight_address_map[weight_address_map_index + 2]
|
| 890 |
+
pad = weight_address_map[weight_address_map_index + 3]
|
| 891 |
+
for s in range(read_size):
|
| 892 |
+
assert dst_address + s < mm_weight_block_size
|
| 893 |
+
if pad:
|
| 894 |
+
mm_weight_block[dst_address + s] = 0
|
| 895 |
+
else:
|
| 896 |
+
assert src_address + s < len(conv_weight)
|
| 897 |
+
mm_weight_block[dst_address + s] = conv_weight[src_address + s]
|
| 898 |
+
weight_address_map_index += 4
|
| 899 |
+
return (mm_weight_block.reshape(mm_weight_block_shape), weight_address_map_index)
|
| 900 |
+
|
| 901 |
+
|
| 902 |
+
def blocked_mm_with_conv_act(
|
| 903 |
+
conv_act,
|
| 904 |
+
mm_weight,
|
| 905 |
+
act_address_map,
|
| 906 |
+
weight_address_map,
|
| 907 |
+
num_blocks_act_h,
|
| 908 |
+
num_blocks_act_w,
|
| 909 |
+
num_blocks_weight_w,
|
| 910 |
+
act_block_h,
|
| 911 |
+
act_block_w,
|
| 912 |
+
weight_block_w,
|
| 913 |
+
):
|
| 914 |
+
# act refers to conv activation tensor
|
| 915 |
+
# weight refers to conv weight tensor
|
| 916 |
+
mm_output_shape = [
|
| 917 |
+
1,
|
| 918 |
+
1,
|
| 919 |
+
num_blocks_act_h * act_block_h * 32,
|
| 920 |
+
num_blocks_weight_w * weight_block_w * 32,
|
| 921 |
+
]
|
| 922 |
+
ret = torch.zeros(mm_output_shape, dtype=torch.bfloat16).float()
|
| 923 |
+
mm_output_block_shape = [1, 1, act_block_h * 32, weight_block_w * 32]
|
| 924 |
+
act_address_map_index = 0
|
| 925 |
+
weight_address_map_index = 0
|
| 926 |
+
weight_block_h = act_block_w
|
| 927 |
+
num_groups = act_address_map[act_address_map_index]
|
| 928 |
+
assert num_groups == num_blocks_act_h * num_blocks_act_w * num_blocks_weight_w
|
| 929 |
+
weight_num_groups = act_address_map[weight_address_map_index]
|
| 930 |
+
assert weight_num_groups == num_groups
|
| 931 |
+
act_address_map_index += 1
|
| 932 |
+
weight_address_map_index += 1
|
| 933 |
+
for block_act_h in range(num_blocks_act_h):
|
| 934 |
+
# Reset weight (weight) to the starting tile in this column
|
| 935 |
+
for block_weight_w in range(num_blocks_weight_w):
|
| 936 |
+
output_block = torch.zeros(mm_output_block_shape, dtype=torch.bfloat16).float()
|
| 937 |
+
for block_act_w in range(num_blocks_act_w):
|
| 938 |
+
address_map_this_block_size = act_address_map[act_address_map_index]
|
| 939 |
+
act_address_map_index += 1
|
| 940 |
+
weight_address_map_this_block_size = weight_address_map[weight_address_map_index]
|
| 941 |
+
weight_address_map_index += 1
|
| 942 |
+
(mm_act_block, act_address_map_index) = read_conv_act_into_mm_act_block(
|
| 943 |
+
conv_act,
|
| 944 |
+
act_address_map_index,
|
| 945 |
+
act_address_map,
|
| 946 |
+
address_map_this_block_size,
|
| 947 |
+
act_block_h,
|
| 948 |
+
act_block_w,
|
| 949 |
+
)
|
| 950 |
+
(
|
| 951 |
+
mm_weight_block,
|
| 952 |
+
weight_address_map_index,
|
| 953 |
+
) = read_conv_weight_into_mm_weight_block(
|
| 954 |
+
mm_weight,
|
| 955 |
+
weight_address_map_index,
|
| 956 |
+
weight_address_map,
|
| 957 |
+
weight_address_map_this_block_size,
|
| 958 |
+
weight_block_h,
|
| 959 |
+
weight_block_w,
|
| 960 |
+
)
|
| 961 |
+
# Untilize weight block (this CPU reference does matmul on untilized blocks)
|
| 962 |
+
mm_weight_block = untilize(mm_weight_block)
|
| 963 |
+
for out_h_block in range(act_block_h * 32):
|
| 964 |
+
for out_w_block in range(weight_block_w * 32):
|
| 965 |
+
output_block[0][0][out_h_block][out_w_block] += torch.dot(
|
| 966 |
+
mm_act_block[0, 0, out_h_block, :].reshape(-1),
|
| 967 |
+
mm_weight_block[0, 0, :, out_w_block].reshape(-1),
|
| 968 |
+
)
|
| 969 |
+
start_oh = block_act_h * act_block_h * 32
|
| 970 |
+
start_ow = block_weight_w * weight_block_w * 32
|
| 971 |
+
end_oh = start_oh + (act_block_h * 32)
|
| 972 |
+
end_ow = start_ow + (weight_block_w * 32)
|
| 973 |
+
ret[0, 0, start_oh:end_oh, start_ow:end_ow] = output_block
|
| 974 |
+
|
| 975 |
+
return ret
|
| 976 |
+
|
| 977 |
+
|
| 978 |
+
def is_conv_supported_on_device(conv_params):
|
| 979 |
+
K, C, R, S, U, V, P_H, P_W, dilation, groups = [conv_params[i] for i in range(10)]
|
| 980 |
+
|
| 981 |
+
if K % 32 != 0 or dilation != 1 or groups != 1:
|
| 982 |
+
logger.warning("DOES NOT HAVE SUPPORT FOR Conv with following parameters -")
|
| 983 |
+
logger.warning(
|
| 984 |
+
"K="
|
| 985 |
+
+ str(K)
|
| 986 |
+
+ " C="
|
| 987 |
+
+ str(C)
|
| 988 |
+
+ " R="
|
| 989 |
+
+ str(R)
|
| 990 |
+
+ " S="
|
| 991 |
+
+ str(S)
|
| 992 |
+
+ " U="
|
| 993 |
+
+ str(U)
|
| 994 |
+
+ " V="
|
| 995 |
+
+ str(V)
|
| 996 |
+
+ " PH="
|
| 997 |
+
+ str(P_H)
|
| 998 |
+
+ " PW="
|
| 999 |
+
+ str(P_W)
|
| 1000 |
+
+ " dilation="
|
| 1001 |
+
+ str(dilation)
|
| 1002 |
+
+ " groups="
|
| 1003 |
+
+ str(groups)
|
| 1004 |
+
)
|
| 1005 |
+
return False
|
| 1006 |
+
|
| 1007 |
+
return True
|
| 1008 |
+
|
| 1009 |
+
|
| 1010 |
+
def is_x2_harvested(device):
|
| 1011 |
+
grid = device.compute_with_storage_grid_size()
|
| 1012 |
+
return device.arch() == Arch.WORMHOLE_B0 and (grid.x, grid.y) == (8, 7)
|
| 1013 |
+
|
| 1014 |
+
|
| 1015 |
+
def is_single_chip():
|
| 1016 |
+
return ttnn.GetNumAvailableDevices() == 1
|
| 1017 |
+
|
| 1018 |
+
|
| 1019 |
+
def is_blackhole():
|
| 1020 |
+
ARCH_NAME = ttnn.get_arch_name()
|
| 1021 |
+
return "blackhole" in ARCH_NAME
|
| 1022 |
+
|
| 1023 |
+
|
| 1024 |
+
def is_wormhole_b0():
|
| 1025 |
+
ARCH_NAME = ttnn.get_arch_name()
|
| 1026 |
+
return "wormhole_b0" in ARCH_NAME
|
| 1027 |
+
|
| 1028 |
+
|
| 1029 |
+
def is_watcher_enabled():
|
| 1030 |
+
watcher = os.environ.get("TT_METAL_WATCHER")
|
| 1031 |
+
lightweight_asserts = os.environ.get("TT_METAL_LIGHTWEIGHT_KERNEL_ASSERTS")
|
| 1032 |
+
return (watcher is not None and watcher != "") or lightweight_asserts == "1"
|
| 1033 |
+
|
| 1034 |
+
|
| 1035 |
+
def is_llk_assert_enabled():
|
| 1036 |
+
llk_assert = os.environ.get("TT_METAL_LLK_ASSERTS")
|
| 1037 |
+
return llk_assert == "1"
|
| 1038 |
+
|
| 1039 |
+
|
| 1040 |
+
def is_n300():
|
| 1041 |
+
return os.environ.get("MESH_DEVICE", "N150") == "N300"
|
| 1042 |
+
|
| 1043 |
+
|
| 1044 |
+
def is_slow_dispatch():
|
| 1045 |
+
return os.environ.get("TT_METAL_SLOW_DISPATCH_MODE") == "1"
|
| 1046 |
+
|
| 1047 |
+
|
| 1048 |
+
def ti_skip(condition, reason="Invalid test parameters"):
|
| 1049 |
+
return pytest.mark.skipif(condition, reason="Skipping unsupported case: " + reason)
|
| 1050 |
+
|
| 1051 |
+
|
| 1052 |
+
def skip_for_blackhole(reason_str="not a blackhole test"):
|
| 1053 |
+
return ti_skip(is_blackhole(), reason=reason_str)
|
| 1054 |
+
|
| 1055 |
+
|
| 1056 |
+
def skip_for_wormhole_b0(reason_str="not a wormhole test"):
|
| 1057 |
+
return ti_skip(is_wormhole_b0(), reason=reason_str)
|
| 1058 |
+
|
| 1059 |
+
|
| 1060 |
+
def skip_with_watcher(reason_str="Test is not passing with watcher enabled"):
|
| 1061 |
+
return ti_skip(is_watcher_enabled(), reason=reason_str)
|
| 1062 |
+
|
| 1063 |
+
|
| 1064 |
+
def skip_with_llk_assert(reason_str="Test is not passing with LLK asserts enabled"):
|
| 1065 |
+
return ti_skip(is_llk_assert_enabled(), reason=reason_str)
|
| 1066 |
+
|
| 1067 |
+
|
| 1068 |
+
def run_for_blackhole(reason_str="only runs for Blackhole"):
|
| 1069 |
+
return ti_skip(not is_blackhole(), reason=reason_str)
|
| 1070 |
+
|
| 1071 |
+
|
| 1072 |
+
def run_for_wormhole_b0(reason_str="only runs for Wormhole B0"):
|
| 1073 |
+
return ti_skip(not is_wormhole_b0(), reason=reason_str)
|
| 1074 |
+
|
| 1075 |
+
|
| 1076 |
+
def run_for_wormhole_b0_or_blackhole(reason_str="only runs for Wormhole B0 or Blackhole"):
|
| 1077 |
+
return ti_skip(not (is_wormhole_b0() or is_blackhole()), reason=reason_str)
|
| 1078 |
+
|
| 1079 |
+
|
| 1080 |
+
def run_for_n_dev(n, reason_str="Test is not meant for this number of devices"):
|
| 1081 |
+
return ti_skip(ttnn.get_num_devices() != n, reason=reason_str)
|
| 1082 |
+
|
| 1083 |
+
|
| 1084 |
+
def skip_for_n_dev(n, reason_str="Test is not meant for this number of devices"):
|
| 1085 |
+
return ti_skip(ttnn.get_num_devices() == n, reason=reason_str)
|
| 1086 |
+
|
| 1087 |
+
|
| 1088 |
+
def skip_for_n_or_less_dev(n, reason_str="Test is not meant for this number of devices"):
|
| 1089 |
+
return ti_skip(ttnn.get_num_devices() <= n, reason=reason_str)
|
| 1090 |
+
|
| 1091 |
+
|
| 1092 |
+
def skip_for_slow_dispatch(reason_str="not working for slow dispatch"):
|
| 1093 |
+
return ti_skip(is_slow_dispatch(), reason=reason_str)
|
| 1094 |
+
|
| 1095 |
+
|
| 1096 |
+
def ttl_complex_2_torch_complex(tt_tensor):
|
| 1097 |
+
torch_tensor = tt2torch_tensor(tt_tensor)
|
| 1098 |
+
|
| 1099 |
+
# extract real and imag parts of the complex tensor
|
| 1100 |
+
real = torch_tensor[:, :, :, : torch_tensor.shape[-1] // 2].to(torch.bfloat16).to(torch.float)
|
| 1101 |
+
imag = torch_tensor[:, :, :, torch_tensor.shape[-1] // 2 :].to(torch.bfloat16).to(torch.float)
|
| 1102 |
+
|
| 1103 |
+
# create torch complex tensor
|
| 1104 |
+
result = torch.complex(real, imag)
|
| 1105 |
+
return result
|
| 1106 |
+
|
| 1107 |
+
|
| 1108 |
+
def pad_and_fold_conv_filters_for_unity_stride(filter_pyt_nchw_tensor, stride_h, stride_w):
|
| 1109 |
+
assert stride_h == stride_w
|
| 1110 |
+
assert filter_pyt_nchw_tensor.shape[2] == filter_pyt_nchw_tensor.shape[3]
|
| 1111 |
+
# Fold activation for unity stride
|
| 1112 |
+
# Pad channel size to 4. This is to make sure L1 read addresses are 16 bit aligned
|
| 1113 |
+
C = _nearest_y(filter_pyt_nchw_tensor.shape[1], 4)
|
| 1114 |
+
# Pad filter to nearest stride
|
| 1115 |
+
Padded_filter_height = _nearest_y(filter_pyt_nchw_tensor.shape[2], stride_h)
|
| 1116 |
+
Padded_filter_width = _nearest_y(filter_pyt_nchw_tensor.shape[3], stride_w)
|
| 1117 |
+
filter_pyt_padded = torch.nn.functional.pad(
|
| 1118 |
+
filter_pyt_nchw_tensor,
|
| 1119 |
+
(
|
| 1120 |
+
0,
|
| 1121 |
+
Padded_filter_width - filter_pyt_nchw_tensor.shape[3],
|
| 1122 |
+
0,
|
| 1123 |
+
Padded_filter_height - filter_pyt_nchw_tensor.shape[2],
|
| 1124 |
+
0,
|
| 1125 |
+
C - filter_pyt_nchw_tensor.shape[1],
|
| 1126 |
+
),
|
| 1127 |
+
)
|
| 1128 |
+
# Fold filter for unity stride.
|
| 1129 |
+
filter_pyt_padded_folded = torch.zeros(
|
| 1130 |
+
[
|
| 1131 |
+
filter_pyt_padded.shape[0],
|
| 1132 |
+
C * stride_h * stride_w,
|
| 1133 |
+
(int)(filter_pyt_padded.shape[2] / stride_h),
|
| 1134 |
+
(int)(filter_pyt_padded.shape[3] / stride_w),
|
| 1135 |
+
]
|
| 1136 |
+
)
|
| 1137 |
+
for h in range(0, filter_pyt_padded.shape[2], stride_h):
|
| 1138 |
+
for w in range(0, filter_pyt_padded.shape[3], stride_w):
|
| 1139 |
+
folded_h = (int)(h / stride_h)
|
| 1140 |
+
folded_w = (int)(w / stride_w)
|
| 1141 |
+
for i in range(4):
|
| 1142 |
+
start_c = i * C
|
| 1143 |
+
filter_pyt_padded_folded[:, start_c : start_c + C, folded_h, folded_w] = filter_pyt_padded[
|
| 1144 |
+
:, :, h + (int)(i / stride_w), w + (int)(i % stride_w)
|
| 1145 |
+
]
|
| 1146 |
+
return filter_pyt_padded_folded
|
| 1147 |
+
|
| 1148 |
+
|
| 1149 |
+
# produces a tensor where each element in a page is the page number
|
| 1150 |
+
# this tensor is easy to debug and visualize
|
| 1151 |
+
def get_debug_tensor(num_pages_width, num_pages_height, dtype, page_width=32, page_height=32):
|
| 1152 |
+
torch_tensor = None
|
| 1153 |
+
for row_idx in range(0, int(num_pages_height)):
|
| 1154 |
+
tile_row = None
|
| 1155 |
+
for col_idx in range(0, int(num_pages_width)):
|
| 1156 |
+
tile_idx = col_idx + num_pages_width * row_idx
|
| 1157 |
+
tile = torch.full((1, 1, page_width, page_height), tile_idx + 1, dtype=dtype)
|
| 1158 |
+
if tile_row == None:
|
| 1159 |
+
tile_row = tile
|
| 1160 |
+
else:
|
| 1161 |
+
tile_row = torch.cat((tile_row, tile), 3)
|
| 1162 |
+
if torch_tensor == None:
|
| 1163 |
+
torch_tensor = tile_row
|
| 1164 |
+
else:
|
| 1165 |
+
torch_tensor = torch.cat((torch_tensor, tile_row), 2)
|
| 1166 |
+
|
| 1167 |
+
return torch_tensor
|
| 1168 |
+
|
| 1169 |
+
|
| 1170 |
+
# ── transformers 5.x Cache API compatibility ────────────────────────────────
|
| 1171 |
+
# transformers 5.x removed the legacy Cache API: DynamicCache no longer exposes
|
| 1172 |
+
# from_legacy_cache / to_legacy_cache / key_cache / value_cache (per-layer KV now
|
| 1173 |
+
# lives at cache.layers[i].keys/.values). These helpers work on both 4.x and 5.x.
|
| 1174 |
+
def hf_cache_layer_kv(cache, layer_idx):
|
| 1175 |
+
"""Return (key, value) tensors for a layer of a transformers Cache.
|
| 1176 |
+
|
| 1177 |
+
Handles the legacy tuple-of-tuples past_key_values, transformers <5 Cache
|
| 1178 |
+
(key_cache/value_cache), and transformers >=5 Cache (layers[i].keys/.values).
|
| 1179 |
+
"""
|
| 1180 |
+
if isinstance(cache, (tuple, list)): # legacy tuple-of-tuples past_key_values
|
| 1181 |
+
return cache[layer_idx][0], cache[layer_idx][1]
|
| 1182 |
+
if hasattr(cache, "key_cache"): # transformers < 5.x Cache
|
| 1183 |
+
return cache.key_cache[layer_idx], cache.value_cache[layer_idx]
|
| 1184 |
+
layer = cache.layers[layer_idx] # transformers >= 5.x Cache
|
| 1185 |
+
return layer.keys, layer.values
|
| 1186 |
+
|
| 1187 |
+
|
| 1188 |
+
def hf_cache_to_legacy(cache):
|
| 1189 |
+
"""Export a transformers Cache to the legacy tuple-of-(key, value) format."""
|
| 1190 |
+
if hasattr(cache, "to_legacy_cache"): # transformers < 5.x
|
| 1191 |
+
return cache.to_legacy_cache()
|
| 1192 |
+
return tuple((layer.keys, layer.values) for layer in cache.layers) # transformers >= 5.x
|
| 1193 |
+
|
| 1194 |
+
|
| 1195 |
+
def hf_dynamic_cache_from_legacy(layer_kvs):
|
| 1196 |
+
"""Build a transformers DynamicCache from per-layer (key, value) tuples."""
|
| 1197 |
+
from transformers import DynamicCache
|
| 1198 |
+
|
| 1199 |
+
layer_kvs = tuple(layer_kvs)
|
| 1200 |
+
if hasattr(DynamicCache, "from_legacy_cache"): # transformers < 5.x
|
| 1201 |
+
return DynamicCache.from_legacy_cache(layer_kvs)
|
| 1202 |
+
return DynamicCache(layer_kvs) # transformers >= 5.x
|
| 1203 |
+
|
| 1204 |
+
|
| 1205 |
+
def hf_cache_num_layers(cache):
|
| 1206 |
+
"""Number of populated layers in a transformers Cache (version-tolerant)."""
|
| 1207 |
+
return len(cache.key_cache) if hasattr(cache, "key_cache") else len(cache.layers)
|
| 1208 |
+
|
| 1209 |
+
|
| 1210 |
+
def hf_empty_encoder_decoder_cache():
|
| 1211 |
+
"""Create an empty transformers EncoderDecoderCache (version-tolerant)."""
|
| 1212 |
+
from transformers import DynamicCache, EncoderDecoderCache
|
| 1213 |
+
|
| 1214 |
+
if hasattr(EncoderDecoderCache, "from_legacy_cache"): # transformers < 5.x
|
| 1215 |
+
return EncoderDecoderCache.from_legacy_cache(None)
|
| 1216 |
+
return EncoderDecoderCache(DynamicCache(), DynamicCache()) # transformers >= 5.x
|
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/demos/gpt_oss/tt/ccl.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
import ttnn
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class CCLManager:
|
| 8 |
+
def __init__(self, mesh_device, num_links, topology=ttnn.Topology.Ring):
|
| 9 |
+
self.mesh_device = mesh_device
|
| 10 |
+
self.num_links = num_links
|
| 11 |
+
self.topology = topology
|
| 12 |
+
|
| 13 |
+
# Cache for ping pong buffers: key = (shape_tuple, dim, mesh_axis), value = [buffer1, buffer2]
|
| 14 |
+
self._ping_pong_buffer_cache = {}
|
| 15 |
+
self._ping_pong_buffer_indices = {}
|
| 16 |
+
|
| 17 |
+
# Setup semaphores
|
| 18 |
+
self._init_subdevice()
|
| 19 |
+
|
| 20 |
+
# Initialize semaphores for reduce scatter and all gather
|
| 21 |
+
self._init_semaphores()
|
| 22 |
+
self.rs_ping_pong_idx = 0
|
| 23 |
+
self.ag_ping_pong_idx = 0
|
| 24 |
+
self.barrier_idx = 0
|
| 25 |
+
|
| 26 |
+
def _init_subdevice(self):
|
| 27 |
+
compute_grid_size = ttnn.CoreCoord(8, 8)
|
| 28 |
+
self.ccl_cores = ttnn.CoreRangeSet(
|
| 29 |
+
{ttnn.CoreRange(ttnn.CoreCoord(0, 0), ttnn.CoreCoord(compute_grid_size.x - 1, compute_grid_size.y - 1))}
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
_worker_sub_device = ttnn.SubDevice(
|
| 33 |
+
[
|
| 34 |
+
self.ccl_cores,
|
| 35 |
+
]
|
| 36 |
+
)
|
| 37 |
+
self.ccl_sub_device_id = ttnn.SubDeviceId(0)
|
| 38 |
+
|
| 39 |
+
def _init_semaphores(self):
|
| 40 |
+
# Initialize semaphores for reduce scatter ping pong
|
| 41 |
+
rs_n_sems = 3 * 2 # 3 semaphores * 2 for ping pong
|
| 42 |
+
self.rs_ping_pong_semaphores = [
|
| 43 |
+
ttnn.create_global_semaphore(self.mesh_device, self.ccl_cores, 0) for _ in range(rs_n_sems)
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
# Initialize semaphores for all gather ping pong
|
| 47 |
+
ag_n_sems = 2 * 2 # 2 semaphores * 2 for ping pong (2 buffers)
|
| 48 |
+
self.ag_ping_pong_semaphores = [
|
| 49 |
+
ttnn.create_global_semaphore(self.mesh_device, self.ccl_cores, 0) for _ in range(ag_n_sems)
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
# Initialize barrier semaphores
|
| 53 |
+
barrier_ns_sems = 2 * 1
|
| 54 |
+
self.barrier_semaphore = [
|
| 55 |
+
ttnn.create_global_semaphore(self.mesh_device, self.ccl_cores, 0) for _ in range(barrier_ns_sems)
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
def get_rs_ping_pong_semaphore(self):
|
| 59 |
+
"""
|
| 60 |
+
Get semaphores for reduce scatter ping pong operations.
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
List of 3 semaphores for the current ping pong cycle
|
| 64 |
+
"""
|
| 65 |
+
cur_idx = self.rs_ping_pong_idx
|
| 66 |
+
n_sems = 3
|
| 67 |
+
self.rs_ping_pong_idx = (cur_idx + 1) % 2
|
| 68 |
+
return self.rs_ping_pong_semaphores[cur_idx * n_sems : (cur_idx + 1) * n_sems]
|
| 69 |
+
|
| 70 |
+
def get_ag_ping_pong_semaphore(self):
|
| 71 |
+
"""
|
| 72 |
+
Get semaphores for all gather ping pong operations.
|
| 73 |
+
|
| 74 |
+
Returns:
|
| 75 |
+
List of 3 semaphores for the current ping pong cycle
|
| 76 |
+
"""
|
| 77 |
+
cur_idx = self.ag_ping_pong_idx
|
| 78 |
+
n_sems = 2
|
| 79 |
+
self.ag_ping_pong_idx = (cur_idx + 1) % 2
|
| 80 |
+
return self.ag_ping_pong_semaphores[cur_idx * n_sems : (cur_idx + 1) * n_sems]
|
| 81 |
+
|
| 82 |
+
def get_barrier_semaphore(self):
|
| 83 |
+
"""
|
| 84 |
+
Get semaphores for barrier operations.
|
| 85 |
+
"""
|
| 86 |
+
cur_idx = self.barrier_idx
|
| 87 |
+
self.barrier_idx = (cur_idx + 1) % 2
|
| 88 |
+
return self.barrier_semaphore[cur_idx]
|
| 89 |
+
|
| 90 |
+
def reset_global_semaphores(self):
|
| 91 |
+
"""Reset all global semaphores to 0"""
|
| 92 |
+
for sem in self.rs_ping_pong_semaphores:
|
| 93 |
+
ttnn.reset_global_semaphore_value(sem, 0)
|
| 94 |
+
for sem in self.ag_ping_pong_semaphores:
|
| 95 |
+
ttnn.reset_global_semaphore_value(sem, 0)
|
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:745c72341b2d4058b7a08d6e8ed95d0e493586d5b4d3b78d75fc973d5fe8db51","size":856,"annotations":{"io.containerd.image.name":"docker.io/tt-model/nemotron-3.5-lightning-30b-a3b-nvfp4-tt-p300x2-vllm026:745c72341b2d","org.opencontainers.image.ref.name":"745c72341b2d"}}]}
|
image/manifest.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
[{"Config":"blobs/sha256/f2b4df69449c2b17e203e5c5c45a7eafb9e17691b52baee2b55f054f2b69b0a3","RepoTags":["tt-model/nemotron-3.5-lightning-30b-a3b-nvfp4-tt-p300x2-vllm026:745c72341b2d"],"Layers":["blobs/sha256/e51aee9c82ec5dd5ba2add49c45c6d85d460512757e2615b69bcdf9469c7cb58","blobs/sha256/f95188533c275c88f64d0ea2af69d9c5bd5f6e8ae6228a72f7ca1a474391490e","blobs/sha256/44958d2683cf8259e11d1fc5969eebacbd9182cba8c61a944d02ad6f9d0208f5","blobs/sha256/f4403345261d6a0a456e52ecb4bfcefafc8f60fe769f7a158c108d85f14a8442","blobs/sha256/0c3f1a4dac205920b50ede37e2a71089fc73c112353f1817ba049a573e3d85b0","blobs/sha256/7a4a65a66726b5cae2adac1ecf91f6ef3279e817aa14905130b096af0713543b","blobs/sha256/a0a726a404e7bde4fd5f62328276f46da44372697b7f29f475ab73c4f3981c02","blobs/sha256/1d138841c7fb11420920117c66dc923e100debfad7e2715f74e3b402c7a2b42f","blobs/sha256/2e0cfc89ff175fb9daec4a421637369dce62441303743f043c88fa8e4d5c84ca","blobs/sha256/3a03bff8c5a73a353bad8ae751e0703b43b10a63ccc7bb88c69f2559a04b3c25","blobs/sha256/45d1acbd1c6ba16132bd0ed960cdd7869ce831966b46cd7054c5c6db89f50d7c","blobs/sha256/633e13d0cbce3c46913bfa2b768a5b2d8485589423834ca7e36e06be81df2b5f","blobs/sha256/b7ff18b6e28d541b44e6b3859161b646143687aee783a5140bba7c82eb6f8dc3","blobs/sha256/87f9f907633999cc2166f3a11c05ef6c65bcdba7d728b5f46e54e8ae8e35a655","blobs/sha256/bc24046f95cf467afdbb275d4d7115fa2898918e49ef5a38b6f9d4e0369e7931","blobs/sha256/7de6f03cf8ad41347dfcd688ad0e2fd2b9f9bda48f10225e50418c2f2003c515","blobs/sha256/7f224de3c4cc07456c8a346f6d71cd8270921b772536691313ded51c82ce5b09","blobs/sha256/625235333d0c70632afb385283d5a06a49496d65f369b50a319656e103fe2281","blobs/sha256/4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1","blobs/sha256/66f86dec5bd03c37f55c9103d282115cfd2097437c2967808baa3532558a39bb","blobs/sha256/9ad78b76ad8377c5e1e7270d222f4ba7ebb2a4d8c6f8144fc03a36d317b2b852","blobs/sha256/c274f728a5846ebb5617e94318b4b25ed1df2e5aa7d63af334e0e4dd4c937424"]}]
|
image/oci-layout
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"imageLayoutVersion":"1.0.0"}
|
requirements.lock
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
agent-detector==2.0.0
|
| 2 |
+
aiohappyeyeballs==2.7.1
|
| 3 |
+
aiohttp==3.14.3
|
| 4 |
+
aiosignal==1.4.0
|
| 5 |
+
annotated-doc==0.0.5
|
| 6 |
+
annotated-types==0.8.0
|
| 7 |
+
anthropic==1.5.0
|
| 8 |
+
anyio==4.15.1
|
| 9 |
+
apache-tvm-ffi==0.1.10
|
| 10 |
+
astor==0.8.1
|
| 11 |
+
attrs==26.1.0
|
| 12 |
+
blake3==1.0.9
|
| 13 |
+
cachetools==7.1.8
|
| 14 |
+
cbor2==6.1.4
|
| 15 |
+
certifi==2026.7.22
|
| 16 |
+
cffi==2.1.1
|
| 17 |
+
cfgv==3.5.0
|
| 18 |
+
charset-normalizer==3.5.1
|
| 19 |
+
click==8.5.0
|
| 20 |
+
cloudpickle==3.1.2
|
| 21 |
+
compressed-tensors==0.17.0
|
| 22 |
+
contourpy==1.3.3
|
| 23 |
+
cryptography==50.0.1
|
| 24 |
+
cuda-bindings==13.4.1
|
| 25 |
+
cuda-core==1.2.0
|
| 26 |
+
cuda-pathfinder==1.8.1
|
| 27 |
+
cuda-python==13.4.1
|
| 28 |
+
cuda-tile==1.6.0
|
| 29 |
+
cycler==0.12.1
|
| 30 |
+
depyf==0.20.0
|
| 31 |
+
detect-installer==0.2.1
|
| 32 |
+
dill==0.4.1
|
| 33 |
+
distlib==0.4.3
|
| 34 |
+
distro==1.9.0
|
| 35 |
+
dnspython==2.8.0
|
| 36 |
+
docstring_parser==0.18.0
|
| 37 |
+
einops==0.8.2
|
| 38 |
+
elastic-transport==9.4.2
|
| 39 |
+
elasticsearch==9.5.1
|
| 40 |
+
email-validator==2.3.0
|
| 41 |
+
fastapi==0.136.3
|
| 42 |
+
fastapi-cli==0.0.32
|
| 43 |
+
fastapi-cloud-cli==0.26.0
|
| 44 |
+
fastar==0.12.0
|
| 45 |
+
fastsafetensors==0.4.0
|
| 46 |
+
filelock==3.32.6
|
| 47 |
+
flashinfer-python==0.6.14
|
| 48 |
+
fonttools==4.65.0
|
| 49 |
+
frozenlist==1.8.0
|
| 50 |
+
fsspec==2026.7.0
|
| 51 |
+
googleapis-common-protos==1.75.3
|
| 52 |
+
graphviz==0.21
|
| 53 |
+
grpcio==1.83.1
|
| 54 |
+
h11==0.16.0
|
| 55 |
+
hf-xet==1.6.0
|
| 56 |
+
httpcore==1.0.9
|
| 57 |
+
httpcore2==2.12.0
|
| 58 |
+
httptools==0.8.0
|
| 59 |
+
httpx==0.28.1
|
| 60 |
+
httpx2==2.12.0
|
| 61 |
+
huggingface_hub==1.16.1
|
| 62 |
+
humming-kernels==0.1.10
|
| 63 |
+
identify==2.6.19
|
| 64 |
+
idna==3.19
|
| 65 |
+
ijson==3.5.1
|
| 66 |
+
interegular==0.3.3
|
| 67 |
+
Jinja2==3.1.6
|
| 68 |
+
jiter==0.16.0
|
| 69 |
+
jmespath==1.1.0
|
| 70 |
+
jsonschema==4.26.0
|
| 71 |
+
jsonschema-specifications==2025.9.1
|
| 72 |
+
kiwisolver==1.5.1
|
| 73 |
+
lark==1.2.2
|
| 74 |
+
linkify-it-py==2.2.0
|
| 75 |
+
llguidance==1.7.6
|
| 76 |
+
llvmlite==0.47.0
|
| 77 |
+
lm-format-enforcer==0.11.3
|
| 78 |
+
loguru==0.6.0
|
| 79 |
+
markdown-it-py==4.2.0
|
| 80 |
+
MarkupSafe==3.0.3
|
| 81 |
+
matplotlib==3.11.1
|
| 82 |
+
mcp==2.2.0
|
| 83 |
+
mcp-types==2.2.0
|
| 84 |
+
mdit-py-plugins==0.6.1
|
| 85 |
+
mdurl==0.1.2
|
| 86 |
+
mistral_common==1.11.7
|
| 87 |
+
ml_dtypes==0.6.0
|
| 88 |
+
model-hosting-container-standards==0.1.16
|
| 89 |
+
mpmath==1.3.0
|
| 90 |
+
msgspec==0.21.1
|
| 91 |
+
multidict==6.8.0
|
| 92 |
+
networkx==3.6.1
|
| 93 |
+
ninja==1.13.2
|
| 94 |
+
nodeenv==1.10.0
|
| 95 |
+
numba==0.65.0
|
| 96 |
+
numpy==2.3.5
|
| 97 |
+
nvidia-cuda-cccl==13.3.4.2.1
|
| 98 |
+
nvidia-cuda-crt==13.4.59
|
| 99 |
+
nvidia-cuda-nvcc==13.4.59
|
| 100 |
+
nvidia-cuda-nvdisasm==13.4.49
|
| 101 |
+
nvidia-cuda-nvrtc==13.4.59
|
| 102 |
+
nvidia-cuda-runtime==13.4.49
|
| 103 |
+
nvidia-cudnn-frontend==1.28.0
|
| 104 |
+
nvidia-cutlass-dsl==4.6.0
|
| 105 |
+
nvidia-cutlass-dsl-libs-base==4.6.0
|
| 106 |
+
nvidia-cutlass-dsl-libs-core==4.6.0
|
| 107 |
+
nvidia-cutlass-dsl-libs-cu12==4.6.0
|
| 108 |
+
nvidia-cutlass-dsl-libs-cu13==4.6.0
|
| 109 |
+
nvidia-ml-py==13.610.43
|
| 110 |
+
nvidia-nvvm==13.4.59
|
| 111 |
+
nvtx==0.2.15
|
| 112 |
+
openai==3.13.0
|
| 113 |
+
openai-harmony==0.0.8
|
| 114 |
+
opencv-python-headless==4.11.0.86
|
| 115 |
+
opentelemetry-api==1.44.0
|
| 116 |
+
opentelemetry-exporter-otlp==1.44.0
|
| 117 |
+
opentelemetry-exporter-otlp-proto-common==1.44.0
|
| 118 |
+
opentelemetry-exporter-otlp-proto-grpc==1.44.0
|
| 119 |
+
opentelemetry-exporter-otlp-proto-http==1.44.0
|
| 120 |
+
opentelemetry-proto==1.44.0
|
| 121 |
+
opentelemetry-sdk==1.44.0
|
| 122 |
+
opentelemetry-semantic-conventions==0.65b0
|
| 123 |
+
opentelemetry-semantic-conventions-ai==0.5.1
|
| 124 |
+
outlines_core==0.2.14
|
| 125 |
+
packaging==26.3
|
| 126 |
+
pandas==3.0.5
|
| 127 |
+
partial-json-parser==0.2.1.1.post7
|
| 128 |
+
pillow==12.3.0
|
| 129 |
+
platformdirs==4.11.8
|
| 130 |
+
pre_commit==4.6.2
|
| 131 |
+
prometheus-fastapi-instrumentator==8.1.0
|
| 132 |
+
prometheus_client==0.26.0
|
| 133 |
+
propcache==0.5.2
|
| 134 |
+
protobuf==6.33.6
|
| 135 |
+
psutil==7.2.2
|
| 136 |
+
py-cpuinfo==9.0.0
|
| 137 |
+
pybase64==1.5.0
|
| 138 |
+
pycountry==26.2.16
|
| 139 |
+
pycparser==3.0
|
| 140 |
+
pydantic==2.13.5
|
| 141 |
+
pydantic-extra-types==2.11.1
|
| 142 |
+
pydantic-settings==2.15.0
|
| 143 |
+
pydantic_core==2.46.5
|
| 144 |
+
pyelftools==0.33
|
| 145 |
+
Pygments==2.21.0
|
| 146 |
+
PyJWT==2.14.0
|
| 147 |
+
pyluwen==0.9.0
|
| 148 |
+
pynvvideocodec==2.0.4
|
| 149 |
+
pyparsing==3.3.2
|
| 150 |
+
python-dateutil==2.9.0.post0
|
| 151 |
+
python-discovery==1.6.0
|
| 152 |
+
python-dotenv==1.2.3
|
| 153 |
+
python-json-logger==4.2.0
|
| 154 |
+
python-multipart==0.0.32
|
| 155 |
+
PyYAML==6.0.3
|
| 156 |
+
pyzmq==27.2.0
|
| 157 |
+
quack-kernels==0.6.3
|
| 158 |
+
referencing==0.37.0
|
| 159 |
+
regex==2026.9.10
|
| 160 |
+
requests==2.34.2
|
| 161 |
+
rich==15.0.0
|
| 162 |
+
rich-toolkit==0.20.5
|
| 163 |
+
rignore==0.8.1
|
| 164 |
+
rpds-py==2026.6.3
|
| 165 |
+
safetensors==0.8.0
|
| 166 |
+
seaborn==0.13.2
|
| 167 |
+
sentencepiece==0.2.2
|
| 168 |
+
sentry-sdk==2.69.1
|
| 169 |
+
setproctitle==1.3.7
|
| 170 |
+
setuptools==80.10.2
|
| 171 |
+
setuptools-scm==8.1.0
|
| 172 |
+
shellingham==1.5.4
|
| 173 |
+
six==1.17.0
|
| 174 |
+
sniffio==1.3.1
|
| 175 |
+
sse-starlette==3.4.11
|
| 176 |
+
starlette==1.6.0
|
| 177 |
+
supervisor==4.3.0
|
| 178 |
+
sympy==1.14.0
|
| 179 |
+
tabulate==0.10.0
|
| 180 |
+
tblib==3.2.2
|
| 181 |
+
textual==8.2.8
|
| 182 |
+
tiktoken==0.14.0
|
| 183 |
+
tilelang==0.1.9
|
| 184 |
+
tokenizers==0.22.2
|
| 185 |
+
tokenspeed-mla==0.1.8
|
| 186 |
+
tokenspeed-triton==3.8.10.post20260906
|
| 187 |
+
tomli==2.4.1
|
| 188 |
+
torch==2.11.0+cpu
|
| 189 |
+
torch_c_dlpack_ext==0.1.5
|
| 190 |
+
torchcodec==0.16.0+cpu
|
| 191 |
+
torchvision==0.26.0+cpu
|
| 192 |
+
tqdm==4.70.1
|
| 193 |
+
transformers==5.10.2
|
| 194 |
+
triton==3.8.0
|
| 195 |
+
truststore==0.10.4
|
| 196 |
+
tt-smi==6.5.0
|
| 197 |
+
tt-tools-common==1.6.0
|
| 198 |
+
tt-umd==0.9.10
|
| 199 |
+
ttnn==0.65.2.dev7097+gcfd2056ecb4
|
| 200 |
+
ttnn==0.65.1rc17.dev7096+ggcfd2056ecb4
|
| 201 |
+
typer==0.27.2
|
| 202 |
+
typing-inspection==0.4.4
|
| 203 |
+
typing_extensions==4.16.0
|
| 204 |
+
urllib3==2.7.0
|
| 205 |
+
uvicorn==0.52.4
|
| 206 |
+
uvloop==0.22.1
|
| 207 |
+
virtualenv==21.7.9
|
| 208 |
+
vllm==0.26.0+empty
|
| 209 |
+
vllm-tt-plugin==0.1.0
|
| 210 |
+
watchfiles==1.2.0
|
| 211 |
+
websockets==17.1
|
| 212 |
+
wheel==0.48.0
|
| 213 |
+
xgrammar==0.2.6
|
| 214 |
+
yarl==1.24.5
|
| 215 |
+
z3-solver==4.15.4.0
|
tt_kernel_manifest.json
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema_version": "5.1",
|
| 3 |
+
"name": "nemotron-3.5-lightning-30b-a3b-nvfp4-tt-p300x2-vllm026",
|
| 4 |
+
"tt_metal_version": "0.65.2.dev7097+gcfd2056ecb4",
|
| 5 |
+
"arch": "blackhole",
|
| 6 |
+
"device_count": 4,
|
| 7 |
+
"producer": {
|
| 8 |
+
"tt_kernel_version": "0.1.0",
|
| 9 |
+
"created_at": "2026-09-11T17:52:26.396571+00:00",
|
| 10 |
+
"hostname": "tt-quietbox"
|
| 11 |
+
},
|
| 12 |
+
"weights": {
|
| 13 |
+
"repo_id": "nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4",
|
| 14 |
+
"revision": "6dbbd757ea75a8ece6e0702872e3ae53f9987728",
|
| 15 |
+
"allow_patterns": null,
|
| 16 |
+
"ignore_patterns": null,
|
| 17 |
+
"repo_type": "model"
|
| 18 |
+
},
|
| 19 |
+
"mesh": null,
|
| 20 |
+
"entrypoint": null,
|
| 21 |
+
"resources": null,
|
| 22 |
+
"capabilities": null,
|
| 23 |
+
"env": {},
|
| 24 |
+
"bundled": null,
|
| 25 |
+
"deps": null,
|
| 26 |
+
"container": {
|
| 27 |
+
"image": {
|
| 28 |
+
"registry": "hf",
|
| 29 |
+
"repository": "nemotron-3.5-lightning-30b-a3b-nvfp4-tt-p300x2-vllm026",
|
| 30 |
+
"tag": "tt-model/nemotron-3.5-lightning-30b-a3b-nvfp4-tt-p300x2-vllm026:745c72341b2d",
|
| 31 |
+
"digest": "sha256:745c72341b2d4058b7a08d6e8ed95d0e493586d5b4d3b78d75fc973d5fe8db51"
|
| 32 |
+
},
|
| 33 |
+
"kind": "vllm-plugin",
|
| 34 |
+
"runtime": {
|
| 35 |
+
"vllm": {
|
| 36 |
+
"version": "0.26.0"
|
| 37 |
+
},
|
| 38 |
+
"plugin": {
|
| 39 |
+
"path": "/home/ttuser/nemo35lightning/vllm-tt-plugin",
|
| 40 |
+
"sha": "52db64380a1564c2c16959b55c2cd7378ecbe9f0",
|
| 41 |
+
"dirty": false
|
| 42 |
+
},
|
| 43 |
+
"extra_models_dir": "models/autoports/nvidia_nvidia_nemotron_3_5_lightning_30b_a3b_nvfp4/vllm_bundle",
|
| 44 |
+
"wheels": [
|
| 45 |
+
"/home/ttuser/nemo35lightning/wheels_pins/torch-2.11.0+cpu-cp312-cp312-manylinux_2_28_x86_64.whl",
|
| 46 |
+
"/home/ttuser/nemo35lightning/wheels_pins/transformers-5.10.2-py3-none-any.whl",
|
| 47 |
+
"/home/ttuser/nemo35lightning/wheels_pins/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
| 48 |
+
"/home/ttuser/nemo35lightning/wheels_pins/huggingface_hub-1.16.1-py3-none-any.whl",
|
| 49 |
+
"/home/ttuser/nemo35lightning/wheels_pins/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
| 50 |
+
"/home/ttuser/nemo35lightning/wheels_pins/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",
|
| 51 |
+
"/home/ttuser/nemo35lightning/wheels_pins/loguru-0.6.0-py3-none-any.whl"
|
| 52 |
+
],
|
| 53 |
+
"lock": "requirements.lock"
|
| 54 |
+
},
|
| 55 |
+
"serve": {
|
| 56 |
+
"hardware": "p300x2",
|
| 57 |
+
"mesh_device": "P300x2",
|
| 58 |
+
"port": null,
|
| 59 |
+
"max_model_len": 1048576,
|
| 60 |
+
"max_num_seqs": 32,
|
| 61 |
+
"block_size": 64,
|
| 62 |
+
"server_timeout": null,
|
| 63 |
+
"capabilities": {
|
| 64 |
+
"tool_parser": "qwen3_coder",
|
| 65 |
+
"reasoning_parser": "nemotron_v3"
|
| 66 |
+
},
|
| 67 |
+
"additional_config": {
|
| 68 |
+
"tt": {
|
| 69 |
+
"sample_on_device_mode": "all",
|
| 70 |
+
"trace_region_size": 1073741824,
|
| 71 |
+
"fabric_config": "FABRIC_1D_RING"
|
| 72 |
+
}
|
| 73 |
+
},
|
| 74 |
+
"args": [
|
| 75 |
+
[
|
| 76 |
+
"--hf-overrides",
|
| 77 |
+
"{\"quantization_config\":null}"
|
| 78 |
+
],
|
| 79 |
+
[
|
| 80 |
+
"--revision",
|
| 81 |
+
"6dbbd757ea75a8ece6e0702872e3ae53f9987728"
|
| 82 |
+
],
|
| 83 |
+
[
|
| 84 |
+
"--tokenizer-revision",
|
| 85 |
+
"6dbbd757ea75a8ece6e0702872e3ae53f9987728"
|
| 86 |
+
]
|
| 87 |
+
],
|
| 88 |
+
"env": {
|
| 89 |
+
"TT_METAL_VISIBLE_DEVICES": "0,1,2,3",
|
| 90 |
+
"ARCH_NAME": "blackhole",
|
| 91 |
+
"TORCHDYNAMO_DISABLE": "1",
|
| 92 |
+
"VLLM_RPC_TIMEOUT": "900000",
|
| 93 |
+
"NEMOTRON_WEIGHT_CACHE": "/cache/nemotron_weights",
|
| 94 |
+
"NEMOTRON_REVISION": "6dbbd757ea75a8ece6e0702872e3ae53f9987728"
|
| 95 |
+
}
|
| 96 |
+
},
|
| 97 |
+
"serve_profiles": [
|
| 98 |
+
{
|
| 99 |
+
"hardware": null,
|
| 100 |
+
"mesh_device": null,
|
| 101 |
+
"port": null,
|
| 102 |
+
"max_model_len": null,
|
| 103 |
+
"max_num_seqs": null,
|
| 104 |
+
"block_size": null,
|
| 105 |
+
"server_timeout": null,
|
| 106 |
+
"capabilities": null,
|
| 107 |
+
"additional_config": {},
|
| 108 |
+
"args": [],
|
| 109 |
+
"env": {},
|
| 110 |
+
"name": "default",
|
| 111 |
+
"description": null
|
| 112 |
+
}
|
| 113 |
+
],
|
| 114 |
+
"default_profile": null,
|
| 115 |
+
"code_dir": "code",
|
| 116 |
+
"verify": [
|
| 117 |
+
"import vllm; assert vllm.__version__.startswith('0.26.'), vllm.__version__",
|
| 118 |
+
"from vllm.reasoning import ReasoningParserManager as R; R.get_reasoning_parser('nemotron_v3')",
|
| 119 |
+
"from vllm.tool_parsers import ToolParserManager as T; T.get_tool_parser('qwen3_coder')",
|
| 120 |
+
"import vllm_tt_plugin, pathlib; assert '/tt-metal/' not in vllm_tt_plugin.__file__ and 'site-packages' in vllm_tt_plugin.__file__, vllm_tt_plugin.__file__",
|
| 121 |
+
"from models.autoports.nvidia_nvidia_nemotron_3_5_lightning_30b_a3b_nvfp4.tt.generator_vllm import NemotronHForCausalLM; assert NemotronHForCausalLM",
|
| 122 |
+
"from pathlib import Path; p = Path('/opt/tt-metal/models/autoports/nvidia_nvidia_nemotron_3_5_lightning_30b_a3b_nvfp4/doc/datatype_sweep/selected_precision_config.json'); assert p.is_file(), 'selected_precision_config.json missing: the model would silently serve at code-default precision'",
|
| 123 |
+
"from models.autoports.nvidia_nvidia_nemotron_3_5_lightning_30b_a3b_nvfp4.tt import precision; r = precision.resolve_policies(); assert r[2]['config_id'] != 'code-defaults', r[2]",
|
| 124 |
+
"import transformers, tokenizers, safetensors, numpy, torch; assert transformers.__version__ == '5.10.2', transformers.__version__; assert numpy.__version__ == '2.3.5', numpy.__version__; assert torch.__version__ == '2.11.0+cpu', torch.__version__"
|
| 125 |
+
],
|
| 126 |
+
"built": {
|
| 127 |
+
"image": "tt-model/nemotron-3.5-lightning-30b-a3b-nvfp4-tt-p300x2-vllm026:745c72341b2d",
|
| 128 |
+
"repo": "ndaly/Nemotron-3.5-Lightning-30B-A3B-NVFP4-tt-p300x2-vllm026",
|
| 129 |
+
"tt_model_version": "0.1.0",
|
| 130 |
+
"created_at": "2026-09-11T17:47:54+00:00",
|
| 131 |
+
"tt_metal": {
|
| 132 |
+
"sha": "cfd2056ecb4601bd8b136b4ef1f89c6329f2eaef",
|
| 133 |
+
"describe": "v0.74.0-dev20260622-202-gcfd2056ecb-dirty",
|
| 134 |
+
"dirty": true,
|
| 135 |
+
"scm_version": "0.65.2.dev7097+gcfd2056ecb4",
|
| 136 |
+
"mode": "local",
|
| 137 |
+
"remote": "https://github.com/tenstorrent/tt-metal.git",
|
| 138 |
+
"branch": "agentic-research/hous/multigoal-claude",
|
| 139 |
+
"pushed": false
|
| 140 |
+
},
|
| 141 |
+
"code_sha256": "5787b8dfce9d7e38e43fdb6e6bc7cbfcb93b48d48719c9e825261a5624c2d151",
|
| 142 |
+
"plugin": {
|
| 143 |
+
"sha": "52db64380a1564c2c16959b55c2cd7378ecbe9f0",
|
| 144 |
+
"path": "/home/ttuser/nemo35lightning/vllm-tt-plugin",
|
| 145 |
+
"dirty": false
|
| 146 |
+
},
|
| 147 |
+
"image_digest": "sha256:745c72341b2d4058b7a08d6e8ed95d0e493586d5b4d3b78d75fc973d5fe8db51"
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
}
|