Download code/models/common/tests/demos/run_helpers.py from sunandbean/diffrhythm-1.2-p100a: direct link, hf CLI and curl.
- Browser
- Download file 44.7 kB
-
https://huggingface.co/sunandbean/diffrhythm-1.2-p100a/resolve/main/code/models/common/tests/demos/run_helpers.py
- Command line
-
hf download hf://sunandbean/diffrhythm-1.2-p100a/code/models/common/tests/demos/run_helpers.py
-
curl -L -o run_helpers.py https://huggingface.co/sunandbean/diffrhythm-1.2-p100a/resolve/main/code/models/common/tests/demos/run_helpers.py
44.7 kB
| # SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC | |
| # SPDX-License-Identifier: Apache-2.0 | |
| """Reusable teacher-forcing and benchmark run helpers for demos and tests.""" | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import time | |
| from dataclasses import dataclass, field | |
| from pathlib import Path | |
| import torch | |
| from loguru import logger | |
| import ttnn | |
| _SAME_SAMPLING_PARAMS = object() | |
| def make_contiguous_page_table(batch_size: int, max_seq_len: int, block_size: int = 32) -> torch.Tensor: | |
| """Create a contiguous demo page table with one disjoint block range per user.""" | |
| if min(batch_size, max_seq_len, block_size) <= 0: | |
| raise ValueError("page-table dimensions must be positive") | |
| blocks_per_user = (max_seq_len + block_size - 1) // block_size | |
| return torch.arange(batch_size * blocks_per_user, dtype=torch.int32).reshape(batch_size, blocks_per_user) | |
| class TeacherForceResult: | |
| """Result from a teacher-forcing evaluation run.""" | |
| predicted_tokens: list[int] | |
| predicted_tokens_per_user: list[list[int]] | |
| reference_top5: torch.Tensor | |
| prefill_time_s: float = 0.0 | |
| compile_decode_time_s: float = 0.0 | |
| decode_times_s: list[float] = field(default_factory=list) | |
| batch_size: int = 1 | |
| prefill_len: int = 0 | |
| def top1_accuracy(self) -> float: | |
| matches = sum( | |
| 1 for i, prediction in enumerate(self.predicted_tokens) if self.reference_top5[i, 0].item() == prediction | |
| ) | |
| return matches / len(self.predicted_tokens) | |
| def top5_accuracy(self) -> float: | |
| matches = sum( | |
| 1 for i, prediction in enumerate(self.predicted_tokens) if prediction in self.reference_top5[i, :] | |
| ) | |
| return matches / len(self.predicted_tokens) | |
| def ttft_ms(self) -> float: | |
| return self.prefill_time_s / self.batch_size * 1000 if self.batch_size else 0.0 | |
| def prefill_time_to_token_s(self) -> float: | |
| return self.prefill_time_s / self.batch_size if self.batch_size else 0.0 | |
| def prefill_tok_s(self) -> float: | |
| return (self.batch_size * self.prefill_len) / self.prefill_time_s if self.prefill_time_s > 0 else 0.0 | |
| def decode_tok_s_u(self) -> float: | |
| return len(self.decode_times_s) / sum(self.decode_times_s) if self.decode_times_s else 0.0 | |
| def decode_tok_s(self) -> float: | |
| return self.decode_tok_s_u * self.batch_size | |
| class PerfBenchmarkResult: | |
| """Result from a performance benchmark run.""" | |
| prefill_time_s: float | |
| compile_decode_time_s: float | |
| decode_times_s: list[float] | |
| batch_size: int | |
| num_decode_tokens: int | |
| generated_token_ids: list[list[int]] | |
| decode_iteration_times_s: list[float] = field(default_factory=list) | |
| argmax_top2_margins: list[list[float]] | None = None | |
| def ttft_ms(self) -> float: | |
| """TTTv1-style average time to first token per user.""" | |
| return self.prefill_time_s / self.batch_size * 1000 | |
| def tok_s_u(self) -> float: | |
| """Tokens per second per user during steady-state decode.""" | |
| if not self.decode_times_s: | |
| return 0.0 | |
| return len(self.decode_times_s) / sum(self.decode_times_s) | |
| def tok_s(self) -> float: | |
| """Total decode throughput.""" | |
| return self.tok_s_u * self.batch_size | |
| def decode_latency_mean_ms(self) -> float: | |
| if not self.decode_times_s: | |
| return 0.0 | |
| return sum(self.decode_times_s) / len(self.decode_times_s) * 1000 | |
| def meets_target(self, expected: dict, tolerance: float = 0.05) -> dict[str, bool]: | |
| """Check benchmark metrics against the expected thresholds.""" | |
| return { | |
| "tok_s_u": self.tok_s_u >= expected["tok_s_u"] * (1 - tolerance), | |
| "ttft_ms": self.ttft_ms <= expected["ttft_ms"] * (1 + tolerance), | |
| } | |
| def _compile_prefill_and_decode( | |
| execution_target, | |
| *, | |
| prefill_tokens: torch.Tensor, | |
| prefill_page_table: torch.Tensor, | |
| kv_cache=None, | |
| prompt_lens: torch.Tensor | None = None, | |
| empty_slots: list[int] | None = None, | |
| start_pos: torch.Tensor | None = None, | |
| sampling_params=None, | |
| prefill_sampling_params=_SAME_SAMPLING_PARAMS, | |
| decode_tokens: torch.Tensor | None = None, | |
| decode_start_pos: torch.Tensor | None = None, | |
| decode_page_table: torch.Tensor | None = None, | |
| ) -> None: | |
| """Compile the concrete prefill and decode cases through the public target surface.""" | |
| assert prefill_tokens.dim() == 2, f"prefill_tokens must be [batch_size, seq_len], got {prefill_tokens.dim()}D" | |
| assert ( | |
| prefill_page_table.dim() == 2 | |
| ), f"prefill_page_table must be [batch_size, max_blocks], got {prefill_page_table.dim()}D" | |
| batch_size = prefill_tokens.shape[0] | |
| if decode_tokens is None: | |
| decode_tokens = torch.zeros(batch_size, dtype=torch.long, device=prefill_tokens.device) | |
| if decode_start_pos is None: | |
| decode_start_pos = torch.full( | |
| (decode_tokens.shape[0],), | |
| prefill_tokens.shape[-1], | |
| dtype=torch.long, | |
| device=prefill_tokens.device, | |
| ) | |
| if decode_page_table is None: | |
| decode_page_table = prefill_page_table | |
| if prefill_sampling_params is _SAME_SAMPLING_PARAMS: | |
| prefill_sampling_params = sampling_params | |
| if sampling_params is not None: | |
| execution_target.compile_decode( | |
| tokens=decode_tokens, | |
| start_pos=decode_start_pos, | |
| page_table=decode_page_table, | |
| kv_cache=kv_cache, | |
| sampling_params=sampling_params, | |
| ) | |
| execution_target.compile_prefill( | |
| tokens=prefill_tokens, | |
| page_table=prefill_page_table, | |
| kv_cache=kv_cache, | |
| prompt_lens=prompt_lens, | |
| empty_slots=empty_slots, | |
| start_pos=start_pos, | |
| sampling_params=prefill_sampling_params, | |
| ) | |
| return | |
| execution_target.compile_prefill( | |
| tokens=prefill_tokens, | |
| page_table=prefill_page_table, | |
| kv_cache=kv_cache, | |
| prompt_lens=prompt_lens, | |
| empty_slots=empty_slots, | |
| start_pos=start_pos, | |
| sampling_params=None, | |
| ) | |
| execution_target.compile_decode( | |
| tokens=decode_tokens, | |
| start_pos=decode_start_pos, | |
| page_table=decode_page_table, | |
| kv_cache=kv_cache, | |
| sampling_params=None, | |
| ) | |
| def _profiler_start(profiler, name: str) -> None: | |
| if profiler is not None: | |
| profiler.start(name) | |
| def _profiler_end(profiler, name: str) -> None: | |
| if profiler is not None: | |
| profiler.end(name) | |
| def run_teacher_forcing( | |
| executor, | |
| *, | |
| prompt_tokens: torch.Tensor, | |
| reference_tokens: torch.Tensor, | |
| top5_tokens: torch.Tensor, | |
| kv_cache: list, | |
| page_table: torch.Tensor, | |
| max_batch_size: int = 1, | |
| profiler=None, | |
| ) -> TeacherForceResult: | |
| """Run teacher-forcing accuracy measurement against an execution target.""" | |
| execution_target = executor | |
| batch_size = prompt_tokens.shape[0] | |
| assert ( | |
| batch_size == max_batch_size | |
| ), f"Teacher forcing expects active batch to match max_batch_size, got {batch_size} vs {max_batch_size}" | |
| prompt_len = prompt_tokens.shape[-1] | |
| num_target = len(reference_tokens) - prompt_len | |
| prompt_lens = torch.tensor([prompt_len] * batch_size) | |
| empty_slots = list(range(batch_size)) | |
| _compile_prefill_and_decode( | |
| execution_target, | |
| prefill_tokens=prompt_tokens, | |
| prefill_page_table=page_table, | |
| kv_cache=kv_cache, | |
| prompt_lens=prompt_lens, | |
| empty_slots=empty_slots, | |
| ) | |
| logger.info(f"Teacher forcing: prefilling {prompt_len} tokens with batch={batch_size}") | |
| _profiler_start(profiler, "inference_prefill") | |
| try: | |
| start_time = time.perf_counter() | |
| prefill_output = execution_target.prefill_forward( | |
| prompt_tokens, | |
| page_table=page_table, | |
| kv_cache=kv_cache, | |
| prompt_lens=prompt_lens, | |
| empty_slots=empty_slots, | |
| ) | |
| _synchronize_target(execution_target) | |
| prefill_time_s = time.perf_counter() - start_time | |
| finally: | |
| _profiler_end(profiler, "inference_prefill") | |
| first_tokens = torch.argmax(prefill_output, dim=-1).view(-1).tolist() | |
| predicted_tokens_per_user = [[int(token)] for token in first_tokens] | |
| logger.info(f"Teacher forcing: decoding {num_target - 1} tokens") | |
| compile_decode_time_s = 0.0 | |
| decode_times_s = [] | |
| _profiler_start(profiler, "inference_decode") | |
| try: | |
| for step in range(1, num_target): | |
| ground_truth_token = reference_tokens[prompt_len + step - 1] | |
| decode_token = torch.full((batch_size,), ground_truth_token, dtype=torch.long) | |
| current_pos = torch.full((batch_size,), prompt_len + step - 1, dtype=torch.long) | |
| start_time = time.perf_counter() | |
| logits, _ = execution_target.decode_forward( | |
| decode_token, | |
| current_pos, | |
| page_table=page_table, | |
| kv_cache=kv_cache, | |
| read_from_device=True, | |
| ) | |
| elapsed = time.perf_counter() - start_time | |
| if step == 1: | |
| compile_decode_time_s = elapsed | |
| else: | |
| decode_times_s.append(elapsed) | |
| next_tokens = torch.argmax(logits[:, -1, :], dim=-1).view(-1).tolist() | |
| for user_id, token in enumerate(next_tokens): | |
| predicted_tokens_per_user[user_id].append(int(token)) | |
| finally: | |
| _profiler_end(profiler, "inference_decode") | |
| return TeacherForceResult( | |
| predicted_tokens=predicted_tokens_per_user[0], | |
| predicted_tokens_per_user=predicted_tokens_per_user, | |
| reference_top5=top5_tokens[:num_target], | |
| prefill_time_s=prefill_time_s, | |
| compile_decode_time_s=compile_decode_time_s, | |
| decode_times_s=decode_times_s, | |
| batch_size=batch_size, | |
| prefill_len=prompt_len, | |
| ) | |
| def _split_output(output): | |
| return output if isinstance(output, tuple) else (output, None) | |
| def _target_mesh_device(execution_target): | |
| return getattr(execution_target, "mesh_device", None) | |
| def _target_cluster_shape(execution_target): | |
| cluster_shape = getattr(execution_target, "cluster_shape", None) | |
| if cluster_shape is not None: | |
| return list(cluster_shape) | |
| mesh_device = _target_mesh_device(execution_target) | |
| return list(mesh_device.shape) if mesh_device is not None else [1, 1] | |
| def _synchronize_target(execution_target): | |
| mesh_devices = getattr(execution_target, "mesh_devices", None) | |
| if mesh_devices is not None: | |
| for mesh_device in mesh_devices: | |
| if mesh_device is not None: | |
| ttnn.synchronize_device(mesh_device) | |
| return | |
| mesh_device = _target_mesh_device(execution_target) | |
| if mesh_device is not None: | |
| ttnn.synchronize_device(mesh_device) | |
| def _concat_host_output(output, cluster_shape): | |
| output_tensors = [ttnn.to_torch(tensor) for tensor in ttnn.get_device_tensors(output)] | |
| _, columns = cluster_shape | |
| mesh_rows = [output_tensors[i : i + columns] for i in range(0, len(output_tensors), columns)] | |
| return torch.cat([torch.cat(row, dim=-1) for row in mesh_rows], dim=1) | |
| def _process_legacy_sampled_tokens(output, batch_size, cluster_shape): | |
| torch_output = _concat_host_output(output, cluster_shape) | |
| if torch_output.ndim >= 4: | |
| if torch_output.shape[2] >= batch_size: | |
| return torch_output[0, 0, :batch_size, 0] | |
| if torch_output.shape[3] >= batch_size: | |
| return torch_output[0, 0, 0, :batch_size] | |
| return torch_output.reshape(-1)[:batch_size] | |
| def _to_host(value, *, blocking): | |
| if value is None: | |
| return None | |
| if isinstance(value, torch.Tensor): | |
| return value.cpu() | |
| try: | |
| return value.cpu(blocking=blocking) | |
| except TypeError: | |
| return value.cpu() | |
| def _submit_decode_read(execution_target, decode_output): | |
| read_decode_output = getattr(execution_target, "read_decode_output", None) | |
| if callable(read_decode_output): | |
| return read_decode_output(decode_output, async_read=True) | |
| output, log_probs = _split_output(decode_output) | |
| host_output = (_to_host(output, blocking=False), _to_host(log_probs, blocking=False)) | |
| return host_output, [ttnn.record_event(_target_mesh_device(execution_target), 0)] | |
| def _synchronize_read_events(events): | |
| if events is None: | |
| return | |
| if not isinstance(events, (list, tuple, set)): | |
| events = [events] | |
| for event in events: | |
| ttnn.event_synchronize(event) | |
| def _consume_sampled_output( | |
| execution_target, | |
| host_output, | |
| batch_size, | |
| cluster_shape, | |
| generated_token_ids, | |
| *, | |
| process_host_output, | |
| ): | |
| process_decode_output_host = ( | |
| getattr(execution_target, "process_decode_output_host", None) if process_host_output else None | |
| ) | |
| if callable(process_decode_output_host): | |
| tokens, _ = process_decode_output_host(host_output, is_tokens=True) | |
| else: | |
| tokens, _ = _split_output(host_output) | |
| if isinstance(tokens, torch.Tensor): | |
| tokens = tokens.view(-1)[:batch_size].detach().cpu() | |
| else: | |
| tokens = _process_legacy_sampled_tokens(tokens, batch_size, cluster_shape) | |
| tokens = tokens.view(-1)[:batch_size].detach().cpu() | |
| for user_id, token in enumerate(tokens.tolist()): | |
| generated_token_ids[user_id].append(int(token)) | |
| def _host_argmax_with_margins(logits: torch.Tensor, batch_size: int) -> tuple[torch.Tensor, torch.Tensor]: | |
| """Return host argmax and top1-minus-top2 margins for eval diagnostics.""" | |
| rows = logits[:, -1, :] | |
| top2 = torch.topk(rows.float(), k=2, dim=-1) | |
| tokens = top2.indices[:, 0].view(-1)[:batch_size].detach().cpu() | |
| margins = (top2.values[:, 0] - top2.values[:, 1]).view(-1)[:batch_size].detach().cpu() | |
| return tokens, margins | |
| def run_perf_benchmark( | |
| executor, | |
| *, | |
| tokens: torch.Tensor, | |
| kv_cache: list, | |
| page_table: torch.Tensor, | |
| num_decode_tokens: int = 128, | |
| max_batch_size: int = 1, | |
| prompt_lens: torch.Tensor | None = None, | |
| start_pos: list[int] | None = None, | |
| sampling_params=None, | |
| prefill_sampling_params=_SAME_SAMPLING_PARAMS, | |
| pipeline_readback: bool = False, | |
| profiler=None, | |
| collect_argmax_diagnostics: bool = False, | |
| ) -> PerfBenchmarkResult: | |
| """Run the timed prefill and decode loop against a public execution target.""" | |
| execution_target = executor | |
| mesh_device = _target_mesh_device(execution_target) | |
| has_public_readback = callable(getattr(execution_target, "read_decode_output", None)) | |
| has_legacy_readback = mesh_device is not None and hasattr(ttnn, "record_event") | |
| can_pipeline_readback = ( | |
| sampling_params is not None | |
| and pipeline_readback | |
| and hasattr(ttnn, "event_synchronize") | |
| and (has_public_readback or has_legacy_readback) | |
| ) | |
| if sampling_params is not None and pipeline_readback and not can_pipeline_readback: | |
| logger.warning("PIPELINE_READBACK requested, but this execution target does not expose async readback") | |
| batch_size, prompt_len = tokens.shape | |
| max_batch_size = max(max_batch_size, batch_size) | |
| cluster_shape = _target_cluster_shape(execution_target) | |
| prompt_lens = prompt_lens if prompt_lens is not None else torch.tensor([prompt_len] * batch_size) | |
| if prefill_sampling_params is _SAME_SAMPLING_PARAMS: | |
| prefill_sampling_params = sampling_params | |
| prefill_kwargs = dict( | |
| # Decode always carries the full lane-capacity table. Prefill only owns | |
| # the active request rows, and its public contract requires matching | |
| # token/page-table batch dimensions. | |
| page_table=page_table[:batch_size], | |
| kv_cache=kv_cache, | |
| prompt_lens=prompt_lens, | |
| empty_slots=list(range(batch_size)), | |
| start_pos=start_pos, | |
| ) | |
| compile_tokens = torch.zeros(max_batch_size, prompt_len, dtype=tokens.dtype) | |
| compile_tokens[:batch_size] = tokens | |
| compile_prompt_lens = torch.zeros(max_batch_size, dtype=prompt_lens.dtype) | |
| compile_prompt_lens[:batch_size] = prompt_lens | |
| compile_page_table = page_table | |
| compile_empty_slots = list(range(batch_size)) | |
| if batch_size < max_batch_size: | |
| # A partial-cardinality diagnostic still decodes on the model's full | |
| # lane-capacity program, but prefill owns only its active requests. | |
| compile_tokens = tokens | |
| compile_prompt_lens = prompt_lens | |
| compile_page_table = page_table[:batch_size] | |
| _compile_prefill_and_decode( | |
| execution_target, | |
| prefill_tokens=compile_tokens, | |
| prefill_page_table=compile_page_table, | |
| kv_cache=kv_cache, | |
| prompt_lens=compile_prompt_lens, | |
| empty_slots=compile_empty_slots, | |
| start_pos=start_pos, | |
| sampling_params=sampling_params, | |
| prefill_sampling_params=prefill_sampling_params, | |
| decode_tokens=torch.zeros(max_batch_size, dtype=torch.long), | |
| decode_start_pos=torch.full((max_batch_size,), prompt_len, dtype=torch.long), | |
| decode_page_table=page_table, | |
| ) | |
| _profiler_start(profiler, "inference_prefill") | |
| try: | |
| start_time = time.perf_counter() | |
| prefill_output = execution_target.prefill_forward( | |
| tokens, | |
| **prefill_kwargs, | |
| sampling_params=prefill_sampling_params, | |
| ) | |
| _synchronize_target(execution_target) | |
| prefill_time = time.perf_counter() - start_time | |
| finally: | |
| _profiler_end(profiler, "inference_prefill") | |
| first_margins = None | |
| if collect_argmax_diagnostics and sampling_params is None: | |
| first_token, first_margins = _host_argmax_with_margins(prefill_output, batch_size) | |
| else: | |
| first_token = prefill_output[0] if isinstance(prefill_output, tuple) else torch.argmax(prefill_output, dim=-1) | |
| first_token = first_token.view(-1)[:batch_size].detach().cpu() | |
| generated_token_ids = [[int(token)] for token in first_token.tolist()] | |
| argmax_top2_margins = [[float(margin)] for margin in first_margins.tolist()] if first_margins is not None else None | |
| current_tokens = torch.zeros(max_batch_size, dtype=torch.long) | |
| current_tokens[:batch_size] = first_token | |
| current_pos = torch.full((max_batch_size,), -1, dtype=torch.long) | |
| current_pos[:batch_size] = prompt_lens[:batch_size] | |
| compile_time = None | |
| decode_times = [] | |
| decode_iteration_times = [] | |
| sampled_decode_start = None | |
| pending_host_output = None | |
| pending_read_events = None | |
| _profiler_start(profiler, "inference_decode") | |
| try: | |
| for iteration in range(num_decode_tokens): | |
| start_time = time.perf_counter() | |
| read_from_device = sampling_params is None or not can_pipeline_readback | |
| if sampling_params is not None and iteration == 1: | |
| sampled_decode_start = start_time | |
| decode_output = execution_target.decode_forward( | |
| current_tokens, | |
| current_pos, | |
| page_table=page_table, | |
| kv_cache=kv_cache, | |
| read_from_device=read_from_device, | |
| sampling_params=sampling_params, | |
| reset_batch=iteration == 0, | |
| ) | |
| output, _ = _split_output(decode_output) | |
| completed_host_output = None | |
| if can_pipeline_readback: | |
| host_output, read_events = _submit_decode_read(execution_target, decode_output) | |
| if pending_read_events is not None: | |
| _synchronize_read_events(pending_read_events) | |
| completed_host_output = pending_host_output | |
| pending_host_output = host_output | |
| pending_read_events = read_events | |
| if sampling_params is not None and iteration == 0: | |
| _synchronize_target(execution_target) | |
| elapsed = time.perf_counter() - start_time | |
| if iteration == 0: | |
| compile_time = elapsed | |
| else: | |
| decode_iteration_times.append(elapsed) | |
| if sampling_params is None or can_pipeline_readback: | |
| decode_times.append(elapsed) | |
| if completed_host_output is not None: | |
| _consume_sampled_output( | |
| execution_target, | |
| completed_host_output, | |
| batch_size, | |
| cluster_shape, | |
| generated_token_ids, | |
| process_host_output=True, | |
| ) | |
| if sampling_params is None: | |
| if isinstance(output, torch.Tensor) and output.dim() >= 2: | |
| if collect_argmax_diagnostics: | |
| next_token, next_margins = _host_argmax_with_margins(output, batch_size) | |
| else: | |
| next_token = torch.argmax(output[:, -1, :], dim=-1) | |
| else: | |
| if collect_argmax_diagnostics: | |
| raise TypeError("argmax margin diagnostics require host decode logits") | |
| next_token = output | |
| next_token = next_token.view(-1)[:batch_size].detach().cpu() | |
| for user_id, token in enumerate(next_token.tolist()): | |
| generated_token_ids[user_id].append(int(token)) | |
| if argmax_top2_margins is not None: | |
| argmax_top2_margins[user_id].append(float(next_margins[user_id])) | |
| current_tokens[:batch_size] = next_token | |
| elif not can_pipeline_readback: | |
| _consume_sampled_output( | |
| execution_target, | |
| decode_output, | |
| batch_size, | |
| cluster_shape, | |
| generated_token_ids, | |
| process_host_output=False, | |
| ) | |
| current_pos[:batch_size] += 1 | |
| finally: | |
| _profiler_end(profiler, "inference_decode") | |
| if sampling_params is not None: | |
| if pending_read_events is not None: | |
| _synchronize_read_events(pending_read_events) | |
| _consume_sampled_output( | |
| execution_target, | |
| pending_host_output, | |
| batch_size, | |
| cluster_shape, | |
| generated_token_ids, | |
| process_host_output=True, | |
| ) | |
| if sampled_decode_start is not None and not can_pipeline_readback: | |
| _synchronize_target(execution_target) | |
| sampled_decode_time = time.perf_counter() - sampled_decode_start | |
| decode_times = [sampled_decode_time / (num_decode_tokens - 1)] * (num_decode_tokens - 1) | |
| return PerfBenchmarkResult( | |
| prefill_time_s=prefill_time, | |
| compile_decode_time_s=compile_time or 0.0, | |
| decode_times_s=decode_times, | |
| batch_size=batch_size, | |
| num_decode_tokens=num_decode_tokens, | |
| generated_token_ids=generated_token_ids, | |
| decode_iteration_times_s=decode_iteration_times, | |
| argmax_top2_margins=argmax_top2_margins, | |
| ) | |
| def _add_token_ids(target: set[int], value) -> None: | |
| if value is None or isinstance(value, bool): | |
| return | |
| if isinstance(value, int): | |
| target.add(int(value)) | |
| return | |
| if isinstance(value, (list, tuple, set)): | |
| for item in value: | |
| _add_token_ids(target, item) | |
| def _stop_token_ids(tokenizer) -> set[int]: | |
| stop_ids: set[int] = set() | |
| _add_token_ids(stop_ids, getattr(tokenizer, "eos_token_id", None)) | |
| _add_token_ids(stop_ids, getattr(tokenizer, "stop_tokens", None)) | |
| convert_tokens_to_ids = getattr(tokenizer, "convert_tokens_to_ids", None) | |
| if callable(convert_tokens_to_ids): | |
| eot_id = convert_tokens_to_ids("<|eot_id|>") | |
| if isinstance(eot_id, int) and eot_id >= 0: | |
| stop_ids.add(eot_id) | |
| return stop_ids | |
| def _truncate_at_stop(token_ids: list[int], stop_ids: set[int]) -> list[int]: | |
| for index, token_id in enumerate(token_ids): | |
| if token_id in stop_ids: | |
| return token_ids[:index] | |
| return token_ids | |
| def assert_no_special_tokens( | |
| generated_token_ids: list[list[int]], | |
| tokenizer, | |
| *, | |
| case_name: str = "", | |
| is_ci_env: bool | None = None, | |
| ) -> None: | |
| """Warn locally; fail under CI or ``TT_DEMO_STRICT_SPECIAL_TOKENS=1``.""" | |
| if is_ci_env is None: | |
| is_ci_env = os.environ.get("CI") == "true" or os.environ.get("TT_DEMO_STRICT_SPECIAL_TOKENS") == "1" | |
| special_ids = set(getattr(tokenizer, "all_special_ids", []) or []) | |
| stop_ids = _stop_token_ids(tokenizer) | |
| offending_users = 0 | |
| for token_ids in generated_token_ids: | |
| output_before_stop = _truncate_at_stop(list(token_ids), stop_ids) | |
| if any(token_id in special_ids for token_id in output_before_stop): | |
| offending_users += 1 | |
| if offending_users == 0: | |
| return | |
| prefix = f"[{case_name}] " if case_name else "" | |
| message = f"{prefix}model produced special tokens ({offending_users}/{len(generated_token_ids)} users)" | |
| logger.warning(message) | |
| if is_ci_env: | |
| raise AssertionError(message) | |
| def load_eval_repeat_prompts_batch32() -> list[str]: | |
| """The 32 numeric sequence-continuation prompts TTTv1's ci-eval-32 uses (parity).""" | |
| path = Path("models/tt_transformers/demo/sample_prompts/eval_repeat_prompts_batch32.json") | |
| with open(path) as f: | |
| data = json.load(f) | |
| return [entry["prompt"] for entry in data] | |
| def rotate_prompts(all_prompts: list[str], repeat: int) -> list[str]: | |
| """Rotate the prompt->slot assignment by ``repeat``: slot j holds prompt (j+repeat)%N.""" | |
| n = len(all_prompts) | |
| return [all_prompts[(j + repeat) % n] for j in range(n)] | |
| def eval_page_table_for_repeat(page_table: torch.Tensor, repeat: int, *, mode: str) -> torch.Tensor: | |
| """Select physical KV allocation for one rotated eval repeat. | |
| ``slot-stable`` is the TTTv1 acceptance geometry: page-table row ``j`` | |
| remains at decode slot ``j`` while prompts rotate. ``prompt-stable`` is a | |
| diagnostic A/B: rows rotate with prompts, so a prompt retains its original | |
| physical KV blocks even as it moves to another decode slot. | |
| """ | |
| if mode == "slot-stable": | |
| return page_table | |
| if mode == "prompt-stable": | |
| return torch.roll(page_table, shifts=-repeat, dims=0) | |
| raise ValueError(f"unsupported eval page-table mode {mode!r}; use 'slot-stable' or 'prompt-stable'") | |
| def eval_decode_trace_mode(mode: str) -> str: | |
| """Resolve the eval decode execution A/B without changing its default gate.""" | |
| if mode == "traced": | |
| return "decode_only" | |
| if mode == "eager": | |
| return "none" | |
| raise ValueError(f"unsupported eval decode mode {mode!r}; use 'traced' or 'eager'") | |
| def require_canonical_eval_modes_in_ci(environ) -> None: | |
| """Prevent diagnostic A/B knobs from replacing a canonical CI gate.""" | |
| if environ.get("CI") != "true": | |
| return | |
| noncanonical = [] | |
| if environ.get("EVAL_DECODE_MODE", "traced") != "traced": | |
| noncanonical.append("EVAL_DECODE_MODE") | |
| if environ.get("EVAL_PAGE_TABLE_MODE", "slot-stable") != "slot-stable": | |
| noncanonical.append("EVAL_PAGE_TABLE_MODE") | |
| noncanonical.extend(name for name in ("EVAL_IDENTICAL_PROMPT_INDEX", "EVAL_ACTIVE_BATCH_SIZE") if name in environ) | |
| if noncanonical: | |
| raise RuntimeError("diagnostic eval modes cannot replace the canonical CI gate: " + ", ".join(noncanonical)) | |
| def truncate_at_stop(ids: list[int], stop_ids: set[int]) -> list[int]: | |
| """Prefix of ``ids`` up to (excluding) the first id in ``stop_ids``.""" | |
| out: list[int] = [] | |
| for t in ids: | |
| if t in stop_ids: | |
| break | |
| out.append(t) | |
| return out | |
| def hf_stop_ids(tokenizer, hf_model_id: str | None = None) -> set[int]: | |
| """Best-effort stop-token id set for an HF ``AutoTokenizer``. | |
| Raw HF tokenizers have no ``.stop_tokens`` (that only exists on the TTTv1 wrapped | |
| tokenizer). Build the set from ``eos_token_id`` (int|list|None), and — when an | |
| ``hf_model_id`` is supplied — also fold in the model's ``generation_config`` eos ids, | |
| since chat models (e.g. Llama-3 Instruct) often carry extra eot ids there rather than | |
| on ``eos_token_id``. Missing/empty -> empty set (truncation simply runs full length). | |
| """ | |
| stop: set[int] = set() | |
| def _add(value) -> None: | |
| if value is None: | |
| return | |
| if isinstance(value, bool): # guard: bool is an int subclass | |
| return | |
| if isinstance(value, int): | |
| stop.add(int(value)) | |
| elif isinstance(value, (list, tuple, set)): | |
| for e in value: | |
| _add(e) | |
| _add(getattr(tokenizer, "eos_token_id", None)) | |
| # tt_transformers ModelArgs.tokenizer augments the HF tokenizer with ``stop_tokens`` | |
| # (eos + any extra eot ids); raw HF AutoTokenizers don't have it (getattr -> None). | |
| _add(getattr(tokenizer, "stop_tokens", None)) | |
| if hf_model_id is not None: | |
| try: | |
| from transformers import GenerationConfig | |
| gen_cfg = GenerationConfig.from_pretrained(hf_model_id) | |
| _add(getattr(gen_cfg, "eos_token_id", None)) | |
| except Exception as e: # generation_config absent / unreadable — eos_token_id is enough | |
| logger.debug(f"ci-eval-32: could not read generation_config eos ids for {hf_model_id}: {e}") | |
| return stop | |
| def decode_eval_output(tokenizer, token_ids: list[int], stop_ids: set[int]) -> str: | |
| """Return the continuation text used by TTTv1's ci-eval-32 comparison. | |
| Different valid BPE segmentations can decode to the same text. The TTTv1 | |
| reference stores ``tokenizer.decode(...)`` results and compares those | |
| strings, so comparing token-id lists here would make the port stricter than | |
| its source workload and report false slot-rotation failures. | |
| """ | |
| decoded = tokenizer.decode(truncate_at_stop(token_ids, stop_ids)) | |
| if not isinstance(decoded, str): | |
| raise TypeError(f"tokenizer.decode must return str, got {type(decoded).__name__}") | |
| return decoded | |
| def assert_cross_cardinality_consistency( | |
| outputs_by_cardinality: dict[int, dict[str, str]], | |
| *, | |
| expected_cardinalities: tuple[int, ...] = (1, 2, 4, 32), | |
| ) -> None: | |
| """Require each fixed request's decoded output to be invariant as batch cardinality grows.""" | |
| if tuple(outputs_by_cardinality) != expected_cardinalities: | |
| raise AssertionError( | |
| f"cross-cardinality experiment expected {expected_cardinalities}, " f"got {tuple(outputs_by_cardinality)}" | |
| ) | |
| reference: dict[str, tuple[int, str]] = {} | |
| for cardinality, outputs in outputs_by_cardinality.items(): | |
| if len(outputs) != cardinality: | |
| raise AssertionError(f"cardinality {cardinality} returned {len(outputs)} request outputs") | |
| for request_id, output in outputs.items(): | |
| if request_id in reference: | |
| reference_cardinality, reference_output = reference[request_id] | |
| if output != reference_output: | |
| raise AssertionError( | |
| f"request {request_id!r} differs at cardinality {reference_cardinality}->{cardinality}: " | |
| f"{reference_output[:120]!r} != {output[:120]!r}" | |
| ) | |
| else: | |
| reference[request_id] = (cardinality, output) | |
| def assert_cross_batch_consistency( | |
| per_repeat_outputs: list[list[str]], | |
| *, | |
| per_repeat_token_ids: list[list[list[int]]] | None = None, | |
| per_repeat_prompt_lens: list[list[int]] | None = None, | |
| per_repeat_argmax_margins: list[list[list[float]] | None] | None = None, | |
| ) -> None: | |
| """Assert decoded prompt-position invariance across repeats. | |
| ``per_repeat_outputs[b][u]`` = decoded continuation for slot ``u`` of repeat ``b``. | |
| With slot j of repeat b holding prompt (j+b)%N (see ``rotate_prompts``), the same prompt | |
| sits at slot (offset+1)%N of repeat b and slot offset of repeat b+1 — so those two | |
| outputs must be identical if no per-user state leaks. | |
| """ | |
| num_batches = len(per_repeat_outputs) | |
| assert num_batches >= 2, "cross-batch consistency needs >=2 repeats" | |
| n = len(per_repeat_outputs[0]) | |
| failed, total = 0, 0 | |
| first_failure = None | |
| for b in range(num_batches - 1): | |
| cur, nxt = per_repeat_outputs[b], per_repeat_outputs[b + 1] | |
| for offset in range(n): | |
| total += 1 | |
| if cur[(offset + 1) % n] != nxt[offset]: | |
| failed += 1 | |
| if first_failure is None: | |
| current_slot = (offset + 1) % n | |
| prompt_index = (offset + b + 1) % n | |
| token_detail = "" | |
| if per_repeat_token_ids is not None: | |
| current_tokens = per_repeat_token_ids[b][current_slot] | |
| next_tokens = per_repeat_token_ids[b + 1][offset] | |
| common_tokens = 0 | |
| for current_token, next_token in zip(current_tokens, next_tokens): | |
| if current_token != next_token: | |
| break | |
| common_tokens += 1 | |
| current_token = current_tokens[common_tokens] if common_tokens < len(current_tokens) else None | |
| next_token = next_tokens[common_tokens] if common_tokens < len(next_tokens) else None | |
| token_detail = ( | |
| f"; first token divergence at generation step {common_tokens} " | |
| f"({current_token!r} != {next_token!r})" | |
| ) | |
| if per_repeat_argmax_margins is not None: | |
| current_repeat_margins = per_repeat_argmax_margins[b] | |
| next_repeat_margins = per_repeat_argmax_margins[b + 1] | |
| if current_repeat_margins is not None and next_repeat_margins is not None: | |
| current_margin = current_repeat_margins[current_slot][common_tokens] | |
| next_margin = next_repeat_margins[offset][common_tokens] | |
| token_detail += f"; top2 margins {current_margin:.6g} and {next_margin:.6g}" | |
| length_detail = "" | |
| if per_repeat_prompt_lens is not None: | |
| current_length = per_repeat_prompt_lens[b][current_slot] | |
| next_length = per_repeat_prompt_lens[b + 1][offset] | |
| length_detail = f"; prompt lengths {current_length} and {next_length}" | |
| first_failure = ( | |
| b, | |
| offset, | |
| current_slot, | |
| prompt_index, | |
| cur[current_slot], | |
| nxt[offset], | |
| token_detail, | |
| length_detail, | |
| ) | |
| assert failed == 0, ( | |
| f"ci-eval-32: {failed}/{total} cross-batch consistency checks failed " | |
| f"(first at repeat {first_failure[0]} slot {first_failure[2]} -> " | |
| f"repeat {first_failure[0] + 1} slot {first_failure[1]}, prompt index {first_failure[3]}" | |
| f"{first_failure[6]}{first_failure[7]}; " | |
| f"decoded outputs {first_failure[4][:80]!r} != {first_failure[5][:80]!r})" | |
| ) | |
| def assert_within_batch_slot_consistency( | |
| decoded_outputs: list[str], | |
| *, | |
| token_ids: list[list[int]], | |
| argmax_margins: list[list[float]] | None, | |
| prompt_index: int, | |
| ) -> None: | |
| """Prove one fixed request is invariant across logical slots in one run.""" | |
| reference = decoded_outputs[0] | |
| for slot, decoded in enumerate(decoded_outputs[1:], start=1): | |
| if decoded == reference: | |
| continue | |
| reference_tokens = token_ids[0] | |
| slot_tokens = token_ids[slot] | |
| common_tokens = 0 | |
| for reference_token, slot_token in zip(reference_tokens, slot_tokens): | |
| if reference_token != slot_token: | |
| break | |
| common_tokens += 1 | |
| reference_token = reference_tokens[common_tokens] if common_tokens < len(reference_tokens) else None | |
| slot_token = slot_tokens[common_tokens] if common_tokens < len(slot_tokens) else None | |
| margin_detail = "" | |
| if argmax_margins is not None: | |
| margin_detail = ( | |
| f"; top2 margins {argmax_margins[0][common_tokens]:.6g} " | |
| f"and {argmax_margins[slot][common_tokens]:.6g}" | |
| ) | |
| raise AssertionError( | |
| f"ci-eval-32 identical-request diagnostic: prompt index {prompt_index} differs between " | |
| f"logical slots 0 and {slot} at generation step {common_tokens} " | |
| f"({reference_token!r} != {slot_token!r}){margin_detail}; " | |
| f"decoded outputs {reference[:80]!r} != {decoded[:80]!r}" | |
| ) | |
| def run_eval_repeat_batch32( | |
| *, | |
| make_executor, | |
| allocate_kv_cache, | |
| page_table: torch.Tensor, | |
| prompts: list[str], | |
| tokenizer, | |
| tokenize_fn, | |
| num_decode_tokens: int, | |
| max_batch_size: int, | |
| sampling_params=None, | |
| repeat_batches: int = 3, | |
| hf_model_id: str | None = None, | |
| first_repeat_profiler=None, | |
| page_table_mode: str = "slot-stable", | |
| identical_prompt_index: int | None = None, | |
| active_batch_size: int | None = None, | |
| ) -> PerfBenchmarkResult: | |
| """Drive the ci-eval-32 determinism case, building a fresh traced executor per repeat. | |
| Each repeat builds its own traced executor (``make_executor()``) and its own zeroed KV | |
| cache (``allocate_kv_cache(executor)``), so the rotated batches are fully independent — | |
| no shared device or host state can leak across repeats. The executor is cleaned up after | |
| each repeat. (The model is bit-deterministic across repeats either way; fresh-per-repeat | |
| is simply the cleanest independence guarantee for a determinism test, and the trace | |
| recapture cost is negligible at batch-32.) | |
| Args: | |
| make_executor: Zero-arg callable returning a fresh traced executor | |
| (``run_perf_benchmark`` requires traced). Called once per repeat. | |
| allocate_kv_cache: Callable(executor) -> fresh zeroed kv_cache bound on that executor. | |
| page_table: Fixed contiguous page table (shared across repeats). | |
| prompts: The N (=max_batch_size) prompts to rotate (TTTv1 ci-eval-32 numeric prompts; | |
| see the module note above re: degenerate-output sensitivity on small models). | |
| tokenizer: HF tokenizer (for stop / special ids). | |
| tokenize_fn: Callable(list[str]) -> (tokens, prompt_lens). | |
| num_decode_tokens: Decode steps per repeat. | |
| max_batch_size: Padded batch (== len(prompts) for this fixed-32 case). | |
| sampling_params: None -> host argmax (deterministic, mesh-agnostic default). | |
| repeat_batches: Number of rotated repeats (TTTv1 uses 3). | |
| hf_model_id: Optional, to enrich stop ids from generation_config. | |
| first_repeat_profiler: Optional profiler passed only to the first repeat, allowing a caller | |
| to emit perf telemetry without changing the three-repeat determinism geometry. | |
| page_table_mode: ``slot-stable`` preserves TTTv1 acceptance geometry. | |
| ``prompt-stable`` keeps each prompt on the same physical KV blocks | |
| as a diagnostic A/B while it rotates through decode slots. | |
| identical_prompt_index: Diagnostic-only fixed request replicated into | |
| every logical slot. Slot consistency is checked after the first | |
| batch, before any repeat-lifecycle comparison. | |
| active_batch_size: Diagnostic-only number of leading logical slots to | |
| activate while retaining the model's full lane capacity. This is | |
| restricted to the identical-request probe so inactive lanes cannot | |
| complicate prompt-rotation semantics. | |
| """ | |
| assert ( | |
| len(prompts) == max_batch_size | |
| ), f"ci-eval-32 expects len(prompts)==max_batch_size; got {len(prompts)} vs {max_batch_size}" | |
| if active_batch_size is not None: | |
| if identical_prompt_index is None: | |
| raise ValueError("active_batch_size requires identical_prompt_index") | |
| if not 1 <= active_batch_size <= max_batch_size: | |
| raise ValueError(f"active_batch_size must be in [1, {max_batch_size}]") | |
| if identical_prompt_index is not None: | |
| if not 0 <= identical_prompt_index < len(prompts): | |
| raise ValueError(f"identical_prompt_index must be in [0, {len(prompts) - 1}]") | |
| prompts = [prompts[identical_prompt_index]] * (active_batch_size or len(prompts)) | |
| stop_ids = hf_stop_ids(tokenizer, hf_model_id) | |
| special_ids = set(getattr(tokenizer, "all_special_ids", []) or []) | |
| # Garbage guard targets only special tokens that are NOT recognized stops: a legitimate | |
| # stop is removed by truncation, so anything special left in the body is degenerate output. | |
| garbage_ids = special_ids - stop_ids | |
| logger.info( | |
| f"ci-eval-32: repeat_batches={repeat_batches}, N={len(prompts)}, " | |
| f"stop_ids={sorted(stop_ids)}, |special_ids|={len(special_ids)}, sampling_params={sampling_params}, " | |
| f"page_table_mode={page_table_mode}, identical_prompt_index={identical_prompt_index}, " | |
| f"active_batch_size={len(prompts)}" | |
| ) | |
| per_repeat: list[list[str]] = [] | |
| per_repeat_token_ids: list[list[list[int]]] = [] | |
| per_repeat_prompt_lens: list[list[int]] = [] | |
| per_repeat_argmax_margins: list[list[list[float]] | None] = [] | |
| first_result = None | |
| for i in range(repeat_batches): | |
| traced_executor = make_executor() | |
| try: | |
| kv_cache = allocate_kv_cache(traced_executor) | |
| rotated = rotate_prompts(prompts, i) | |
| tokens, prompt_lens = tokenize_fn(rotated) | |
| repeat_page_table = eval_page_table_for_repeat(page_table, i, mode=page_table_mode) | |
| result = run_perf_benchmark( | |
| traced_executor, | |
| tokens=tokens, | |
| kv_cache=kv_cache, | |
| page_table=repeat_page_table, | |
| num_decode_tokens=num_decode_tokens, | |
| max_batch_size=max_batch_size, | |
| prompt_lens=prompt_lens, | |
| sampling_params=sampling_params, | |
| profiler=first_repeat_profiler if i == 0 else None, | |
| collect_argmax_diagnostics=sampling_params is None, | |
| ) | |
| finally: | |
| traced_executor.cleanup() | |
| truncated = [truncate_at_stop(ids, stop_ids) for ids in result.generated_token_ids] | |
| for u, ids in enumerate(truncated): | |
| bad = set(ids) & garbage_ids | |
| assert not bad, f"ci-eval-32: user {u} produced special token(s) {sorted(bad)} mid-stream" | |
| decoded = [decode_eval_output(tokenizer, ids, stop_ids) for ids in result.generated_token_ids] | |
| per_repeat.append(decoded) | |
| per_repeat_token_ids.append(result.generated_token_ids) | |
| per_repeat_prompt_lens.append([int(length) for length in prompt_lens]) | |
| per_repeat_argmax_margins.append(getattr(result, "argmax_top2_margins", None)) | |
| if identical_prompt_index is not None: | |
| assert_within_batch_slot_consistency( | |
| decoded, | |
| token_ids=result.generated_token_ids, | |
| argmax_margins=getattr(result, "argmax_top2_margins", None), | |
| prompt_index=identical_prompt_index, | |
| ) | |
| if i == 0: | |
| first_result = result | |
| logger.info( | |
| f"ci-eval-32 repeat {i}: truncated token lengths = {[len(t) for t in truncated]}, " | |
| f"decoded character lengths = {[len(text) for text in decoded]}" | |
| ) | |
| if identical_prompt_index is None: | |
| assert_cross_batch_consistency( | |
| per_repeat, | |
| per_repeat_token_ids=per_repeat_token_ids, | |
| per_repeat_prompt_lens=per_repeat_prompt_lens, | |
| per_repeat_argmax_margins=per_repeat_argmax_margins, | |
| ) | |
| logger.info(f"ci-eval-32: all {(repeat_batches - 1) * len(prompts)} cross-batch consistency checks passed") | |
| else: | |
| logger.info( | |
| f"ci-eval-32 identical-request diagnostic: prompt index {identical_prompt_index} " | |
| f"is invariant across all {len(prompts)} logical slots" | |
| ) | |
| assert first_result is not None | |
| return first_result | |