Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
471 changes: 456 additions & 15 deletions CMakeLists.txt

Large diffs are not rendered by default.

3,269 changes: 3,126 additions & 143 deletions apps/benchmark/native/benchmark_worker.cpp

Large diffs are not rendered by default.

357 changes: 301 additions & 56 deletions apps/benchmark/native/dataset_benchmark.cpp

Large diffs are not rendered by default.

174 changes: 168 additions & 6 deletions apps/benchmark/performance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ performance matrix -> reference runner
~~~

Neither the native core nor any family imports benchmark code. The candidate
worker loads a bundle with the public load_task(bundle, runtime_root) API and
calls the exact abstract Task interface implemented by that family.
worker selects the bundle's runtime mode before execution: migrated bundles use
the public header-only Task SDK and C ABI, while existing bundles use
load_task(bundle, runtime_root). Both call the family's implementation directly;
a failed SDK call is never retried through an old interface.

## Task API benchmark

Expand All @@ -37,6 +39,165 @@ are mutable and are rebuilt rather than assumed unchanged. An explicitly
supplied bundle outside the managed cache remains the caller's provenance
responsibility. `--rebuild` forces a fresh managed build.

### Select another Task from the same bundle

A family-owned testcase can select a secondary Task without changing the
manifest's primary `task` or rebuilding the bundle:

```json
{
"name": "token-features",
"selected_task": "text_to_token_features",
"inputs": {"token_ids": [7, 9]},
"config": {}
}
```

The existing `--case token-features` option selects this workload. Alternatively,
`--task text_to_token_features` selects the interface for the chosen testcase;
its inputs must match that interface. The operation defaults from the selected
Task. An explicit incompatible operation is an error, not a request to try
another interface. `selected_task` is call selection, not family Config.

The worker checks that the loaded model actually binds the requested Task.
Bundle identity and managed-cache checks still use the manifest's primary Task.
Results, reproduction records and history comparison retain the selected Task,
so token features and pooled features cannot become the same performance series.
Performance references receive the selection separately from the unchanged
manifest. Their input and output support must match the chosen contract; the
text-only reference loaders reject token IDs instead of replacing them with
empty text. This selection path requires a family migrated to the Task SDK.

The `head_scores` operation uses `head-scores-shape` for performance comparison:
both outputs must contain every finite score and agree on tensor shape, score
kind, pooling and normalization. It does not compare score accuracy or turn
logits into embeddings. Numerical acceptance remains in the owning family's
correctness tests; a matching reference implementation is still required.

The `geometry`, `predict_structure` and `refine_pose` operations similarly use
`metric-geometry-shape`, `molecular-structure-shape` and `pose-refinement-shape`.
These contracts check complete output fields and artifact lengths, semantic
layouts, confidence presence and pose callback metadata. Their receipts mark
`numerical_parity_checked: false`: predicted values and opaque serialization
lengths need not be identical across implementations. Existing family numerical
thresholds remain authoritative.

`offline_speech_dialogue` uses `offline-speech-shape`: the complete candidate
event PCM and reference Float32 WAV must agree on input/output counts and audio
formats. Text is retained per epoch, with final text replacing partial updates
instead of duplicating them. The comparison records both texts but does not
judge text or audio accuracy; `text_parity_checked` and
`numerical_parity_checked` are false. It cannot qualify live or tool dialogue.

### Family-owned performance declarations

The canonical release suite also reads optional
`families/<family>/tests/performance.yaml` files using the same
`trtmc.perf-suite/v2` format. Each file may name only that family's manifests and
testcases. Entry IDs must be globally unique; family files cannot replace central
entries or add exclusions. A standalone user suite does not implicitly include
other suites. Check, prepare, run and resume use the same composition.

Use an existing reference adapter when it implements the actual workload.
Otherwise declare a Python script relative to the owning family directory:

```yaml
schema_version: trtmc.perf-suite/v2
name: example-performance
defaults:
measurement: {warmup: 3, iterations: 10}
entries:
- id: example.head_scores
family: example
model: example-model
operation: head_scores
workload: {testcase: example-head-scores}
baseline:
runner: task-reference
script: tests/performance_reference.py
mode: pytorch-eager
timing_scope: task-model-call-wall
input_preparation_included: false
asset_loading_included: false
```

This is an illustrative declaration, not an existing qualified model. `script`
and `adapter` are mutually exclusive. The script must be an existing `.py` file
inside its family, without symlinks or parent-directory traversal. It runs through
the existing subprocess mechanism; the core does not import or execute it.

