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
32 changes: 22 additions & 10 deletions tests/integration/defs/accuracy/test_llm_api_pytorch.py
Original file line number Diff line number Diff line change
Expand Up @@ -7178,20 +7178,22 @@ class TestNemotron35Lightning(LlmapiAccuracyTestHarness):
EXTRA_EVALUATOR_KWARGS = dict(chat_template_kwargs=dict(
enable_thinking=False))

@skip_no_hopper
def test_nvfp4_marlin_mtp3_chunked_prefill(self):
"""Single-GPU Hopper guard for the Marlin NVFP4 path.
def _run_mtp3_chunked_prefill(self, moe_backend, nvfp4_gemm_config=None):
"""Evaluate the MTP=3 + chunked-prefill combination on one MoE backend.

The checkpoint is MIXED_PRECISION: routed experts, shared experts and
lm_head are W4A16_NVFP4, the Mamba projections are FP8 and the MTP
layers are left unquantized. ``moe_config.backend=MARLIN`` plus
``nvfp4_gemm_config.allowed_backends=['marlin']`` pin both the MoE and
the dense NVFP4 GEMMs to Marlin, which is Ada/Hopper only. Chunked
prefill, CUDA graphs and the overlap scheduler are enabled together so
the combination with MTP drafting is covered end to end.
layers are left unquantized. Chunked prefill, CUDA graphs and the
overlap scheduler are enabled together so the combination with MTP
drafting is covered end to end. Only the backend pinning differs
between callers; everything else is held fixed so the two runs are
comparable against the same accuracy references.
"""
max_batch_size = 32
mtp_config = MTPDecodingConfig(max_draft_len=3)
extra_args = {}
if nvfp4_gemm_config is not None:
extra_args["nvfp4_gemm_config"] = nvfp4_gemm_config
with LLM(
self.MODEL_PATH,
kv_cache_config=KvCacheConfig(
Expand All @@ -7207,9 +7209,9 @@ def test_nvfp4_marlin_mtp3_chunked_prefill(self):
cuda_graph_config=CudaGraphConfig(max_batch_size=max_batch_size,
enable_padding=True),
disable_overlap_scheduler=False,
moe_config=MoeConfig(backend="MARLIN"),
nvfp4_gemm_config={"allowed_backends": ["marlin"]},
moe_config=MoeConfig(backend=moe_backend),
speculative_config=mtp_config,
**extra_args,
) as llm:
assert llm.args.quant_config.quant_algo == QuantAlgo.MIXED_PRECISION
task = MMLU(self.MODEL_NAME)
Expand All @@ -7219,6 +7221,16 @@ def test_nvfp4_marlin_mtp3_chunked_prefill(self):
task.evaluate(llm,
extra_evaluator_kwargs=self.EXTRA_EVALUATOR_KWARGS)

@skip_no_hopper
Comment thread
JennyLiu-nv marked this conversation as resolved.
def test_nvfp4_marlin_mtp3_chunked_prefill(self):
self._run_mtp3_chunked_prefill(
moe_backend="MARLIN",
nvfp4_gemm_config={"allowed_backends": ["marlin"]})

@skip_pre_blackwell
def test_nvfp4_cutedsl_mtp3_chunked_prefill(self):
self._run_mtp3_chunked_prefill(moe_backend="CUTEDSL")


@skip_pre_blackwell
class TestMiniMaxM3(LlmapiAccuracyTestHarness):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
kv_cache_config:
dtype: fp8
enable_block_reuse: false
mamba_state_config:
periodic_snapshot_interval: 8192
free_gpu_memory_fraction: 0.8
mamba_ssm_cache_dtype: float16
mamba_ssm_stochastic_rounding: true
mamba_ssm_philox_rounds: 5
cuda_graph_config:
enable_padding: true
max_batch_size: 16
speculative_config:
decoding_type: MTP
max_draft_len: 3
allow_advanced_sampling: true
enable_chunked_prefill: true
num_postprocess_workers: 4
print_iter_log: true
stream_interval: 10
disable_overlap_scheduler: false
230 changes: 165 additions & 65 deletions tests/integration/defs/examples/serve/test_serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,81 @@ def check_openai_chat_completion(http_port,
raise


_SHORT_PROMPT = "What is the capital of France?"
_LONG_PROMPT = ("Please summarize the following passage in one sentence.\n\n" +
("The quick brown fox jumps over the lazy dog. " * 600))


def check_mixed_prompt_batch(client,
model_name,
join_timeout=300,
require_reasoning=True):
"""Send 1 long + 3 short chat requests at once and check every reply.

