Skip to content

Commit 66966ff

Browse files
Merge commit 'b010cef540ccae9d207aed2dbacb1b12ecc5f654' into chore/bump-mcore-main-260813
Signed-off-by: svcnemo-autobot <svcnemo-autobot@nvidia.com>
2 parents e18be3a + b010cef commit 66966ff

29 files changed

Lines changed: 1455 additions & 35 deletions

‎.github/actions/test-template/action.yml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ inputs:
5959
runner:
6060
description: "Runner to use for test"
6161
required: true
62+
shard-id:
63+
description: "Zero-based pytest shard index"
64+
required: false
65+
default: ""
66+
num-shards:
67+
description: "Total number of pytest shards"
68+
required: false
69+
default: ""
6270

6371
runs:
6472
using: "composite"
@@ -141,6 +149,8 @@ runs:
141149
--env NEMO_HOME=$NEMO_HOME \
142150
--env RUN_ID=${{ github.run_id }} \
143151
--env UV_NO_SYNC=1 \
152+
--env SHARD_ID=${{ inputs.shard-id }} \
153+
--env NUM_SHARDS=${{ inputs.num-shards }} \
144154
--workdir /opt/Megatron-Bridge \
145155
$VOLUME_ARGS \
146156
${{ inputs.container-image }} \

‎.github/scripts/test_docker_dependency_layers.sh‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,12 @@ if ! grep -A1 '^te = \[$' pyproject.toml | grep -Fxq ' "megatron-core[te]",';
236236
echo "Bridge's TE extra must enable the selected MCore ref's TE extra" >&2
237237
exit 1
238238
fi
239-
grep -Fq '{ name = "megatron-core", extra = ["te"] }' uv.lock
239+
if ! grep -Fq \
240+
'{ name = "megatron-core", extras = ["te"], marker = "extra == '\''te'\''", editable = "3rdparty/Megatron-LM" }' \
241+
uv.lock; then
242+
echo "Bridge's lock metadata must preserve the MCore TE extra" >&2
243+
exit 1
244+
fi
240245
if grep -q 'transformer-engine @ git+https://github.com/NVIDIA/TransformerEngine.git@' pyproject.toml || \
241246
grep -q '^name = "transformer-engine"$' pyproject.toml; then
242247
echo "Bridge must inherit the TransformerEngine source and metadata from the selected MCore ref" >&2

