Skip to content
Merged
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
21 changes: 21 additions & 0 deletions families/convbert/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,13 +262,34 @@ def _positive_int(value: object, name: str) -> int:
def _tokenizer_runtime_contract(model_dir: Path) -> dict[str, object]:
"""Resolve this family's exact native-tokenizer framing."""

import tempfile

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained(
str(model_dir),
trust_remote_code=True,
use_fast=True,
)
tokenizer_path = model_dir / "tokenizer.json"
if not tokenizer_path.is_file():
temporary_path: Path | None = None
try:
with tempfile.NamedTemporaryFile(
dir=model_dir,
prefix=".trtmc-convbert-tokenizer-",
suffix=".json",
delete=False,
) as output:
temporary_path = Path(output.name)
tokenizer.backend_tokenizer.save(str(temporary_path))
if not temporary_path.is_file() or temporary_path.stat().st_size == 0:
raise RuntimeError("tokenizer conversion did not create tokenizer.json")
temporary_path.replace(tokenizer_path)
temporary_path = None
finally:
if temporary_path is not None:
temporary_path.unlink(missing_ok=True)
default_ids = list(tokenizer.encode("hello"))
plain_ids = list(tokenizer.encode("hello", add_special_tokens=False))
if default_ids == plain_ids:
Expand Down
40 changes: 40 additions & 0 deletions families/convbert/tests/test_tokenizer_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""ConvBERT tokenizer bundle regression coverage."""

from __future__ import annotations

import sys
from pathlib import Path
from types import SimpleNamespace

import pytest

pytest.importorskip("tensorrt", reason="TensorRT is required for family builder tests")

from families.convbert import model


def test_runtime_contract_materializes_missing_fast_tokenizer(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
class Backend:
def save(self, path: str) -> None:
Path(path).write_text('{"model":{"type":"WordPiece"}}', encoding="utf-8")

class Tokenizer:
backend_tokenizer = Backend()

@staticmethod
def encode(_text: str, add_special_tokens: bool = True) -> list[int]:
return [101, 42, 102] if add_special_tokens else [42]

auto = SimpleNamespace(from_pretrained=lambda *_args, **_kwargs: Tokenizer())
monkeypatch.setitem(sys.modules, "transformers", SimpleNamespace(AutoTokenizer=auto))

contract = model._tokenizer_runtime_contract(tmp_path)

assert (tmp_path / "tokenizer.json").is_file()
assert contract["tokenizer_prefix_ids"] == [101]
assert contract["tokenizer_suffix_ids"] == [102]
21 changes: 21 additions & 0 deletions families/deberta/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,13 +629,34 @@ def _positive_int(value: object, name: str) -> int:
def _tokenizer_runtime_contract(model_dir: Path) -> dict[str, object]:
"""Resolve this family's exact native-tokenizer framing."""

import tempfile

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained(
str(model_dir),
trust_remote_code=True,
use_fast=True,
)
tokenizer_path = model_dir / "tokenizer.json"
if not tokenizer_path.is_file():
temporary_path: Path | None = None
try:
with tempfile.NamedTemporaryFile(
dir=model_dir,
prefix=".trtmc-deberta-tokenizer-",
suffix=".json",
delete=False,
) as output:
temporary_path = Path(output.name)
tokenizer.backend_tokenizer.save(str(temporary_path))
if not temporary_path.is_file() or temporary_path.stat().st_size == 0:
raise RuntimeError("tokenizer conversion did not create tokenizer.json")
temporary_path.replace(tokenizer_path)
temporary_path = None
finally:
if temporary_path is not None:
temporary_path.unlink(missing_ok=True)
default_ids = list(tokenizer.encode("hello"))
plain_ids = list(tokenizer.encode("hello", add_special_tokens=False))
if default_ids == plain_ids:
Expand Down
40 changes: 40 additions & 0 deletions families/deberta/tests/test_tokenizer_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""DeBERTa tokenizer bundle regression coverage."""

from __future__ import annotations

import sys
from pathlib import Path
from types import SimpleNamespace

import pytest

pytest.importorskip("tensorrt", reason="TensorRT is required for family builder tests")

from families.deberta import model


def test_runtime_contract_materializes_missing_fast_tokenizer(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
class Backend:
def save(self, path: str) -> None:
Path(path).write_text('{"model":{"type":"BPE"}}', encoding="utf-8")

class Tokenizer:
backend_tokenizer = Backend()

@staticmethod
def encode(_text: str, add_special_tokens: bool = True) -> list[int]:
return [1, 42, 2] if add_special_tokens else [42]

auto = SimpleNamespace(from_pretrained=lambda *_args, **_kwargs: Tokenizer())
monkeypatch.setitem(sys.modules, "transformers", SimpleNamespace(AutoTokenizer=auto))

contract = model._tokenizer_runtime_contract(tmp_path)

assert (tmp_path / "tokenizer.json").is_file()
assert contract["tokenizer_prefix_ids"] == [1]
assert contract["tokenizer_suffix_ids"] == [2]
26 changes: 22 additions & 4 deletions families/deepseek_ocr/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

from typing import TYPE_CHECKING

import gc
import sys
from pathlib import Path

Expand Down Expand Up @@ -1343,6 +1344,8 @@ def _build_sam_attention(

def _load_vision_weights(
model_dir: str,
*,
precision: str,
) -> dict[str, np.ndarray]:
"""Load all vision pipeline weights from safetensors."""
readers = _open_safetensors(Path(model_dir))
Expand Down Expand Up @@ -1474,7 +1477,13 @@ def _load_vision_weights(
# --- View separator ---
vw["view_sep"] = _load_tensor(readers, "model.view_seperator").astype(np.float32)

return vw
if precision == "fp16":
return {name: np.ascontiguousarray(value, dtype=np.float16) for name, value in vw.items()}
if precision == "fp32":
return vw
raise ValueError(
f"Unsupported DeepSeek-OCR vision precision {precision!r}; expected fp32 or fp16"
)


def _build_deepseek_ocr_vision_engine(
Expand All @@ -1493,7 +1502,6 @@ def _build_deepseek_ocr_vision_engine(
"""
print("[trtmc build] Building DeepSeek-OCR-2 vision engine (native TRT) ...", file=sys.stderr)

