Skip to content

[Feature] Add Nemotron-CC v1 and v2 dataset support - #83

Open
akaashrp wants to merge 1 commit into
mlc-ai:mainfrom
akaashrp:feature/nemotron-cc
Open

[Feature] Add Nemotron-CC v1 and v2 dataset support#83
akaashrp wants to merge 1 commit into
mlc-ai:mainfrom
akaashrp:feature/nemotron-cc

Conversation

@akaashrp

@akaashrp akaashrp commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@akaashrp akaashrp changed the title Add Nemotron-CC v1 and v2 dataset support (feat):add Nemotron-CC v1 and v2 dataset support Aug 6, 2026
@akaashrp akaashrp changed the title (feat):add Nemotron-CC v1 and v2 dataset support [Feature] Add Nemotron-CC v1 and v2 dataset support Aug 6, 2026
@mlc-ai mlc-ai deleted a comment from akaashrp Aug 21, 2026
@haok1402

Copy link
Copy Markdown
Collaborator

CI never ran here. GitHub's Actions incident (Aug 6, 15:05–00:14 UTC) throttled webhooks to ~15%, so the pull_request event for this PR was dropped and can't be replayed. Closing and reopening to trigger Precommit Check.

@haok1402 haok1402 closed this Aug 21, 2026
@haok1402 haok1402 reopened this Aug 21, 2026
@haok1402

Copy link
Copy Markdown
Collaborator

@claude review

Comment thread pithtrain/tasks/pretrain_lm.py Outdated
scheduler.load_state_dict(st)


def maybe_reload_dataset_mixture(cfg: PretrainLMCfg) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compactness: The entire online-reweighting path is a speculative hook — no training entrypoint in the repo sets dataset_mixture_hot_reload_path. This adds ~65 lines here (mtime polling, per-step broadcast, versioning) plus two config knobs (dataset_mixture_hot_reload_path, dataset_mixture_poll_interval_steps) and two context fields (dataset_mixture_last_mtime_ns, dataset_mixture_version), all for a feature nothing exercises.

Simpler: set the mixture once in setup_dataset from cfg.dataset_mixture, and drop this function, its train_step call, the two config fields, and the two context fields. Add the reload path back when a run actually drives it.

Comment thread pithtrain/modules/dataset.py Outdated
Comment on lines +168 to +169
self._weights: Dict[str, float] = {}
self._weight_vec = np.empty(0, dtype=np.float64)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compactness: _weight_vec is written here and in update_weights (line 206) but never read anywhere. Dead field — drop the initializer here and the assignment in update_weights.

Suggested change
self._weights: Dict[str, float] = {}
self._weight_vec = np.empty(0, dtype=np.float64)
self._weights: Dict[str, float] = {}
self._cdf = np.empty(0, dtype=np.float64)

Comment on lines +75 to +84
if __name__ == "__main__":
raw_root = Path("workspace/datasets/nemotron-cc-v1/rawtxt")
download_raw_files(raw_root)

if __name__ == "__main__":
cfg = TokenizeCorpusCfg()
cfg.tokenizer_name = "Qwen/Qwen3-30B-A3B"
cfg.source_path = Path("workspace/datasets/nemotron-cc-v1/rawtxt")
cfg.output_path = Path("workspace/datasets/nemotron-cc-v1/toktxt/qwen3")
launch(cfg)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compactness: Two consecutive if __name__ == "__main__": guards where one does. Merge them (and reuse raw_root):

Suggested change
if __name__ == "__main__":
raw_root = Path("workspace/datasets/nemotron-cc-v1/rawtxt")
download_raw_files(raw_root)
if __name__ == "__main__":
cfg = TokenizeCorpusCfg()
cfg.tokenizer_name = "Qwen/Qwen3-30B-A3B"
cfg.source_path = Path("workspace/datasets/nemotron-cc-v1/rawtxt")
cfg.output_path = Path("workspace/datasets/nemotron-cc-v1/toktxt/qwen3")
launch(cfg)
if __name__ == "__main__":
raw_root = Path("workspace/datasets/nemotron-cc-v1/rawtxt")
download_raw_files(raw_root)
cfg = TokenizeCorpusCfg()
cfg.tokenizer_name = "Qwen/Qwen3-30B-A3B"
cfg.source_path = raw_root
cfg.output_path = Path("workspace/datasets/nemotron-cc-v1/toktxt/qwen3")
launch(cfg)