Args:
require_reasoning: Require non-empty ``reasoning_content``; set False
where an empty thinking step is acceptable.
"""
results = {}
errors = {}

def _complete(label, prompt):
try:
resp = client.chat.completions.create(
model=model_name,
messages=[{
"role": "user",
"content": prompt
}],
max_completion_tokens=1024,
stream=False,
)
assert len(resp.choices) == 1, \
f"[{label}] expected 1 choice, got {len(resp.choices)}"
results[label] = resp.choices[0].message
except Exception as exc:
errors[label] = str(exc)

threads = [
threading.Thread(name="long_0",
target=_complete,
args=("long_0", _LONG_PROMPT),
daemon=True)
]
for i in range(3):
threads.append(
threading.Thread(name=f"short_{i}",
target=_complete,
args=(f"short_{i}", _SHORT_PROMPT),
daemon=True))

for t in threads:
t.start()
for t in threads:
t.join(timeout=join_timeout)

hung = [t.name for t in threads if t.is_alive()]
assert not hung, f"Timed out waiting for request threads: {hung}"
assert not errors, f"Requests failed: {errors}"
assert "long_0" in results, "Long prompt (chunked prefill) got no response"
assert all(f"short_{i}" in results
for i in range(3)), "One or more short prompts got no response"

for label, msg in results.items():
reasoning = msg.reasoning_content or ""
content = msg.content or ""
if require_reasoning:
# content may be empty if the token budget was consumed by
# thinking, which is expected model behaviour, not a server error.
assert len(reasoning) > 0, \
f"[{label}] empty reasoning_content — request did not complete"
else:
assert len(reasoning) + len(content) > 0, \
f"[{label}] empty response — request did not complete"
print_info(f"[{label}] reasoning: {reasoning!r}")
print_info(f"[{label}] content: {content!r}")


@pytest.mark.parametrize("config_flag", ["--extra_llm_api_options", "--config"])
@skip_no_hopper
def test_config_file_loading(serve_test_root, config_flag):
Expand Down Expand Up @@ -298,77 +373,102 @@ def test_nemotron3_super_120b_nvfp4(serve_test_root):
api_key="tensorrt_llm",
)

# Short prompt: exercises MTP decode path.
short_prompt = "What is the capital of France?"
with popen(cmd, env=env) as proc:
_wait_for_server_ready(proc, http_port=port, timeout=7200)
print_info("Server ready — sending mixed short+long prompt batch...")
check_mixed_prompt_batch(client, model_name)

print_info("test_nemotron3_super_120b_nvfp4 PASSED")


_NEMOTRON35_LIGHTNING_COMMON_CONFIG = "Nemotron35_Lightning_30B.yml"
_NEMOTRON35_LIGHTNING_VARIANTS = {
"nvfp4": {
"model_dir": "NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4",
"config_overrides": {
"moe_config": {
"backend": "CUTEDSL"
},
"nvfp4_gemm_config": {
"allowed_backends":
["marlin", "cutlass", "cublaslt", "cuda_core"]
},
},
},
"bf16": {
"model_dir": "NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16",
"config_overrides": {
"moe_config": {
"backend": "CUTLASS"
},
},
},
}


@skip_pre_blackwell
@pytest.mark.parametrize("precision", list(_NEMOTRON35_LIGHTNING_VARIANTS))
def test_nemotron35_lightning_30b(serve_test_root, tmp_path, precision):
"""Nemotron 3.5 Lightning 30B-A3B with chunked prefill + MTP=3."""
variant = _NEMOTRON35_LIGHTNING_VARIANTS[precision]
model_path = f"{llm_models_root()}/{variant['model_dir']}"
common_config = (f"{serve_test_root}/test_configs/"
f"{_NEMOTRON35_LIGHTNING_COMMON_CONFIG}")

assert os.path.exists(model_path), f"Model not found: {model_path}"
assert os.path.exists(common_config), f"Config not found: {common_config}"

with open(common_config) as f:
config = yaml.safe_load(f)
overlapping = set(config) & set(variant["config_overrides"])
assert not overlapping, \
f"[{precision}] overrides would replace common settings: {overlapping}"
config.update(variant["config_overrides"])

config_file = str(tmp_path / f"nemotron35_lightning_{precision}.yml")
with open(config_file, "w") as f:
yaml.dump(config, f)
print_info(f"[{precision}] merged serve config:\n{yaml.dump(config)}")

# Long prompt: 3000+ words to exceed max_num_tokens (8192 tokens) and
# force chunked prefill. The content is repeated to guarantee length.
long_prompt = (
"Please summarize the following passage in one sentence.\n\n" +
("The quick brown fox jumps over the lazy dog. " * 600))
port = get_free_port_in_ci()
env = os.environ.copy()
env["TLLM_ALLOW_LONG_MAX_MODEL_LEN"] = "1"

cmd = [
"trtllm-serve",
model_path,
"--host",
"0.0.0.0",
"--port",
str(port),
"--max_batch_size",
"8",
"--max_num_tokens",
"8192",
"--trust_remote_code",
"--reasoning_parser",
"nemotron-v3",
"--tool_parser",
"qwen3_coder",
"--config",
config_file,
]

model_name = os.path.basename(model_path)
client = OpenAI(
base_url=f"http://localhost:{port}/v1",
api_key="tensorrt_llm",
)

with popen(cmd, env=env) as proc:
_wait_for_server_ready(proc, http_port=port, timeout=7200)
print_info("Server ready — sending mixed short+long prompt batch...")
check_mixed_prompt_batch(client,
model_name,
join_timeout=600,
require_reasoning=False)

# Send all requests in parallel threads so the server batches them.
results = {}
errors = {}

def _complete(label, prompt):
try:
resp = client.chat.completions.create(
model=model_name,
messages=[{
"role": "user",
"content": prompt
}],
max_completion_tokens=1024,
stream=False,
)
assert len(resp.choices) == 1, \
f"[{label}] expected 1 choice, got {len(resp.choices)}"
results[label] = resp.choices[0].message
except Exception as exc:
errors[label] = str(exc)

threads = []
# 1 long-prompt request + 3 short-prompt requests sent simultaneously.
threads.append(
threading.Thread(name="long_0",
target=_complete,
args=("long_0", long_prompt),
daemon=True))
for i in range(3):
threads.append(
threading.Thread(name=f"short_{i}",
target=_complete,
args=(f"short_{i}", short_prompt),
daemon=True))

for t in threads:
t.start()
for t in threads:
t.join(timeout=300)

hung = [t.name for t in threads if t.is_alive()]
assert not hung, f"Timed out waiting for request threads: {hung}"
assert not errors, f"Requests failed: {errors}"
assert "long_0" in results, "Long prompt (chunked prefill) got no response"
assert all(
f"short_{i}" in results
for i in range(3)), "One or more short prompts got no response"

for label, msg in results.items():
# A valid reasoning-model response must have reasoning_content.
# content may be empty if the token budget was consumed by thinking,
# which is expected model behaviour, not a server error.
assert len(msg.reasoning_content) > 0, \
f"[{label}] empty reasoning_content — request did not complete"
print_info(f"[{label}] reasoning: {msg.reasoning_content!r}")
print_info(f"[{label}] content: {msg.content!r}")

print_info("test_nemotron3_super_120b_nvfp4 PASSED")
print_info(f"test_nemotron35_lightning_30b[{precision}] PASSED")


_NEMOTRON3_NANO_OMNI_MODEL_DIR = "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-NVFP4"
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/defs/perf/_model_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
# Nemotron-3-Nano-Omni-30B (text + image multimodal)
"nemotron_3_nano_omni_nvfp4": "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-NVFP4",
"nemotron_3_nano_omni_nvfp4_image": "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-NVFP4",
"nemotron_3.5_lightning_30b_nvfp4_mtp": "NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4",
"nemotron_3.5_lightning_30b_bf16_mtp": "NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16",
# MiniMax M3 (block-sparse MoE, MXFP8 weights, BF16 activations + KV cache)
"minimax_m3_mxfp8": "MiniMax-M3-MXFP8",
# Qwen3.5 dense + MoE
Expand Down
48 changes: 48 additions & 0 deletions tests/integration/defs/perf/pytorch_model_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,54 @@ def get_model_yaml_config(model_label: str,
},
}
},
# Nemotron-3.5-Lightning-30B with MTP=3, NVFP4 and BF16.
{
'patterns': [
'nemotron_3.5_lightning_30b_nvfp4_mtp-serve-pytorch-streaming-',
'nemotron_3.5_lightning_30b_bf16_mtp-serve-pytorch-streaming-',
],
'config': {
'enable_chunked_prefill': True,
'stream_interval': 10,
'num_postprocess_workers': 4,
'cuda_graph_config': {
'enable_padding': True,
'max_batch_size': 16,
},
'kv_cache_config': {
'enable_block_reuse': False,
'free_gpu_memory_fraction': 0.8,
'mamba_ssm_cache_dtype': 'float16',
'mamba_ssm_stochastic_rounding': True,
'mamba_ssm_philox_rounds': 5,
'mamba_state_config': {
'periodic_snapshot_interval': 8192,
},
},
'speculative_config': {
'decoding_type': 'MTP',
'max_draft_len': 3,
},
}
},
{
'patterns':
['nemotron_3.5_lightning_30b_nvfp4_mtp-serve-pytorch-streaming-'],
'config': {
'moe_config': {
'backend': 'CUTEDSL',
},
}
},
{
'patterns':
['nemotron_3.5_lightning_30b_bf16_mtp-serve-pytorch-streaming-'],
'config': {
'moe_config': {
'backend': 'CUTLASS',
},
}
},
# Nemotron-3-Super-120B-NVFP4 (streaming/low-latency variant for spark perf)
# Streaming serve cases use small cuda_graph batch and no attention DP for latency.
{
Expand Down
Loading
Loading