Skip to content
Closed
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
5 changes: 5 additions & 0 deletions apps/benchmark/performance/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ excluded_profiles:
nano width, which is the only one whose class-attention norm covers a
single token, but the release-performance workload and receipt were
collected only for xcit_tiny_12_p16_224.
- model: minicpm5-2b
reason: >-
Functional and HF reference-parity e2e qualification is present, but
this change does not add a matching release-performance workload or
receipt.

entries:
- id: detr.detect
Expand Down
20 changes: 19 additions & 1 deletion families/llama/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@
from pathlib import Path


def _as_id_list(value: object) -> tuple[int, ...]:
"""Normalize an HF eos_token_id-shaped value (missing, a single id, or
a list of ids — e.g. Llama 3.1+, MiniCPM5) into a tuple of ints.

Checking for None (rather than falsiness) matters here: a legitimate
id of 0 must survive, unlike a missing value.
"""
if value is None:
return ()
if isinstance(value, (list, tuple)):
return tuple(int(v) for v in value)
return (int(value),)


@dataclass
class ModelConfig:
"""Parsed model architecture from HF config.json."""
Expand All @@ -26,6 +40,7 @@ class ModelConfig:
rope_theta: float = 10000.0
bos_token_id: int = -1
eos_token_id: int = -1
eos_token_ids: tuple[int, ...] = ()
pad_token_id: int = -1
tie_word_embeddings: bool = False
max_position_embeddings: int = 8192
Expand Down Expand Up @@ -173,6 +188,8 @@ def from_json(text: str) -> ModelConfig:
if not architectures and architecture:
architectures = [architecture]

eos_token_ids = _as_id_list(d.get("eos_token_id"))