The script receives the existing model/revision, family, operation, manifest,
request JSON, selected Task, adapter-options JSON, precision, mode, padding,
warmup, iterations, case name, output path and timing-contract JSON arguments.
When no mode is declared, a custom script uses the neutral label `reference`,
not an implied `torch.compile` claim. Supplied token IDs stay in request JSON;
the script must execute the requested operand and Task, not decode/re-tokenize or
substitute a different operation. Preparation needed by the candidate must happen
explicitly before the candidate runs; a reference is not a preparation hook.

Return `trtmc.perf-baseline/v1` JSON with `status: completed`, matching model,
family, operation, case name, selected Task, precision and mode. Include exact
warmup/iteration counts in `measurement`; include the three declared timing
fields both at top level and in `measurement_policy`. Return one actual finite,
positive `samples_ms` value per measured invocation, its median in
`metrics.latency_ms.p50`, and the complete `output_summary` required by the output
contract. Loading happens once before warmup; input preparation and output
materialization follow the declared clock. Metadata validation does not prove
model accuracy or make an unexecuted reference qualified.

## DataFrame forecast formatting

Install `pandas` separately (`pip install pandas`) when using the optional table
helpers. They do not load a model or run Python inference:

```python
import pandas as pd
from trtmc_benchmark.dataframe import prepare_forecast_frame, format_forecast_frame

frame = pd.read_csv("history.csv", parse_dates=["ds"])
prepared = prepare_forecast_frame(frame, freq="D")
# prepared.request is the existing native worker's batch solve payload.
# Pass it as request= to an existing resolved batch forecast case.
```

Input columns default to `unique_id`, `ds`, and `values`. Series keep first-seen
order; time is sorted within each series. Timestamps must match the explicit
calendar frequency: gaps/duplicates are errors, not silently resampled data.
Use `value_columns=("x", "y")` for one two-channel series, not two batch items.
Missing values retain their masks; no shared imputation or normalization occurs.
`config_by_series={"sensor-a": {"frequency": 0}}` passes family Config unchanged;
calendar `freq` never selects a model's frequency category.

For an existing resolved `case` whose bundle declares one of
`batch_series_to_point_forecast`, `batch_series_to_quantile_forecast`, or
`batch_series_to_point_and_quantile_forecast`, reuse the normal worker:

```python
from pathlib import Path
from trtmc_benchmark.types import MeasurementSpec
from trtmc_benchmark.worker import find_worker, run_worker

case = case.with_values(request=prepared.request,
measurement=MeasurementSpec(warmup=0, iterations=1))
output_dir = Path("forecast-results")
output_dir.mkdir(parents=True, exist_ok=True)
receipt = run_worker(case, output_dir, find_worker())
forecast = format_forecast_frame(prepared, receipt["output_summary"])
```

The case retains its explicit runtime root. This example executes one complete
native batch; normal benchmark warmup/iteration settings repeat that batch.
Scalar-only routes reject the batch payload rather than retrying each series.
Real family native-batch support must be qualified separately; this helper does
not imply that the existing TimesFM batch-one bundle supports native batching.

The result keeps series/horizon/channel order, actual horizon offsets, output
channel labels/units, and all actual quantile levels. The `forecast` column is
the real point output, never a substituted median; quantile-only results have
no invented point column. Unknown output labels remain missing. Heterogeneous
quantile columns have missing cells where a series did not predict that level,
with actual per-series levels recorded in `forecast.attrs`.

## Timing contract

Candidate measurements use one scope: public_task_call_wall.
Expand Down Expand Up @@ -101,8 +262,9 @@ python3 tools/perf_matrix.py report artifacts/perf/<run-directory>

## Configuration

release.yaml owns only model, testcase, operation, measurement, reference, and
comparison semantics. Machine paths live in one environment YAML.
The release suite and optional family-owned suites declare model, testcase,
operation, measurement, reference and comparison semantics. Machine paths live
in the existing environment YAML.

A release entry has one explicit testcase:

Expand All @@ -119,8 +281,8 @@ A release entry has one explicit testcase:
~~~

