Skip to content

Commit 06d40f5

Browse files
authored
[misc] fix: reject unsupported legacy stopping controls (#4954)
Signed-off-by: Yu Yao <yaoyu.094@gmail.com>
1 parent 551160b commit 06d40f5

2 files changed

Lines changed: 162 additions & 0 deletions

File tree

scripts/inference/text_generation.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ def add_args(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
8989
def _validate_args(args: argparse.Namespace) -> None:
9090
if args.use_legacy_generation and args.use_coordinator:
9191
raise ValueError("--use-coordinator is only supported by dynamic generation.")
92+
if args.use_legacy_generation and (args.termination_id is not None or args.stop_words is not None):
93+
raise ValueError(
94+
"MCore legacy static generation does not support --termination-id or --stop-words; "
95+
"use dynamic generation for custom stopping controls."
96+
)
9297
if args.ep > 1 and not args.use_coordinator and not args.use_legacy_generation:
9398
raise ValueError("--use-coordinator is required when --ep is greater than 1.")
9499
if (args.coordinator_host is not None or args.coordinator_port is not None) and not args.use_coordinator:
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Unit tests for the synchronous text-generation entrypoint."""
16+
17+
from __future__ import annotations
18+
19+
import importlib.util
20+
import sys
21+
import types
22+
from pathlib import Path
23+
24+
import pytest
25+
26+
27+
_REPO_ROOT = Path(__file__).resolve().parents[3]
28+
_MODULE_PATH = _REPO_ROOT / "scripts" / "inference" / "text_generation.py"
29+
30+
31+
class _PassthroughInit:
32+
def __init__(self, *args: object, **kwargs: object) -> None:
33+
self.args = args
34+
self.kwargs = kwargs
35+
36+
37+
def _module(name: str, **attrs: object) -> types.ModuleType:
38+
module = types.ModuleType(name)
39+
for attr_name, value in attrs.items():
40+
setattr(module, attr_name, value)
41+
return module
42+
43+
44+
def _add_no_args(parser: object) -> object:
45+
return parser
46+
47+
48+
@pytest.fixture
49+
def text_generation_entrypoint(monkeypatch: pytest.MonkeyPatch):
50+
shared_helpers = {
51+
"HFTokenizerAdapter": _PassthroughInit,
52+
"add_distributed_args": _add_no_args,
53+
"add_engine_args": _add_no_args,
54+
"add_model_loading_args": _add_no_args,
55+
"add_parallelism_args": _add_no_args,
56+
"add_prompt_args": _add_no_args,
57+
"add_sampling_args": _add_no_args,
58+
"build_inference_config": lambda **kwargs: kwargs,
59+
"build_sampling_params": lambda **kwargs: kwargs,
60+
"build_tokenizer": _PassthroughInit,
61+
"load_bridge_model": lambda **kwargs: kwargs,
62+
"load_prompts": lambda *args: list(args),
63+
"resolve_hf_model_path": lambda *args: args[0],
64+
"validate_sequence_length": lambda **kwargs: None,
65+
}
66+
stubs = {
67+
"megatron.core.inference.apis": _module(
68+
"megatron.core.inference.apis",
69+
MegatronLLM=_PassthroughInit,
70+
SamplingParams=_PassthroughInit,
71+
),
72+
"megatron.core.inference.contexts": _module(
73+
"megatron.core.inference.contexts",
74+
StaticInferenceContext=_PassthroughInit,
75+
),
76+
"megatron.core.inference.engines.static_engine": _module(
77+
"megatron.core.inference.engines.static_engine",
78+
StaticInferenceEngine=_PassthroughInit,
79+
),
80+
"megatron.core.inference.model_inference_wrappers.gpt.gpt_inference_wrapper": _module(
81+
"megatron.core.inference.model_inference_wrappers.gpt.gpt_inference_wrapper",
82+
GPTInferenceWrapper=_PassthroughInit,
83+
),
84+
"megatron.core.inference.text_generation_controllers.text_generation_controller": _module(
85+
"megatron.core.inference.text_generation_controllers.text_generation_controller",
86+
TextGenerationController=_PassthroughInit,
87+
),
88+
"megatron.bridge.inference.text_generation": _module(
89+
"megatron.bridge.inference.text_generation",
90+
**shared_helpers,
91+
),
92+
"megatron.bridge.utils.activation_map": _module(
93+
"megatron.bridge.utils.activation_map",
94+
str_to_dtype=lambda value: value,
95+
),
96+
"megatron.bridge.utils.common_utils": _module(
97+
"megatron.bridge.utils.common_utils",
98+
maybe_initialize_distributed=lambda timeout: timeout,
99+
print_rank_0=lambda message: message,
100+
),
101+
}
102+
for name, module in stubs.items():
103+
monkeypatch.setitem(sys.modules, name, module)
104+
105+
spec = importlib.util.spec_from_file_location("text_generation_entrypoint_under_test", _MODULE_PATH)
106+
assert spec is not None and spec.loader is not None
107+
module = importlib.util.module_from_spec(spec)
108+
sys.modules[spec.name] = module
109+
try:
110+
spec.loader.exec_module(module)
111+
yield module
112+
finally:
113+
sys.modules.pop(spec.name, None)
114+
115+
116+
def _args(**overrides: object) -> types.SimpleNamespace:
117+
values = {
118+
"use_legacy_generation": True,
119+
"use_coordinator": False,
120+
"ep": 1,
121+
"coordinator_host": None,
122+
"coordinator_port": None,
123+
"top_n_logprobs": 0,
124+
"return_log_probs": False,
125+
"distributed_timeout_minutes": 10,
126+
"termination_id": None,
127+
"stop_words": None,
128+
}
129+
values.update(overrides)
130+
return types.SimpleNamespace(**values)
131+
132+
133+
@pytest.mark.unit
134+
@pytest.mark.parametrize(
135+
"stopping_override",
136+
[
137+
{"termination_id": 42},
138+
{"stop_words": ["<END>"]},
139+
],
140+
)
141+
def test_legacy_static_rejects_unsupported_stopping_controls(
142+
text_generation_entrypoint: types.ModuleType,
143+
stopping_override: dict[str, object],
144+
) -> None:
145+
with pytest.raises(ValueError, match="legacy static generation does not support"):
146+
text_generation_entrypoint._validate_args(_args(**stopping_override))
147+
148+
149+
@pytest.mark.unit
150+
def test_dynamic_generation_accepts_stopping_controls(text_generation_entrypoint: types.ModuleType) -> None:
151+
text_generation_entrypoint._validate_args(
152+
_args(
153+
use_legacy_generation=False,
154+
termination_id=42,
155+
stop_words=["<END>"],
156+
)
157+
)

0 commit comments

Comments
 (0)