-
Notifications
You must be signed in to change notification settings - Fork 2.7k
[None][fix] two silent DeepSeek-V4 defects: prompt handoff and withheld stream text #18345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
JunyiXu-nv
wants to merge
2
commits into
NVIDIA:main
Choose a base branch
from
JunyiXu-nv:junyix/dsv4-trailing-reminder-prompt-fix
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -275,6 +275,20 @@ def _render_user_content(message: dict[str, Any]) -> str: | |
| return "\n\n".join(parts) | ||
|
|
||
|
|
||
| def _only_reminders_follow(index: int, messages: list[dict[str, Any]]) -> bool: | ||
| """True when every message after `index` is a ``latest_reminder``. | ||
|
|
||
| A reminder carries its own token but never emits a turn boundary, so when | ||
| reminders are all that remains there is no assistant message for the | ||
| preceding user turn to hand the floor to. Deferring the handoff past them | ||
| keeps the prompt ending on it; emitting it early leaves the prompt ending | ||
| inside the reminder text with the assistant turn already open, and the | ||
| model continues the document instead of answering. | ||
| """ | ||
| rest = messages[index + 1 :] | ||
| return bool(rest) and all(m.get("role") == "latest_reminder" for m in rest) | ||
|
|
||
|
|
||
| def _render_message( | ||
| index: int, | ||
| messages: list[dict[str, Any]], | ||
|
|
@@ -367,16 +381,55 @@ def _render_message( | |
| prompt += ASSISTANT_TOKEN | ||
| prompt += THINKING_START_TOKEN if thinking_mode == "thinking" else THINKING_END_TOKEN | ||
| prompt += VALID_TASKS[task] | ||
| elif role in ("user", "developer") and (next_role == "assistant" or add_generation_prompt): | ||
| elif role in ("user", "developer") and ( | ||
| next_role == "assistant" | ||
| or (add_generation_prompt and not _only_reminders_follow(index, messages)) | ||
| ): | ||
| prompt += ASSISTANT_TOKEN | ||
| if thinking_mode == "thinking" and (not drop_thinking or index >= last_user_index): | ||
| prompt += THINKING_START_TOKEN | ||
| else: | ||
| prompt += THINKING_END_TOKEN | ||
| elif role == "latest_reminder" and next_role is None and add_generation_prompt: | ||
| # Deferred from the user turn above so the prompt ends on the boundary | ||
| # rather than inside the reminder text. | ||
| prompt += ASSISTANT_TOKEN | ||
| prompt += THINKING_START_TOKEN if thinking_mode == "thinking" else THINKING_END_TOKEN | ||
|
|
||
| return prompt | ||
|
|
||
|
|
||
| def _map_trailing_system_to_reminder( | ||
| messages: list[dict[str, Any]], | ||
| ) -> list[dict[str, Any]]: | ||
| """Re-role non-leading ``system`` messages as ``latest_reminder``. | ||
|
|
||
| DeepSeek-V4 encodes the system prompt *positionally*: the opening bare-text | ||
| span before the first ``<|User|>`` is the system slot, so ``_render_message`` | ||
| emits system content with no role token at all. That is correct at the front | ||
| and degenerate anywhere else -- a mid-conversation system message renders as | ||
| unmarked text floating between two turns. | ||
|
|
||
| The format already has the right slot for out-of-band mid-conversation | ||
| content: ``latest_reminder``, which carries its own token. Clients that append | ||
| transient system messages (task reminders, background-task notifications) map | ||
| onto exactly that, so route them there instead of emitting bare text. | ||
| """ | ||
| mapped: list[dict[str, Any]] = [] | ||
| in_leading_system_run = True | ||
| for message in messages: | ||
| role = message.get("role") | ||
| if role == "system" and not in_leading_system_run: | ||
| reminder = dict(message) | ||
| reminder["role"] = "latest_reminder" | ||
| mapped.append(reminder) | ||
| continue | ||
| if role != "system": | ||
| in_leading_system_run = False | ||
| mapped.append(message) | ||
| return mapped | ||
|
|
||
|
|
||
| def _encode_messages( | ||
| messages: list[dict[str, Any]], | ||
| thinking_mode: str, | ||
|
|
@@ -386,6 +439,7 @@ def _encode_messages( | |
| ) -> str: | ||
| messages = _merge_tool_messages(messages) | ||
| messages = _sort_tool_results_by_call_order(messages) | ||
| messages = _map_trailing_system_to_reminder(messages) | ||
|
|
||
| effective_drop_thinking = drop_thinking | ||
| if any(message.get("tools") for message in messages): | ||
|
|
@@ -419,12 +473,6 @@ def from_pretrained( | |
| revision: str | None = None, | ||
| **kwargs, | ||
| ) -> "DeepseekV4Tokenizer": | ||
| # AutoTokenizer resolves the checkpoint config first, and the | ||
| # deepseek_v4 model_type is invisible to stock transformers; the | ||
| # registration is an import side effect of tensorrt_llm._torch.configs | ||
| # (previously guaranteed by the eager `import tensorrt_llm`). | ||
| import tensorrt_llm._torch.configs # noqa: F401 | ||
|
Comment on lines
-422
to
-426
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this safe to remove now? Seems like a good thing to remove if so. Guess we can rely on CI to verify |
||
|
|
||
| tokenizer = AutoTokenizer.from_pretrained( | ||
| path_or_repo_id, | ||
| *args, | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: NVIDIA/TensorRT-LLM
Length of output: 340
🏁 Script executed:
Repository: NVIDIA/TensorRT-LLM
Length of output: 50375
🏁 Script executed:
Repository: NVIDIA/TensorRT-LLM
Length of output: 21943
🏁 Script executed:
Repository: NVIDIA/TensorRT-LLM
Length of output: 10773
Defer the
task == "action"handoff when only reminders follow.When
add_generation_promptis true,_render_message()emits<|Assistant|>and<|action|>before trailinglatest_remindermessages. The final reminder then emits a second assistant boundary. Defer both task tokens to the final reminder and add an exact-output regression test. The test file is already included intests/integration/test_lists/test-db/l0_b200.yml.📍 Affects 2 files
tensorrt_llm/tokenizer/deepseek_v4/tokenizer.py#L384-L397(this comment)tests/unittest/llmapi/test_deepseek_v4_tokenizer.py#L287-L360🤖 Prompt for AI Agents
Source: Path instructions