The catalog reads families/*/tests/manifests/*.json directly. It has no second
registry and no family-specific benchmark plugin. A task that the worker does
not implement is reported as unsupported.
model registry; optional performance declarations reuse those exact manifests.
A task that the worker does not implement is reported as unsupported.

The entry, model, and model-selection options select work. resume continues
incomplete rows, and report regenerates JSON and HTML from stored measurements.
Expand Down
82 changes: 80 additions & 2 deletions apps/benchmark/performance/baselines/hf_transformers.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,20 @@ def _request(raw: str) -> dict[str, Any]:
raise ValueError(f"--request-json is not valid JSON: {exc}") from exc
if not isinstance(value, dict):
raise ValueError("--request-json must contain an object")
return value
return flatten_config(value)


def flatten_config(request: Mapping[str, Any]) -> dict[str, Any]:
"""Expose explicit SDK Config to existing reference calls without defaults."""
result = dict(request)
config = result.pop("config", {})
if not isinstance(config, Mapping):
raise ValueError("reference request config must be an object")
duplicates = sorted(result.keys() & config.keys())
if duplicates:
raise ValueError("duplicate reference request/config keys: " + ", ".join(duplicates))
result.update(config)
return result


def _dtype(torch_module: Any, precision: str) -> Any:
Expand Down Expand Up @@ -153,7 +166,11 @@ def _compile(model: Any, arguments: argparse.Namespace) -> dict[str, Any] | None


def _batch_prompt(request: Mapping[str, Any]) -> list[str]:
prompt = request.get("prompt")
if "token_ids" in request:
raise ValueError("this reference text loader does not accept token_ids")
if "prompt" in request and "source_text" in request:
raise ValueError("reference request must not provide both prompt and source_text")
prompt = request.get("source_text", request.get("prompt"))
if not isinstance(prompt, str):
raise ValueError("Transformers baseline requires request.prompt")
batch_size = request.get("batch_size", 1)
Expand All @@ -162,6 +179,59 @@ def _batch_prompt(request: Mapping[str, Any]) -> list[str]:
return [prompt] * batch_size


def _translation_controls(tokenizer: Any, request: Mapping[str, Any]) -> dict[str, int]:
"""Use tokenizer language APIs; fixed-pair models validate their declared pair."""
source = request.get("source_language")
target = request.get("target_language")
for name, value in (("source_language", source), ("target_language", target)):
if value is not None and (not isinstance(value, str) or not value):
raise ValueError(f"request.{name} must be a nonempty language identifier")

def token_id(language: str) -> int:
lookup = getattr(tokenizer, "get_lang_id", None)
if lookup is None:
lookup = tokenizer.convert_tokens_to_ids
value = lookup(language)
if (
isinstance(value, bool)
or not isinstance(value, int)
or value < 0
or value == getattr(tokenizer, "unk_token_id", None)
):
raise ValueError(f"tokenizer does not recognize language {language!r}")
return value

def explicit_id(name: str) -> int | None:
value = request.get(name)
if value is None:
return None
if isinstance(value, bool) or not isinstance(value, int) or value < -1:
raise ValueError(f"request.{name} must be a nonnegative integer or -1")
return None if value == -1 else value

source_id = explicit_id("source_language_token_id")
target_id = explicit_id("forced_bos_token_id")
if source_id is not None:
if source is not None and token_id(source) != source_id:
raise ValueError("source language disagrees with source_language_token_id")
source = tokenizer.convert_ids_to_tokens(source_id)
if source is not None:
if hasattr(tokenizer, "src_lang"):
token_id(source)
tokenizer.src_lang = source
elif source != getattr(tokenizer, "source_lang", None):
raise ValueError("reference tokenizer does not support the requested source language")
if target is not None:
if hasattr(tokenizer, "src_lang"):
resolved = token_id(target)
if target_id is not None and target_id != resolved:
raise ValueError("target language disagrees with forced_bos_token_id")
target_id = resolved
elif target != getattr(tokenizer, "target_lang", None):
raise ValueError("reference tokenizer does not support the requested target language")
return {} if target_id is None else {"forced_bos_token_id": target_id}


def _encoder_call(
tokenizer: Any,
model: Any,
Expand Down Expand Up @@ -227,6 +297,7 @@ def _generation_call(
) -> tuple[Callable[[], Any], Callable[[Any], dict[str, Any]]]:
import torch

request = flatten_config(request)
prompts = _batch_prompt(request)
if len(prompts) != 1:
raise ValueError("the release generation baseline currently requires batch_size=1")
Expand All @@ -253,6 +324,13 @@ def encode_prompt() -> Any:
"use_cache": True,
"pad_token_id": tokenizer.pad_token_id,
}
translation = _translation_controls(tokenizer, request)
if generation_method == "ar-generate" and translation:
raise ValueError("ar-generate reference does not accept translation controls")
generation.update(translation)
for name in ("repetition_penalty", "min_p", "eos_token_id"):
if name in request:
generation[name] = request[name]
if generation["do_sample"]:
generation.update(
{
Expand Down
Loading
Loading