‎.github/workflows/cicd-main.yml‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,10 @@ jobs:
567567
--skip-file /opt/import-check/import_check_skip.txt
568568
569569
cicd-unit-tests-core:
570+
strategy:
571+
fail-fast: false
572+
matrix:
573+
shard-id: [0, 1, 2]
570574
if: |
571575
(
572576
success()
@@ -579,7 +583,7 @@ jobs:
579583
&& (github.event.inputs.test_suite == '' || github.event.inputs.test_suite == 'all' || github.event.inputs.test_suite == 'unit-only' || contains('L0 L1 L2', github.event.inputs.test_suite))
580584
needs: [pre-flight, configure, cicd-container-build]
581585
runs-on: ${{ needs.pre-flight.outputs.runner_prefix }}
582-
name: Launch_Unit_Tests_Core
586+
name: Launch_Unit_Tests_Core_${{ matrix.shard-id }}
583587
env:
584588
TRANSFORMERS_OFFLINE: "1"
585589
HF_HUB_OFFLINE: "1"
@@ -603,6 +607,8 @@ jobs:
603607
script: Launch_Unit_Tests_Core
604608
timeout: 30
605609
is_unit_test: "true"
610+
shard-id: ${{ matrix.shard-id }}
611+
num-shards: 3
606612
github-token: ${{ steps.app-token.outputs.token }}
607613
container-image: ${{ needs.pre-flight.outputs.registry }}/megatron-bridge:${{ needs.configure.outputs.merge_sha }}
608614
test-data-path: ${{ needs.pre-flight.outputs.test_data_path }}

‎.github/workflows/claude-review.yml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ jobs:
2424
auto-review:
2525
name: Claude Review (auto)
2626
needs: [pre-flight]
27-
# Auto-review non-draft PRs from NVIDIA authors on open / reopen / ready-for-review.
27+
# Auto-review same-repository, non-draft PRs from NVIDIA authors on open / reopen / ready-for-review.
28+
# Fork pull_request workflows cannot access the secrets or OIDC token required by the review action.
2829
# Subsequent commits do not re-trigger; re-request a review by commenting
2930
# "/claude review" (handled by the manual-review job below).
3031
# Skip bot-generated PRs: dependency bumps (`bump-ci-container-*`) and
3132
# auto cherry-picks (`cherry-pick-*`).
3233
if: |
3334
github.event_name == 'pull_request' &&
35+
github.event.pull_request.head.repo.full_name == github.repository &&
3436
needs.pre-flight.outputs.is_member == 'true' &&
3537
github.event.pull_request.draft == false &&
3638
!startsWith(github.event.pull_request.head.ref, 'bump-ci-container-') &&

‎docs/models/deepseek/deepseek-v4.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,30 @@
44

55
DeepSeek V4 models are supported via the Bridge system with auto-detected configuration and weight mapping.
66

7+
## Chat SFT preprocessing
8+
9+
`DirectHFSFTDatasetConfig` automatically selects the DeepSeek V4 chat collator
10+
from the tokenizer's `name_or_path`. Because the official V4 tokenizer does not
11+
provide a Jinja chat template, each training row must select the rendering mode
12+
explicitly with either `enable_thinking` or `thinking_mode`:
13+
14+
```json
15+
{
16+
"messages": [
17+
{"role": "user", "content": "Solve 1 + 1."},
18+
{"role": "assistant", "reasoning_content": "Add the terms.", "content": "2"}
19+
],
20+
"enable_thinking": true,
21+
"truncate_history_thinking": false
22+
}
23+
```
24+
25+
Use `enable_thinking: false` for chat mode. Historical reasoning is truncated by
26+
default in thinking mode; set `truncate_history_thinking: false` to retain it.
27+
OpenAI-format tool definitions can be
28+
provided in the row's top-level `tools` field, and tool calls/results remain in
29+
the structured `messages` list.
30+
731
## Model Architecture Features
832

933
- **Hybrid Attention (DSv4HybridSelfAttention)**: Per-layer mix of dense MLA and Compressed Sparse Attention selected by `compress_ratios`

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ test = [
201201
"pytest-timeout>=2.4.0",
202202
"pytest-mock>=3.14.0",
203203
"pytest-runner>=6.0.1",
204+
"pytest-shard>=0.1.2",
204205
"pygithub",
205206
"click",
206207
]

‎scripts/inference/async_text_generation.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ async def _generate(
116116
) as llm:
117117
if llm.is_primary_rank:
118118
results = await asyncio.gather(*(llm.generate(prompt, sampling_params) for prompt in prompts))
119+
failed_results = [result for result in results if result.failed()]
120+
if failed_results:
121+
details = ", ".join(f"request {result.request_id}={result.status.name}" for result in failed_results)
122+
raise RuntimeError(f"Async inference failed: {details}")
119123
print_rank_0("======== ASYNC GENERATED TEXT OUTPUT ========")
120124
for idx, result in enumerate(results):
121125
print_rank_0(f"[{idx}] Prompt: {prompts[idx]}")

‎src/megatron/bridge/data/builders/direct_hf_sft.py‎

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@
5151
logger = logging.getLogger(__name__)
5252

5353
CollateFunction = Callable[..., dict[str, torch.Tensor]]
54+
_DEEPSEEK_V4_TOKENIZER_SIZE = 129280
55+
_DEEPSEEK_V4_ASSISTANT_TOKEN = "<|Assistant|>"
56+
_DEEPSEEK_V4_ASSISTANT_TOKEN_ID = 128804
5457

5558

5659
def _as_source(entry: "HFDatasetSourceConfig | Mapping[str, Any]") -> HFDatasetSourceConfig:
@@ -257,6 +260,26 @@ def select_direct_hf_sft_collate(
257260
raise ValueError("Prompt-completion preprocessing supports text-only examples.")
258261

259262

263+
def _model_collate_key(processor: Any) -> str:
264+
tokenizer = get_processor_tokenizer(processor)
265+
name_or_path = getattr(tokenizer, "name_or_path", "")
266+
convert_tokens_to_ids = getattr(tokenizer, "convert_tokens_to_ids", None)
267+
try:
268+
has_deepseek_v4_fingerprint = len(tokenizer) == _DEEPSEEK_V4_TOKENIZER_SIZE and (
269+
callable(convert_tokens_to_ids)
270+
and convert_tokens_to_ids(_DEEPSEEK_V4_ASSISTANT_TOKEN) == _DEEPSEEK_V4_ASSISTANT_TOKEN_ID
271+
)
272+
except TypeError:
273+
has_deepseek_v4_fingerprint = False
274+
if (
275+
isinstance(name_or_path, str)
276+
and "deepseek-v4" in name_or_path.lower().replace("_", "-")
277+
and has_deepseek_v4_fingerprint
278+
):
279+
return "deepseek-v4"
280+
return type(processor).__name__
281+
282+
260283
def load_direct_hf_sft_train_examples(config: DirectHFSFTDatasetConfig) -> list[dict[str, Any]]:
261284
"""Load the training rows, blending them when the config names several sources."""
262285
sources = config.training_sources
@@ -278,17 +301,24 @@ def build_direct_hf_sft_split(
278301
"""Build one requested direct-HF SFT split."""
279302
if target_length <= 0:
280303
return None
281-
from megatron.bridge.data.collators.registry import model_collate_required_for_all_examples
304+
from megatron.bridge.data.collators.registry import always_use_model_collate, resolve_model_collate
282305

283306
if examples is None:
284307
examples = load_direct_hf_sft_examples(source, config.preprocessing)
285-
if collate_impl is None and model_collate_required_for_all_examples(type(processor).__name__):
308+
collate_key = _model_collate_key(processor)
309+
if collate_impl is None and always_use_model_collate(collate_key):
286310
if not isinstance(config.preprocessing, ChatSFTPreprocessingConfig):
287311
raise ValueError(
288312
f"Processor type '{type(processor).__name__}' requires chat preprocessing through its "
289313
"model-owned collator."
290314
)
291-
selected_collate = None
315+
if collate_key == "deepseek-v4":
316+
selected_collate = partial(
317+
resolve_model_collate(collate_key),
318+
loss_mode=config.preprocessing.loss_mode,
319+
)
320+
else:
321+
selected_collate = None
292322
else:
293323
selected_collate = select_direct_hf_sft_collate(examples, config.preprocessing, collate_impl)
294324
return DirectSFTDataset(

‎src/megatron/bridge/data/collators/registry.py‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
class _ModelCollateSpec:
2626
module_name: str
2727
symbol_name: str
28-
required_for_all_examples: bool = False
28+
always_use_model_collate: bool = False
2929

3030

3131
_MODEL_COLLATE_SPECS = {
@@ -42,7 +42,7 @@ class _ModelCollateSpec:
4242
"NemotronH_Nano_Omni_Reasoning_V3Processor": _ModelCollateSpec(
4343
"megatron.bridge.models.nemotron_omni.data.collate_fn",
4444
"nemotron_omni_expanded_collate_fn",
45-
required_for_all_examples=True,
45+
always_use_model_collate=True,
4646
),
4747
"PixtralProcessor": _ModelCollateSpec(
4848
"megatron.bridge.models.ministral3.data.collate_fn", "ministral3_collate_fn"
@@ -54,13 +54,18 @@ class _ModelCollateSpec:
5454
),
5555
"Glm4vProcessor": _ModelCollateSpec("megatron.bridge.models.glm_vl.data.collate_fn", "glm4v_collate_fn"),
5656
"KimiK25Processor": _ModelCollateSpec("megatron.bridge.models.kimi_vl.data.collate_fn", "kimi_k25_vl_collate_fn"),
57+
"deepseek-v4": _ModelCollateSpec(
58+
"megatron.bridge.models.deepseek.data.collate_fn",
59+
"deepseek_v4_collate_fn",
60+
always_use_model_collate=True,
61+
),
5762
}
5863

5964

60-
def model_collate_required_for_all_examples(processor_type: str) -> bool:
65+
def always_use_model_collate(processor_type: str) -> bool:
6166
"""Return whether a processor must always use its model-owned collator."""
6267
spec = _MODEL_COLLATE_SPECS.get(processor_type)
63-
return spec is not None and spec.required_for_all_examples
68+
return spec is not None and spec.always_use_model_collate
6469

6570

6671
@lru_cache(maxsize=None)

‎src/megatron/bridge/data/collators/sft.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@
1616

1717
from __future__ import annotations
1818

19-
from collections.abc import Mapping
19+
from collections.abc import Callable, Mapping
2020
from dataclasses import dataclass
2121
from typing import Any, Literal
2222

2323
import torch
2424

2525
from megatron.bridge.data.conversation_processing import (
26+
TokenizedConversation,
2627
build_shifted_labels_and_loss_mask,
2728
ensure_position_ids,
2829
get_processor_tokenizer,
@@ -180,6 +181,7 @@ def text_chat_collate_fn(
180181
ignore_index: int = IGNORE_INDEX,
181182
enable_in_batch_packing: bool = False,
182183
in_batch_packing_pad_to_multiple_of: int = 1,
184+
tokenize_impl: Callable[..., TokenizedConversation] | None = None,
183185
**kwargs: Any,
184186
) -> dict[str, Any]:
185187
"""Collate text-only HF chat examples using the shared assistant-mask path.
@@ -204,6 +206,9 @@ def text_chat_collate_fn(
204206
packed-sequence metadata for GPT-style training steps.
205207
in_batch_packing_pad_to_multiple_of: Optional per-sequence length multiple
206208
used when ``enable_in_batch_packing`` inserts padding for CP/SP constraints.
209+
tokenize_impl: Optional model-owned chat tokenizer. Models without a
210+
Jinja chat template may provide an equivalent renderer while reusing
211+
the shared padding, shifting, and packing implementation.
207212
**kwargs: Additional common collate kwargs accepted for parity with
208213
VLM collate functions and ignored by the text-only path.
209214
@@ -218,9 +223,10 @@ def text_chat_collate_fn(
218223
tokenizer = get_processor_tokenizer(processor)
219224
boundary_config = infer_assistant_mask_boundary_config(processor)
220225
skipped_tokens = extract_skipped_token_ids(processor)
226+
tokenize = tokenize_impl or tokenize_chat_example
221227
tokenized_rows = []
222228
for example in examples:
223-
tokenized = tokenize_chat_example(
229+
tokenized = tokenize(
224230
example,
225231
processor,
226232
max_length=max_length,

0 commit comments

Comments
 (0)