Comment on lines +65 to +74
if __name__ == "__main__":
raw_root = Path("workspace/datasets/nemotron-cc-v2/rawtxt")
download_raw_files(raw_root)

if __name__ == "__main__":
cfg = TokenizeCorpusCfg()
cfg.tokenizer_name = "Qwen/Qwen3-30B-A3B"
cfg.source_path = Path("workspace/datasets/nemotron-cc-v2/rawtxt")
cfg.output_path = Path("workspace/datasets/nemotron-cc-v2/toktxt/qwen3")
launch(cfg)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compactness: Same doubled if __name__ == "__main__": guard as v1. Merge into one block:

Suggested change
if __name__ == "__main__":
raw_root = Path("workspace/datasets/nemotron-cc-v2/rawtxt")
download_raw_files(raw_root)
if __name__ == "__main__":
cfg = TokenizeCorpusCfg()
cfg.tokenizer_name = "Qwen/Qwen3-30B-A3B"
cfg.source_path = Path("workspace/datasets/nemotron-cc-v2/rawtxt")
cfg.output_path = Path("workspace/datasets/nemotron-cc-v2/toktxt/qwen3")
launch(cfg)
if __name__ == "__main__":
raw_root = Path("workspace/datasets/nemotron-cc-v2/rawtxt")
download_raw_files(raw_root)
cfg = TokenizeCorpusCfg()
cfg.tokenizer_name = "Qwen/Qwen3-30B-A3B"
cfg.source_path = raw_root
cfg.output_path = Path("workspace/datasets/nemotron-cc-v2/toktxt/qwen3")
launch(cfg)

@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Compactness Review

The core of this PR — parquet reading, WeightedMixtureDataset/SourceDataset, and the Nemotron download scripts — is proportionate to "add Nemotron-CC + weighted mixtures." Two things add surface the feature does not need:

  • Online mixture hot-reload (maybe_reload_dataset_mixture, the two dataset_mixture_hot_reload_path/poll_interval config knobs, and the two dataset_mixture_* context fields) is a speculative hook: no training entrypoint sets a reload path. Setting the mixture once at setup_dataset would drop ~70 lines and four fields.
  • Minor: a dead _weight_vec field, and doubled if __name__ == "__main__": guards in both download scripts.

Details in the inline threads.