vw = _load_vision_weights(model_dir)
if precision == "fp16":
work_np_dtype, work_trt_dtype = np.float16, trt.float16
elif precision == "fp32":
Expand All @@ -1502,6 +1510,7 @@ def _build_deepseek_ocr_vision_engine(
raise ValueError(
f"Unsupported DeepSeek-OCR vision precision {precision!r}; expected fp32 or fp16"
)
vw = _load_vision_weights(model_dir, precision=precision)

# SAM config
sam_hidden = 768
Expand Down Expand Up @@ -2042,6 +2051,9 @@ def build(request: "BuildRequest", writer: "BundleWriter") -> None:
verbose=request.verbose,
parallel_config=parallel,
)
writer.add_bytes("prefill.plan", prefill)
del prefill
gc.collect()
config.raw["_decoder_engine_role"] = "decode"
decode = model.build_engine(
config,
Expand All @@ -2052,9 +2064,15 @@ def build(request: "BuildRequest", writer: "BundleWriter") -> None:
verbose=request.verbose,
parallel_config=parallel,
)
config.raw.pop("_decoder_engine_role", None)
writer.add_bytes("engine.plan", decode)
writer.add_bytes("prefill.plan", prefill)
del decode
gc.collect()
config.raw.pop("_decoder_engine_role", None)
# Vision weights are an independent checkpoint subset. Release the decoder
# arrays before loading it so three full model representations do not
# overlap during a single-device bundle build.
weights.clear()
gc.collect()
vision = model.build_vision_engine(
str(model_dir), config, weights, precision=precision, verbose=request.verbose
)
Expand Down
77 changes: 76 additions & 1 deletion families/deepseek_ocr/tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def build_engine(self, loaded_config, weights, max_length, **options):
def build_vision_engine(self, model_dir, loaded_config, weights, **options):
assert model_dir == str(tmp_path)
assert loaded_config is config
assert weights == {"weights": True}
assert weights == {}
assert options == {"precision": "fp32", "verbose": False}
writer.events.append("build:vision")
vision_calls.append((model_dir, loaded_config, weights, options))
Expand Down Expand Up @@ -137,3 +137,78 @@ def get_vl_config(self, loaded_config):
]
assert writer.json["runtime.json"]["tensor_parallel_size"] == 2
assert "_decoder_engine_role" not in config.raw


def test_single_device_build_streams_each_decoder_plan_before_the_next_build(
monkeypatch, tmp_path
) -> None:
config = SimpleNamespace(
model_type="deepseek_vl_v2",
max_position_embeddings=4096,
num_hidden_layers=2,
vocab_size=32,
bos_token_id=1,
eos_token_id=2,
hidden_size=8,
raw={},
)
writer = _Writer()

class FakeModel:
def load_weights(self, *_args, **_kwargs):
return {"decoder": object()}

def build_engine(self, _config, weights, _max_length, **_options):
role = config.raw["_decoder_engine_role"]
assert weights
writer.events.append(f"build:{role}")
if role == "decode":
assert "write:prefill.plan" in writer.events
return role.encode()

def build_vision_engine(self, _model_dir, _config, weights, **_options):
assert weights == {}
writer.events.append("build:vision")
return b"vision"

def get_vl_config(self, _config):
return {
"image_token_id": 7,
"vision_output_dim": 8,
"prefill_max_length": 64,
}

monkeypatch.setattr(
model_module, "ModelConfig", SimpleNamespace(from_dir=lambda _model_dir: config)
)
monkeypatch.setattr(model_module, "_DeepseekOcrModel", FakeModel)
request = SimpleNamespace(
model_dir=tmp_path,
backend="trt",
dynamic_kv_cache=False,
task="vision_language_generation",
precision="fp16",
max_sequence_length=4096,
tensor_parallel_size=1,
context_parallel_size=1,
quantization=None,
fp32_layers=(1, 2),
image_height=None,
image_width=None,
video_num_frames=None,
max_batch_size=1,
verbose=False,
)

model_module.build(request, writer)

assert writer.events == [
"write:header",
"build:prefill",
"write:prefill.plan",
"build:decode",
"write:engine.plan",
"build:vision",
"write:vision.plan",
]
assert "_decoder_engine_role" not in config.raw
Loading
Loading