return ModelConfig(
model_type=d.get("model_type", "") or architecture,
architectures=architectures,
Expand All @@ -185,7 +202,8 @@ def from_json(text: str) -> ModelConfig:
rms_norm_eps=eps,
rope_theta=rope_theta,
bos_token_id=d.get("bos_token_id", -1) or -1,
eos_token_id=d.get("eos_token_id", -1) or -1,
eos_token_id=eos_token_ids[0] if eos_token_ids else -1,
eos_token_ids=eos_token_ids,
pad_token_id=d.get("pad_token_id", -1) or -1,
tie_word_embeddings=d.get("tie_word_embeddings", False),
max_position_embeddings=d.get("max_position_embeddings",
Expand Down
7 changes: 4 additions & 3 deletions families/llama/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from .build_routing import native_kv_architecture_capability, native_kv_build_capability
from .checkpoint_mapper import WeightDict, load_standard_weights
from .config import ModelConfig
from .config import ModelConfig, _as_id_list
from .dual_profile_decoder_builder import build_dual_profile_decoder_engine
from .native_kv_contract import validate_native_kv_weights
from .standard_decoder_builder import build_standard_decoder_engine
Expand Down Expand Up @@ -98,7 +98,7 @@ def _runtime_config(model_dir: Path, config: ModelConfig, **updates) -> dict:
"num_key_value_heads": config.num_key_value_heads,
"head_dim": config.head_dim,
"bos_token_id": config.bos_token_id,
"eos_token_id": config.eos_token_id,
"eos_token_id": list(config.eos_token_ids) or [-1],
"pad_token_id": config.pad_token_id,
}
runtime.update(config.raw.get("_native_kv_cache_metadata", {}))
Expand All @@ -108,7 +108,8 @@ def _runtime_config(model_dir: Path, config: ModelConfig, **updates) -> dict:
if not isinstance(generation, dict):
raise ValueError("generation_config.json must contain one JSON object")
if "eos_token_id" in generation:
runtime["eos_token_id"] = generation["eos_token_id"]
eos_ids = _as_id_list(generation["eos_token_id"])
runtime["eos_token_id"] = list(eos_ids) if eos_ids else [-1]
runtime.update(updates)
return runtime

Expand Down
9 changes: 6 additions & 3 deletions families/llama/runtime/plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ struct RuntimeConfig {
std::int32_t head_dim;
std::int32_t vocab_size;
std::int32_t bos_token_id;
std::int32_t eos_token_id;
std::vector<std::int32_t> eos_token_ids;
std::int32_t pad_token_id;
std::int32_t max_cache_length;
std::string precision;
Expand Down Expand Up @@ -84,13 +84,16 @@ RuntimeConfig parse_runtime_config(const BundleReader& bundle) {
require_value<std::int32_t>(json, "head_dim"),
require_value<std::int32_t>(json, "vocab_size"),
require_value<std::int32_t>(json, "bos_token_id"),
require_value<std::int32_t>(json, "eos_token_id"),
require_value<std::vector<std::int32_t>>(json, "eos_token_id"),
require_value<std::int32_t>(json, "pad_token_id"),
require_value<std::int32_t>(json, "max_cache_length"),
require_value<std::string>(json, "precision"),
require_value<std::string>(json, "decoder_engine_layout"),
dynamic_kv_cache,
};
if (config.eos_token_ids.empty()) {
throw std::runtime_error("llama runtime.json has an empty 'eos_token_id' list");
}
if (config.hidden_size <= 0 || config.num_layers <= 0 || config.num_heads <= 0 ||
config.num_key_value_heads <= 0 || config.head_dim <= 0 || config.vocab_size <= 0 ||
config.max_cache_length <= 0 ||
Expand Down Expand Up @@ -225,7 +228,7 @@ ITask* create(const FamilyContext& context) {
LlamaTextGenConfig text_config;
text_config.vocab_size = config.vocab_size;
text_config.id_bos = config.bos_token_id;
text_config.id_eos = config.eos_token_id;
text_config.id_eos_ids = config.eos_token_ids;
text_config.chat_template_format =
llama_detect_chat_template_format(chat_template(context.reader));
text_config.prefill_max_length = prefill_token_limit(*modules.prefill);
Expand Down
21 changes: 21 additions & 0 deletions families/llama/tests/manifests/minicpm5-2b.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "minicpm5-2b",
"hf_id": "openbmb/MiniCPM5-2B",
"bundle": "minicpm5-2b.bundle",
"family": "llama",
"task": "text_generation",
"precision": "bf16",
"trust_remote_code": false,
"testcases": [
{
"name": "minicpm5-2b",
"reference_precision": "fp32",
"prompt": "What is the capital of France? Answer in one word.",
"max_new_tokens": 20,
"use_chat_template": true,
"enable_thinking": false
}
],
"max_sequence_length": 512,
"tensor_parallel_size": 1
}
37 changes: 37 additions & 0 deletions families/llama/tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

from families.llama.config import ModelConfig


def test_scalar_eos_token_id() -> None:
config = ModelConfig.create_tiny(model_type="llama", eos_token_id=1)

assert config.eos_token_id == 1
assert config.eos_token_ids == (1,)


def test_list_eos_token_id_preserves_every_id() -> None:
# HF configs allow eos_token_id to be a list of stop tokens (e.g. Llama
# 3.1+, MiniCPM5). Every id must reach the runtime bundle: the model may
# naturally emit any one of them as its real per-turn stop token (for
# Llama 3.1-Instruct, that's <|eot_id|>, the *last* id in the list, not
# the first), so truncating to one id would silently break stopping.
config = ModelConfig.create_tiny(model_type="llama", eos_token_id=[1, 130073])

assert config.eos_token_id == 1
assert config.eos_token_ids == (1, 130073)


def test_empty_eos_token_id_list_falls_back() -> None:
config = ModelConfig.create_tiny(model_type="llama", eos_token_id=[])

assert config.eos_token_id == -1
assert config.eos_token_ids == ()


def test_missing_eos_token_id_falls_back() -> None:
config = ModelConfig.create_tiny(model_type="llama")

assert config.eos_token_id == -1
assert config.eos_token_ids == ()
63 changes: 63 additions & 0 deletions families/llama/tests/test_runtime_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""_runtime_config()'s generation_config.json override path.

Importing families.llama.model drags in the TensorRT import chain (via
standard_decoder_builder.py), so this lives separately from the
TensorRT-free families.llama.tests.test_config.
"""

from __future__ import annotations

import json

import pytest

trt = pytest.importorskip("tensorrt")

from ..config import ModelConfig # noqa: E402
from ..model import _runtime_config # noqa: E402


pytestmark = [pytest.mark.gpu, pytest.mark.trt]


def test_generation_config_overrides_with_full_eos_list(tmp_path) -> None:
# The checkpoint's own config.json disagrees with generation_config.json
# (as real checkpoints do) so this only passes if the override path is
# actually exercised, not the config.json path.
config = ModelConfig.create_tiny(model_type="llama", eos_token_id=1)
(tmp_path / "generation_config.json").write_text(
json.dumps({"eos_token_id": [128001, 128008, 128009]}),
encoding="utf-8",
)

runtime = _runtime_config(tmp_path, config, precision="bf16", max_cache_length=128,
decoder_engine_layout="split")

assert runtime["eos_token_id"] == [128001, 128008, 128009]


def test_generation_config_empty_eos_list_falls_back(tmp_path) -> None:
# An empty override list must not reach the bundle as `null` or `[]` —
# the C++ loader rejects an empty eos_token_id list.
config = ModelConfig.create_tiny(model_type="llama", eos_token_id=1)
(tmp_path / "generation_config.json").write_text(
json.dumps({"eos_token_id": []}),
encoding="utf-8",
)

runtime = _runtime_config(tmp_path, config, precision="bf16", max_cache_length=128,
decoder_engine_layout="split")

assert runtime["eos_token_id"] == [-1]


def test_no_generation_config_keeps_config_json_eos_list(tmp_path) -> None:
config = ModelConfig.create_tiny(model_type="llama", eos_token_id=[1, 130073])

runtime = _runtime_config(tmp_path, config, precision="bf16", max_cache_length=128,
decoder_engine_layout="split")

assert runtime["eos_token_id"] == [1, 130073]
Loading