Comment thread pithtrain/tasks/pretrain_lm.py Outdated
Comment on lines +275 to +276
object_list = [payload]
torch.distributed.broadcast_object_list(object_list, src=0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: broadcast_object_list runs on every poll, unconditionally, even when the mixture is unchanged (payload=None). With the default dataset_mixture_poll_interval_steps=1, that's a pickle-based world collective on the hot train_step path every step whenever hot reload is enabled. On NCCL, broadcast_object_list stages the pickled bytes through a GPU tensor and copies back to CPU to unpickle — a device sync per step that serializes against pipeline compute.

Gate the expensive broadcast behind a cheap check: rank 0 broadcasts a single 1-element int tensor (the mixture version, or a changed flag); only when it advances do all ranks do the object broadcast. That keeps the steady-state per-step cost to one small tensor broadcast instead of a pickle round-trip + sync.

Comment thread pithtrain/modules/training.py Outdated
dataset_mixture_hot_reload_path: Optional[Path] = None
"""Optional JSON file path for online mixture reweighting."""

dataset_mixture_poll_interval_steps: int = 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: Default of 1 means the hot-reload machinery in maybe_reload_dataset_mixture fires every single step (a stat() on rank 0 + a world broadcast_object_list — see the thread there). Mixture reweighting is an operator-driven, coarse-grained action; a per-step poll buys nothing and adds a per-step collective. Default to something coarse (e.g. hundreds of steps) so the steady-state training loop isn't paying for it every step.

@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Consistency Review

Docs and comments track the code changes cleanly. The tokenize_corpus.py module docstring was correctly generalized from "(JSONL, optionally zstd-compressed)" to "raw text files" to match the added .parquet path in read_file, and the new examples/*/README.md prose (Nemotron partition/subset defaults, env-var overrides, training.dataset_sources/dataset_mixture vs. the single-root training.dataset, and the toktxt/<tokenizer> output paths) all match the config fields and script outputs. The duplicated if __name__ == "__main__": block in the two new nemotron scripts follows the existing dclm-script convention, so it is consistent, not stray.

No stale reference was found: grepping AGENTS.md, docs/, and .agents/skills/ for the renamed/changed surfaces (input formats, TrainingCfg.dataset now optional, setup_dataset) turned up nothing describing the old state.

@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Performance Review

This PR is data-loading/tokenization plumbing — it doesn't touch the GPU hot path (DualPipeV overlap, kernels, compiled regions), so no risk to comm overlap or graph stability. Two things to address, both scoped to the new weighted-mixture path (the default single-source ConcatDataset path is unaffected — maybe_reload_dataset_mixture returns immediately for it):

  • maybe_reload_dataset_mixture issues an unconditional broadcast_object_list on every poll, even when the mixture hasn't changed. With the default poll interval of 1, that's a pickle-based world collective + implicit device sync in train_step on every step whenever hot reload is enabled. Gate it behind a cheap version-int broadcast and coarsen the default poll interval. (inline threads)

The new per-sample splitmix64 sampling in WeightedMixtureDataset is pure-Python but small (2 hashes/sample) relative to the memmap reads already in get_global_batch, so not a concern on its own.

Evidence gap: the PR body is empty and no throughput/step-time numbers are provided. Since it adds a per-step call into the training loop, a tokens-per-second comparison of single-source vs. mixture mode (and mixture with hot reload enabled, at the default poll interval) would confirm the steady-state loop doesn't regress.

Comment thread pithtrain/modules/training.py Outdated
Comment on lines +308 to +319
training.dataset_mixture_last_mtime_ns = None
training.dataset_mixture_version = 0
if cfg.dataset_sources:
if not cfg.dataset_mixture:
raise ValueError("dataset_mixture must be provided when dataset_sources is set.")
source_datasets: dict[str, SourceDataset] = {}
for source_name, source_root in sorted(cfg.dataset_sources.items()):
memmap_datasets = []
for file in sorted(source_root.rglob("*.bin")):
memmap_datasets.append(MemmapDataset(file, cfg.sequence_length))
source_datasets[source_name] = SourceDataset(source_name, memmap_datasets)
training.dataset = WeightedMixtureDataset(source_datasets, cfg.seed, cfg.dataset_mixture)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: Hot-reloaded mixture state is not part of the checkpoint, so resume silently trains on the wrong data distribution. AppState/load_checkpoint persist only model/optim/scheduler/rng/step; on resume setup_dataset resets dataset_mixture_version = 0, dataset_mixture_last_mtime_ns = None, and rebuilds the mixture from the initial cfg.dataset_mixture, discarding every hot-reload applied before the checkpoint.

Concrete failure: a curriculum applies weights W2 via the hot-reload file at step 1000, checkpoint at step 2000 with W2 active, crash, resume. If the hot-reload file was since removed/renamed (or dataset_mixture_poll_interval_steps > 1 and the resumed step isn't a poll multiple), mtime_ns is None/non-triggering, so W2 is never re-applied and training continues on the initial W0 mixture. Even with the default poll_interval == 1, a deleted file makes the revert permanent.

To settle it, persist dataset_mixture_version and the current weights in the checkpoint (or a sidecar) and restore them in load_checkpoint before the first maybe_reload_dataset_mixture, so the run continues from the last-applied mixture regardless of the file's current state.

@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Correctness Review

This PR is data-pipeline-only — Nemotron-CC v1/v2 fetch + parquet reading, and a new WeightedMixtureDataset for weighted multi-source sampling. It touches no gradients, parallelism math, numerics, or kernels, so the correctness surface is data sampling and distributed hot-reload consistency, both of which are well covered by the added unit tests. The single-source path stays behavior-identical (dataset is now Optional with an explicit guard).

I traced the sampling and the hot-reload collective and both look correct: _resolve is a pure function of the global index, so CP front/back blocks resolve to the same source/local index; source selection and local index use decorrelated splitmix64 streams; and the broadcast_object_list path is reached identically on all ranks (returns/skips gate only on cfg + training.step), so no rank-divergence hang.

One concrete correctness gap: hot-reloaded mixture weights and version are not checkpointed, so a resume can silently revert to the initial config mixture (details + a fix direction in the inline thread on setup_dataset). Everything else is sound.

@akaashrp
akaashrp force-pushed the feature/nemotron-cc branch from e5bd465 to ca327ca Compare August 30, 2026 17:01
@akaashrp
akaashrp force-pushed the feature/nemotron-cc branch from ca327ca to 7ca6cdc Compare September 9, 2026 06:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants