Skip to content

Testing - #902

Closed
ShriyaRishab wants to merge 7 commits into
shriya/add_PR_templatesfrom
master
Closed

Testing#902
ShriyaRishab wants to merge 7 commits into
shriya/add_PR_templatesfrom
master

Conversation

@ShriyaRishab

Copy link
Copy Markdown
Contributor

Summary

Checklists are appended automatically from PR labels (works for UI, CLI, and fork PRs):

Add the matching label, or pick a template from the GitHub template picker / gh pr create --template <file>.

ShriyaRishab and others added 7 commits August 12, 2026 10:30
- Introduced a default PR template for new benchmarks, outlining necessary steps and checklist items.
- Added a separate PR template for introducing new rounds, including confirmation of round scope and updates to relevant documentation.
- Updated CONTRIBUTING.md to reference the new templates and provide guidance on their usage.

Signed-off-by: ShriyaRishab <spalsamudram@nvidia.com>
* Add recommendation_v4 (HSTU/DLRM-v3 generative-recommenders fork @ d97e51c)

Vendored snapshot of chriscai-amd/generative-recommenders branch chcai/dlrmv4
(HEAD d97e51c) as a sibling of recommendation_v2/torchrec_dlrm. The Python
package generative_recommenders keeps its original name so all imports work
unchanged from the new location.

- recommendation_v4/generative_recommenders/: dlrm_v3, modules, ops, research, tests
- recommendation_v4/configs/: research HSTU gins
- recommendation_v4/scripts/launch_smoke_8gpu.sh: sanitized 8-GPU yambda-5b launcher
  (resolves package root from script path; AMD env defaults; pip_local override)
- recommendation_v4/{setup.py,requirements.txt,main.py,...}: upstream entry points
- .gitmodules: cutlass registered at parent repo level

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Enable Triton HSTU kernels on AMD/ROCm (gfx950 MI350X)

Four fixes unlocking the HSTU_HAMMER_KERNEL=TRITON path on MI350X:

1. triton_hstu_attention.py _should_enable_tma(): add HIP early-out.
   torch.cuda.get_device_capability() on gfx950 returns (9, 5) which would
   pass the major==9 Hopper check and trick the kernel into the TMA path,
   producing kernels that don't compile on ROCm.

2. triton_hstu_attention.py _get_fw_configs(): hoist the USE_TLX/NUM_BUFFERS/
   NUM_MMA_WARPS_PER_GROUP/NUM_MMA_GROUPS defaults loop out of the CUDA-only
   else: branch. The _hstu_attn_fwd signature requires these constexprs
   regardless of backend; missing them on HIP triggered TypeError:
   dynamic_func() missing N required positional arguments at autotune.
   Also gate the H100 TLX configs append on `not torch.version.hip`.

3. triton_jagged_tensors.py concat/split dispatch: route AMD/ROCm through
   *_2D_jagged_multirow instead of the basic _concat_2D_jagged /
   _split_2D_jagged kernels. The basic kernels fail PassManager::run at
   make_ttgir (TritonAMDGPUCanonicalizePointers pass) on ROCm; multirow
   compiles fine. NVIDIA non-Blackwell paths (H100/A100) are unchanged.

4. triton_jagged_tensors.py _Concat2DJaggedFunction.backward: replace the
   raw _split_2D_jagged[grid] call with _triton_split_2D_jagged_internal
   so the backward pass benefits from the same AMD multirow routing as
   the forward.

Verified end-to-end on 8x MI350X: yambda-5b bs=32 seq=4k at 782 global_sps
vs PYTORCH backend 547 sps -- 1.43x throughput, 75% peak VRAM vs 92%.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Fix AttributeError on triton.knobs.nvidia.use_meta_ws

The attribute is absent in some Triton builds (e.g. nvcr.io/nvidia/pytorch:26.01-py3),
causing import-time AttributeError before any training step runs. Use getattr with
a False default so _use_meta_ws() gracefully reports disabled on those builds.

* Make HSTU model arch + dataset history_length gin-tunable

Three small changes so you can sweep model size and per-sample sequence
length from a gin file without editing configs.py.

configs.py:
  - get_hstu_configs is now @gin.configurable. Accepts optional overrides
    for max_seq_len, max_num_candidates, hstu_embedding_table_dim,
    hstu_transducer_embedding_dim, hstu_num_heads, hstu_attn_num_layers,
    hstu_attn_linear_dim, hstu_attn_qk_dim, hstu_input_dropout_ratio,
    hstu_linear_dropout_rate. Per-dataset defaults still apply unless
    explicitly overridden in gin.
  - get_embedding_table_config is now @gin.configurable with an
    embedding_dim override that uniformly sets the dim for all tables
    of the chosen dataset.
  - Drop the YAMBDA_EMBEDDING_DIM constant (was a duplicate of
    HSTU_EMBEDDING_DIM=512). Yambda branch now uses HSTU_EMBEDDING_DIM
    directly. Add a comment noting the model+table dim must stay aligned
    when overriding either via gin.

utils.py:
  - get_dataset accepts an optional history_length kwarg that wins over
    the yambda dataset's hardcoded default of 4096. Caches are still
    keyed on disk under hstu_cache_L<N>/ so switching L between previously
    built values is free.

train/gin/yambda_5b.gin:
  - Pin history_length=2048 and max_seq_len=2048 for the seq-2k smoke
    config. Both lines have inline comments explaining the +9 overhead
    (uid + 7 cross + 1 candidate) so total per-sample seq is ~2046,
    within the 2048 budget.

Verified: default codepath unchanged, gin overrides apply consistently
to both get_hstu_configs (model) and get_embedding_table_config (tables).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Make EmbeddingShardingPlanner hbm_cap_gb gin-tunable, set ddr_cap=0

make_optimizer_and_shard now accepts hbm_cap_gb (default 260, the MI350X
value) via @gin.configurable. The yambda gin pins the same default so
sweeps just change the number in the gin file instead of editing utils.py.

ddr_cap dropped from 32 GiB to 0: with all 11 yambda 5b embedding tables
fitting on 8x MI350X HBM, allowing host DRAM offload only invites the
planner to pick slower per-lookup-PCIe-traffic plans.

Verified gin binding flows through to the Topology: a probe with
hbm_cap_gb=100 produced Topology(hbm_cap=107374182400) and the planner
correctly raised insufficient-storage error at that tightness.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Add yambda 50m/500m/5b preprocessor + DLRM_DATA_PATH env override

preprocess_public_data.py:
  - Add DLRMYambdaProcessor: downloads Yambda multi_event + catalog
    metadata from the yandex/yambda HuggingFace repo, then runs a
    temporal split (300 train days / 30 min gap / 1 test day),
    builds per-user sessions (1800s inactivity threshold), and
    writes the layout DLRMv3YambdaDataset expects:
      <data-path>/raw/<size>/multi_event.parquet
      <data-path>/shared_metadata/{artist,album,embeddings}.parquet
      <data-path>/processed_<size>/{train_sessions,test_events,
                                    session_index}.parquet
      <data-path>/processed_<size>/item_popularity.npy
      <data-path>/processed_<size>/split_meta.json
  - 5b variant uses chunked polars load (10M rows/chunk) to keep
    peak RAM under control (single-shot read of the 50 GB parquet
    OOMs ~150 GB systems).
  - SUPPORTED_DATASETS adds yambda-50m, yambda-500m, yambda-5b.
  - main() takes --data-path for custom output root.
  - Verified end-to-end: 50m run completes in ~2 min, 5b in ~53 min
    (download dominates), output is byte-compatible with the dataset
    cache builder; TRITON training reaches steady state on the
    fresh data at 2050 sps.

utils.py:
  - Add env_path(key, default) @gin.configurable helper. Used as a
    gin macro so any string-valued binding can be overridden by an
    env var without editing the gin file.

train/gin/yambda_5b.gin:
  - Declare DATA_PATH = @env_path() macro with key="DLRM_DATA_PATH"
    and default="/apps/chcai/dlrm_data". Both new_path_prefix
    bindings (make_train_test_dataloaders and get_dataset) now
    consume %DATA_PATH. Setting DLRM_DATA_PATH=/some/path at run
    time redirects the dataset without a gin edit.

datasets/yambda.py:
  - Strip stale references to upstream-internal preprocessing in
    docstrings/comments; point at preprocess_public_data.py instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Fix NCCL init: set CUDA device before init_process_group

Every rank's first CUDA context was landing on GPU 0 (the default
device), so NCCL bound its communicators there before set_device
switched to the correct GPU. This leaked allocations on GPU 0 across
all 8 ranks and caused spurious OOMs during embedding-table init at
high HBM caps. Moving set_device above init_process_group and passing
device_id ensures each rank's NCCL state is created on its own GPU.

* Profiler: write traces to local disk under <repo>/results/<run_name>/

dlrm_v3/utils.py:
  - Replace the hardcoded manifold:// URL in _on_trace_ready_fn with a
    local trace_dir (default /tmp/dlrm_v3_traces). Filename now follows
    trace_step{step}_rank{rank}.json so per-rank captures don't collide.
  - Add _multi_window_schedule helper: a torch.profiler schedule that
    fires around each step in trace_steps=[...] (warmup before, active
    after, RECORD_AND_SAVE at the last active step). Lets one run
    capture multiple windows (e.g. early-step + steady-state) without
    re-running.
  - Make Profiler @gin.configurable. New knobs: trace_dir, trace_steps,
    wait, warmup, repeat, record_shapes, profile_memory, with_stack,
    with_flops, with_modules. Defaults preserve the prior single-window
    behavior (wait=10, warmup=20, active=50, repeat=1) so existing
    callers are unaffected.
  - Add run_results_dir(run_name) gin macro: resolves to
    <recommendation_v4>/results/<run_name>/. Used as the canonical
    output prefix for traces (and any future per-run artifacts).
    recommendation_v4/ is bind-mounted into the training container, so
    files written through this helper persist on the host.

train/gin/yambda_5b.gin:
  - Wire RUN_NAME env override -> run_results_dir(run_name=%RUN_NAME)
    -> Profiler.trace_dir. Sets trace_steps=[52], warmup=5, active=5
    (capture the 5-step window 52-56 on every rank).
  - Toggle train_eval_loop.output_trace = True so the profiler actually
    instantiates.

.gitignore:
  - Add results/ alongside the existing tmp/exps/ckpts/ runtime
    directories so per-run trace dumps don't show up in git status.

Verified: 8x MI350X TRITON yambda-5b run at bs=32 seq=2k drops
8 well-formed trace_step62_rank{0..7}.json files (~37 MB each) into
recommendation_v4/results/default/; visible on the host immediately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Tracing fixes: gin scoping, drop active=10 override, intuitive filenames, trim_warmup

dlrm_v3/utils.py
  * Add run_results_dir(run_name) gin macro (resolves to
    <recommendation_v4>/results/<run_name>/) so trace artifacts persist on
    the host via the bind-mount.
  * Add _trim_warmup_from_trace post-processor: dedupes ProfilerStep spans
    by name first, then keeps only the last N unique steps' worth of
    events. Drops WARMUP-phase events that torch.profiler otherwise
    includes in the chrome trace.
  * Add trim_warmup kwarg (default True) on Profiler; auto-invokes the
    trimmer with N=active so the exported file matches the user-requested
    active window.
  * Filename now uses trace_steps[i] (the user-requested step) as the
    {step} label when multi-window mode is in use, instead of
    torch.profiler's internal step_num (which is off by ~warmup+active
    from the schedule trigger and confused everyone).

train/utils.py
  * Drop hardcoded `active=10` from the four `Profiler(rank, active=10)`
    call sites in train_loop / train_eval_loop. Positional args block
    gin overrides; once removed, Profiler.active in gin (default 50) and
    user gin bindings actually take effect.

train/gin/yambda_5b.gin
  * Fix env_path scoping collision: both DATA_PATH and RUN_NAME used the
    unscoped @env_path() configurable, which made the second binding's
    `env_path.key = "RUN_NAME"` overwrite the first's
    `env_path.key = "DLRM_DATA_PATH"`. Both names then resolved via the
    same env var (whichever was last), pointing DATA_PATH at trace_run2/
    and breaking dataset loads.
    Fixed by giving each call site its own scope: @data/env_path() and
    @run/env_path(), each with independent .key/.default bindings.
  * Set Profiler.trace_steps=[52], warmup=1, active=5; let trim_warmup
    default to True so the exported trace contains exactly 5 active
    ProfilerStep events.

Verified end-to-end:
  - Run with RUN_NAME=trace_run2 writes results/trace_run2/trace_step52_
    rank{0..7}.json (~19 MB each), step labels match trace_steps gin.
  - Triton cache persisted across runs: cold start ~6 min -> warm start
    ~2 min for autotune-to-first-step.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* gin: history_length 2048 → 2039 + expanded per-pool comment

2048 was chosen for "round number near max_seq_len" but it slightly
overflows the per-sample budget: 3 * (2048//3) + 9 = 2055 > 2048, so
the dataset truncates ~7 UIH events to fit. 2039 makes the math exact
(3 * 679 + 9 = 2046 ≤ 2048) so no truncation.

Comment block expanded to document:
  - The 3-pool gather semantic (L//3 events per pool, interleaved
    chronologically).
  - The like-pool under-fill observation: like events are only 1.9%
    of yambda corpus and max user lifetime is ~28k events, so the
    like pool fills to ~105 events per anchor on average (not 679).
    TRITON's jagged attention skips the unfilled slots, so under-fill
    costs sequence budget but not GPU compute.

No code change. Cache for L=2039 already built and reused.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* README: rewrite for yambda-5b fork — upstream link, data prep, per-pool gather

Documents the fork's scope (yambda-5b on HSTU dlrm_v3 path), per-pool gather
strategy with effective fill table, and dataset statistics. Sections indexed
1–5 for navigation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* gin: make hbm_cap_gb overridable via \$HBM_CAP_GB

Adds env_int gin macro (companion to env_path) and wires
make_optimizer_and_shard.hbm_cap_gb through it so the per-rank HBM
ceiling can be tuned without editing the gin file.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: add B200 training recipe for yambda-5b

Document the container image, dependency versions (native NGC torch 2.10,
triton 3.6, source-built fbgemm_gpu, torchrec 1.4.0, polars-u64-idx), gin
training configuration, and env vars needed to reproduce the 8x B200 run.

* bf16 + triton autotune pinning with gin-driven full-tune override

Adds three knobs, all driven from the gin file:
- make_model.bf16_training: enable bf16 autocast for the DlrmHSTU model.
- env_int macro: lets numeric gin values come from env vars (used by the
  existing hbm_cap_gb binding).
- apply_env_bootstrap.TRITON_FULL_AUTOTUNE: when False (default), three
  layer-norm/jagged triton kernels are pinned to a single Config so cold
  starts land at the same steady-state deterministically. When True, the
  full autotune search runs again — use this when changing shape, GPU,
  or triton/torch version, then re-pin from the discovered winners.

train_ranker._main_func now parses gin in two phases (skip_unknown=True
early, full pass after the heavy imports) so the bootstrap env var is set
BEFORE the triton kernel modules evaluate their @triton.autotune
decorators at module load time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: add MI350X training recipe section

Mirrors the B200 layout with MI350X (gfx950, ROCm 7.2.1) specifics:
container image (rocm/primus:v26.3), fbgemm_gpu rebuild requirement (HEAD
nightly_rocm-2026.6.1 for ~30% step-time win over the shipped 2026.5.14),
the gin-driven TRITON_FULL_AUTOTUNE knob, and the measured perf ladder
from fp32/PYTORCH baseline (~28 d/epoch) down to the pinned bf16/TRITON
fast equilibrium (~7.6 d/epoch).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* scripts: add stitch_traces.py

Merges per-rank chrome traces (results/<run>/trace_step{N}_rank{R}.json)
into a single Perfetto-loadable file, remapping pid/flow ids so
cross-rank events land on distinct tracks instead of collapsing onto one.
Used to produce the bf16 + pinned-autotune step-52 trace
(results/verify_rename/trace_step52.json.gz).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: update B200 recipe deps to NGC 26.04 (torch 2.12 / CUDA 13.2)

Refresh the B200 dependency versions to the latest validated stack
(torch 2.12.0a0 / CUDA 13.2, fbgemm_gpu built for sm_100+CUDA 13.2,
CUPTI 13.2), note 26.01 as an equivalent alternative, and record the
TRITON_FULL_AUTOTUNE=True setting for B200.

* docs: refresh B200 recipe deps (fbgemm HEAD, torchrec 1.7 nightly, driver)

Point fbgemm at the latest validated source commit (10b77573, 2026-06-01),
record the tested torchrec 1.7.0.dev nightly (1.4.0 stable fallback),
clarify the fbgemm wheel version string is the build date, and correct the
host/forward-compat driver CUDA versions (13.0 host / 595.58.03 compat).

* MI350X: re-pin 2 triton configs for the torch 2.12 + torchrec 1.7 stack

After upgrading to torch 2.12 / torchrec 1.7 (B200-aligned), the pinned
configs from the torch 2.10 stack stopped landing on the fast equilibrium
because the torchrec 1.7 code path invokes these kernels at different
shape keys. Re-captured winners via a fresh autotune run and updated the
pin sites:

- _weighted_layer_norm_bwd_dx: BLOCK_N 8 -> 1 (num_warps 1 unchanged)
- split_2D_jagged_multirow:    BLOCK_N 1 / num_warps 2 -> BLOCK_N 8 / num_warps 1
- _layer_norm_bwd_dwdb:        BLOCK_N 128, num_warps 8 (unchanged - same winner on both stacks)

Verified: 3 consecutive checkpoints (steps 151/201/251) at 52.75-53.36 ms
deterministic on the new stack. Same equilibrium band as the torch 2.10
stack (51.5-53.0 ms).

Also adds a Stack B section to docs/training_recipe.md (MI350X) documenting
the torch 2.12 swap recipe (torch + torchvision + torchaudio + fbgemm
rebuild + torchrec git tag) so the MI350X recipe is dependency-aligned with
the B200 path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: update MI350X Stack B to fbgemm @ B200 commit + caveat

Bumps the Stack B (torch 2.12 / torchrec 1.7) section to:
- fbgemm commit 10b77573 (same SHA as the B200 path) instead of 1509423
  (one cosmetic commit behind). Wheel rename 2026.6.1 -> 2026.6.2.
- Note that Stack A and Stack B use different pinned triton configs
  (already merged) and explain why (torchrec 1.7 invokes the kernels at
  different shape keys).
- Caveat: HSTU_HAMMER_KERNEL=PYTORCH fallback regresses to ~169 ms on
  Stack B (vs 107 ms on Stack A). TRITON is unaffected and remains the
  default; this only matters for PYTORCH-backend debugging.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: drop Stack A; MI350X recipe is now single-stack (B200-aligned)

Collapses the two-stack MI350X section into one canonical dependency
table: torch 2.12 / torchrec 1.7 / fbgemm @ 10b77573 — the same SHAs as
the B200 path. The image-native torch 2.10 / torchrec 1.4 / fbgemm
2026.5.14 path still works for development but the recipe doc now
documents the validated production stack only.

PYTORCH-backend caveat preserved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: drop PYTORCH-fallback caveat from MI350X recipe

Not relevant — TRITON is the documented default backend.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* MI350X: fit-entity embedding sizes, bs=1024 default, batch-agnostic recipe

Embedding sizes match the true entity counts in yambda-5b:
  item_id   9_390_000 -> 9_390_624
  artist_id 1_290_000 -> 1_293_395
  album_id  3_370_000 -> 3_367_692
  uid       1_000_000 -> 1_000_001
This eliminates the recurring "EmbeddingBoundsCheck ... Setting idx to
zero" warnings at training time.

Gin default raised to batch_size=1024 / eval_batch_size=1024. Measured
steady-state on the torch 2.12 + torchrec 1.7 + fbgemm HEAD stack with
TRITON HSTU + pinned triton configs: ~635 ms/step, ~12.9K sps, ~2.92
days/epoch vs ~7.6 days at bs=32. bs=2048 is feasible but only +3%
throughput at much higher autotune cost, so bs=1024 is the sweet spot.

Triton autotune pin for _weighted_layer_norm_bwd_dx now ships TWO
configs in the pinned list — BLOCK_N=1 (bs=32 winner) and BLOCK_N=8
(bs=1024 winner). Triton's autotune key=[BLOCK_D] dispatches the right
one per shape in <5 sec on cold start (vs ~30 sec from the full pool).
The other two pinned kernels (_layer_norm_bwd_dwdb, split_2D_jagged_multirow)
have identical winners at bs=32 and bs=1024 so they stay single-config.

Training-recipe doc drops the batch_size rows from both MI350X and B200
config tables — the recipe is intentionally batch-size-agnostic now that
the pin set covers a range.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* MI350X: separated-RNG LN-dropout + attention autotune pin + clock guard

Enable the multi-row, separated-RNG _ln_mul_dropout path on AMD MI350 (gfx950),
previously Blackwell-only. Batches rows per program and reuses a precomputed
dropout mask in the backward instead of one-program-per-row fused RNG; +5.6%
end-to-end (-> 14,222 global sps) at bs=1024 on yambda-5b.

- ops/utils.py: add is_amd_mi350() + use_separated_rng_ln_mul_dropout() gate.
- ops/triton/triton_hstu_linear.py: dispatch the fwd LN-dropout to the
  separated-RNG path via the new gate.
- ops/triton/triton_hstu_attention.py: pin fast nonkdim:16 fwd/persistent/bwd
  configs via pinned_or_full (TRITON_FULL_AUTOTUNE=1 still bypasses). Multi-config
  lists with an inline "add a new batch size" guide.
- scripts/launch_smoke_8gpu.sh: GPU clock sanity guard - log perf level + sclk,
  auto-restore 'auto' if a perf_determinism/manual/low lock is found (a half-clock
  lock uniformly slowed every Triton kernel ~1.9x and masked perf changes).
- docs/perf_opt.md: document the LN-dropout fix and the clock-lock caveat.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: TorchRec 3-stage sparse-dist pipeline + gin-selectable HSTU kernel

Add an opt-in TrainPipelineSparseDist path that overlaps the embedding
input-distribution all-to-all with dense fwd/bwd. To make the embedding
collection pipelineable, the merged sparse KJT is now pre-built in the
dataloader (Samples.merged_sparse_features) and the model consumes it via a
_pipeline_mode forward that takes the batch as a single arg, so TorchRec's
tracer resolves the lookup input as a plain getattr off the batch.

- dataset.py: Samples.merged_sparse_features + merge_uih_candidate_kjts, built
  in collate_fn; wired into to()/record_stream()/pin_memory().
- dlrm_hstu.py: _pipeline_mode flag; forward unpacks the batch and preprocess
  accepts the prebuilt merged KJT (falls back to building it when absent).
- utils.py: _PipelineModelWrapper, build_train_pipeline, train_eval_loop
  use_pipeline branch + eval batch-arg; seed all RNGs in setup() for
  reproducible weight init.
- gin/launch: make_model.hammer_kernel selects TRITON vs PYTORCH (env override
  still honored); launch script defers to the gin default. use_pipeline
  defaults to False.

Validated on MI350/ROCm 8-GPU: embedding collection is pipelined (input-dist
a2a moves to hidden); model quality and throughput match the sequential path
(seeded A/B). The exposed embedding-output a2a still dominates the step, so
throughput is unchanged — pipelining is quality- and perf-neutral here.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: streaming (temporal-order) training for yambda-5b

Add a forward-in-time streaming path: slice the timeline into fixed-duration
windows (default 1 day), train window T then eval window T+1, enforcing no
future leakage (across-window + causal-history guarantees). Make it the
default mode in launch_smoke_8gpu.sh.

Window-reset overhead is hidden via a persistent worker pool + double
buffering (next window's index mask and first-batch prefetch overlap compute
on a background thread) and eval-window prefetch one window ahead, dropping
train/eval first-batch waits to ~1-3ms with no steady-state regression.
Window selection uses a lazily-built, mmap'd anchor-timestamp cache so the
default non-streaming path is unaffected.

Also harden trace export (best-effort: IO/permission failures warn instead of
crashing training) now that streaming enables output_trace by default, and
document the path + knobs in the README.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: disable checkpointing by default; fix recipe torch note

save_dmp_checkpoint.path now resolves from $CKPT_PATH and defaults to empty,
so checkpoints (a full DMP is ~100s of GB, and the streaming loop always saves
the final window) are off unless explicitly enabled. Also drop the stale
training-recipe sentence claiming native torch is kept — it contradicts the
dependency table, which replaces torch and keeps only the image's triton.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: ROCm-only Perfetto trace render fixes at export time

Add in-process trace postprocessing in the profiler on_trace_ready
callback to fix two ROCm/roctracer rendering artifacts that make MI350X
traces look wrong in Perfetto (the timing is correct, only the layout):

- _normalize_profilerstep_layout: collapse the fragmented GPU-side
  ProfilerStep#N spans (roctracer splits a step across the HIP null +
  compute streams) into one full-width span per step on the busiest
  compute stream, matching the CUDA look.
- _deoverlap_gpu_slices: pull back sub-us kernel end timestamps so
  back-to-back kernels don't touch/overlap; Perfetto otherwise nests the
  later (long) kernel inside the tiny epilogue and clips it to zero width,
  hiding kernels like _hstu_attn_bwd. Leaves a ~1ns gap (exact end==start
  is just as fatal as an overlap) and leaves real nesting untouched.

Both passes are gated behind _is_rocm() (torch.version.hip) so they are
complete no-ops on CUDA/B200, which don't have these artifacts. All
best-effort: failures degrade to a warning and never crash training.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: ROCm annotation de-overlap so phase spans render full width

Add _deoverlap_gpu_annotations to the trace-export postprocessing, the
annotation-boundary analog of the kernel de-overlap. Kineto projects the
forward/backward phase annotations (## user_forward ##, ## item_forward ##,
## stu_* ##, ...) onto the GPU stream as a chain of end-to-end siblings.
The absolute step timestamps are ~5.4e12 us, where a float64's quantum is
~1 ns, so a sibling boundary that should be coincident lands a few ns off;
when the earlier sibling ends at/after the next one's start, Perfetto nests
and clips the next span to a sliver -- e.g. the 100+ ms ## user_forward ##
vanishes on some ranks/steps purely by rounding luck.

Since annotations form a real nesting hierarchy (user_forward contains the
stu_* spans and their kernels), this walks the per-track slice stack and
only snaps a slice back when the next slice extends beyond it (siblings,
not parent/child), guarding against trimming into a span's own descendants.
It also snaps kernel tails that straddle an annotation boundary. Gated by
_is_rocm() (no-op on B200/CUDA) and best-effort like the other passes.

Verified end-to-end on an 8-rank MI350X run: ## user_forward ## renders
40/40 (was 9/40), total clipped annotations 1352 -> ~5.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: streaming checkpoint resume + step/time checkpoint cadences

Make streaming-train-eval crash-resumable and add general checkpoint
cadence controls:

- Atomic checkpoint saves (.tmp dir + rename), keep_last_n pruning, and
  swap-aside .old overwrite so a save can safely replace an existing
  train_ts dir; stale .tmp/.old swept on the next save.
- Per-rank RNG snapshot/restore for bit-equal dropout replay on resume;
  auto-latest-subdir resolution + (train_ts, batch_idx_in_window) resume
  hint so a run re-enters a partial window and skips already-trained
  batches exact-once.
- Three independent in-window checkpoint cadences via a pure, testable
  decision helper: per-window batch count, monotonic global step
  (e.g. every 1000 steps), and wall-clock interval (e.g. hourly,
  rank-0-decided + broadcast to keep the save barrier in lockstep).
- gin/env bindings for all cadences + a test-only die_at_step hook.

Tests: checkpoint_cadence_test.py (cadence precedence/triggers) and an
end-to-end baseline/interrupt/resume harness (streaming_resume_test.{sh,py})
that gates on functional invariants (RNG restored, correct resumed step,
atomic save, keep_last_n) plus a loose trajectory-closeness bound.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: move streaming resume test harness into train/tests

It's a test driver, not a general script — colocate the shell harness with
its Python comparator under train/tests/ and fix the stale path references.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: sparse full-holdout eval cadence + eval-pool fork-race fix

Add eval_every_n_windows (env EVAL_EVERY_N_WINDOWS, default 1 = no change) so
the heavy full next-day eval window can run every Nth train window (and always
the final one) instead of every window, amortizing its cost on the long run.

Fix a deadlock this exposed in the double-buffer path: the persistent eval
worker pool's first iter() (its only fork) must happen on the main thread
BEFORE the prefetcher's background prep thread starts. Deferring that first
fork into the loop (as the sparse cadence naively did) forks while the bg
thread holds an allocator/GIL-released lock and hangs the run. Always pre-fork
the eval pool before the loop; in-loop re-arms only reset the persistent
workers (no fork) and target the next window that will actually eval.

Also normalize NUM_TRAIN_BATCHES/NUM_EVAL_BATCHES <=0 to None (full window /
full-holdout eval) and bind NUM_EVAL_BATCHES in gin so eval can be capped for
fast validation without affecting the full run.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: self-healing streaming-e2e supervisor + NE/AUC trajectory builder

run_streaming_e2e.sh: head-node supervisor that keeps a multi-day yambda-5b
streaming train+eval alive across (1) trainer crash/OOM, (2) silent SIGKILL,
and (3) node loss. Relaunches from the latest checkpoint each time (exact-once
resume handles continuity). Node failover salloc's a fresh exclusive node on
the partition, provisions the container on it, and resumes from shared NFS;
allocations it creates are released on success (never the user's own --jobid).
Includes disk guard + stale .tmp sweep, keep_last_n retention, an exit-sentinel
+ stall watchdog for crash detection, and a node-health watchdog. Heavily
documented inline.

build_ne_auc_trajectory.py: parse train+eval NE/AUC (+perf) from a run log and
emit combined CSV/JSON plus an NE/AUC-vs-step trajectory plot.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: durable streaming-run metrics (append log + TensorBoard on NFS)

Make the full-run NE/AUC record survive relaunches and node failover:
- launch_smoke_8gpu.sh now appends to $LOG (tee -a) instead of truncating, so
  a supervised run that relaunches many times into the same log keeps its full
  metrics history. The supervisor initializes the log once at run start.
- run_streaming_e2e.sh: truncate $LOG once at start, create the per-run TB dir,
  and export TENSORBOARD_LOG_PATH=/apps/chcai/tb/$RUN_NAME/ into the launch env.
- yambda_5b.gin: MetricsLogger.tensorboard_log_path now reads $TENSORBOARD_LOG_PATH
  (via the existing env_path helper) defaulting to /apps/chcai/tb/yambda_5b/ on
  shared NFS, instead of container-local /tmp (which is wiped on failover).

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: raise streaming-run disk guard for keep_last_n=1 saves

A checkpoint save writes a fresh ~560 GB .tmp before the old copy is
pruned, so peak transient usage is (keep_last_n + 1) copies (~1120 GB at
keep_last_n=1). Bump MIN_FREE_GIB 800 -> 1200 so a (re)launch never wedges
mid-save on a near-full shared NFS volume.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: anchor eval points to train global step in NE/AUC trajectory

Eval used a per-window-resetting internal step counter, so eval markers
bunched at the left of the x-axis instead of overlaying the train curve.
Parse the log sequentially, collapse each eval window to its final
full-holdout metrics, and anchor it to the train global step it ran at
(tagging eval_ts via the [boundary] marker, which can interleave around
the eval's metric lines). Eval points now overlay train on a shared axis.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: supervisor tolerates control-plane outages + attach mode

A SLURM controller outage made the supervisor's squeue/sinfo health check
report "node lost", trigger failover, and then FATAL-exit when salloc
couldn't reach the controller either - abandoning a run whose trainer was
in fact still alive (slurmctld outages don't kill RUNNING jobs).

Harden it:
- controller_up()/wait_for_controller(): treat an unreachable controller as
  transient; wait for recovery (up to --ctrl-wait-max) instead of failing over.
- Direct-SSH fallback (dexec via cached LAST_NODE) so trainer_alive() and the
  mid-run watchdog verify liveness even while the controller is down; only fail
  over if the trainer is genuinely gone, not on a control-plane blip.
- timeout-guard all srun/squeue/sinfo calls so a hung control plane / NFS can't
  wedge the supervisor.
- --attach mode: adopt an already-running trainer (one that outlived a killed
  supervisor) without truncating its log, sweeping its in-flight .tmp, killing
  it, or relaunching - just resume monitoring in place.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: TFLOPS/MFU/HFU reporting + MAX_SEQ_LEN/HISTORY_LENGTH gin knobs

Adds per-step TFLOPS, MFU and HFU to the training perf line + TensorBoard
scalars, plus env-driven gin knobs for sequence-length sweeps. The two
changes ship together because TFLOPS reporting is what makes the 4k vs
2k comparison interpretable (a sps drop alone doesn't say whether the
GPU is doing more work per sample or less work overall).

TFLOPS reporting

  HSTU is structurally different from a standard transformer: the UVQK
  projection fuses Q/K/V/U into one matmul, and SiLU(U)*y elementwise
  gating replaces the FFN block. So a TorchTitan-style "count matmuls
  with factor 6, attention with factor 12" template gives the wrong
  per-sample FLOPs unless it's coded against HSTU's specific shapes.

  DlrmHSTU.get_num_flops_per_sample() implements the HSTU-specific
  dense formula (UVQK projection + Q.K^T + att.V + output projection,
  per layer, times n_layers). Multitask head adds a trivial constant.
  Embedding lookups excluded because they're memory-bound and would
  otherwise pollute MFU. This is the dense yardstick: what the FLOPs
  would be if every sample's UIH filled max_seq_len. It's the standard
  MFU denominator (matches Primus-DLRM's OneTrans accounting style).

  Yambda's per-user history is jagged, so the actual GPU work is
  significantly less than the dense estimate. main_forward stashes
  _last_jagged_flops_per_sample after computing each batch's mean(s)
  and mean(s^2), and MetricsLogger reads + .item()s it once per
  metric_log_frequency (one D->H sync per logging interval, not per
  step). When present, the perf line splits into:

    tflops_algo/gpu  mfu   - dense yardstick, MFU denominator
    tflops_real/gpu  hfu   - actual jagged work, hardware utilization
    fill                   - real / algo, padding-skipped fraction

  When the jagged stash is absent (other model types, or before the
  first main_forward), only tflops_algo/mfu print. When the model
  doesn't expose get_num_flops_per_sample at all, the perf line is
  byte-for-byte unchanged (backward compatible).

  get_gpu_peak_flops("bf16"/"fp32") consults a per-GPU peak table
  (MI355X/MI350X=2300 TF, MI300X/MI325X=1300, B200=2250, H100=990,
  A100=312 for bf16) and warns + defaults to MI350X for unknown
  device names. train_ranker pulls "bf16" when bf16_training=True
  else "fp32"; the dtype string drives only the denominator, not
  anything else.

  TensorBoard scalars added alongside the existing perf/* group:
  perf/train_tflops_algo_gpu, perf/train_mfu_pct,
  perf/train_tflops_real_gpu, perf/train_hfu_pct,
  perf/train_fill_pct.

  Validated on 8x MI350X yambda-5b at the 2k baseline:
    241.6 GFLOP/sample (dense), GPU peak 2300 TFLOPS
    steady-state ~13500 sps -> mfu 17.7-17.9%, hfu 9.7-10.2%,
    fill 55-57% (yambda users average ~1170 events vs 2046 max).
  HSTU's MFU is higher than the OneTrans baseline on the same GPU
  (Primus-DLRM hit 5.7%) because HSTU does less compute per token,
  so the FLOPs it does execute run at higher hardware utilization.

MAX_SEQ_LEN / HISTORY_LENGTH env knobs

  Adds two env-driven gin macros so sequence-length sweeps don't
  require editing yambda_5b.gin (which a running e2e job has parsed
  -- editing the file would change behavior on a supervisor restart):

    get_hstu_configs.max_seq_len = @msl/env_int()   default 2048
    get_dataset.history_length   = @hl/env_int()    default 2039

  Defaults are the current production values, so unset env is a no-op.
  Used the 4k validation run with MAX_SEQ_LEN=4096 HISTORY_LENGTH=4096
  (reuses hstu_cache_L4096/ on disk; ~8 events of trailing UIH
  truncation per sample, negligible).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* dlrmv4: configurable lifetime-AUC backend + fixed-holdout streaming eval

Add a dual-set streaming eval (fresh per-pass "window_*" + cumulative
"lifetime_*") for NE/Accuracy/GAUC/AUC, with a gin-selectable lifetime-AUC
backend for both train and eval: "binned" (BinnedCumulativeAUC, exact
cumulative AUC from an O(bins) histogram, default) or "capped"
(LifetimeAUCMetricComputation, trailing per-rank buffer). Backend, bins, and
window are MetricsLogger gin bindings (env-overridable).

Persist per-rank cumulative metric state in metricbuf_rank{rank}.pt for both
backends across train/eval/eval_cum; keep per-rank state out of the shared
rank-0 blob (strip capped buffers, zero binned histograms) so a resume never
inherits rank-0's counts. Eval set is a stable user-hash holdout over a fixed
window range, validated against a checkpoint split contract on resume.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: default yambda-5b to the 4k-no-truncation seq shape

Set HISTORY_LENGTH=4086 / MAX_SEQ_LEN=4096 as the gin defaults (3*1362+9=4095
≤ 4096, the no-overfill 4k analog of the prior 2039/2048 shape). Override via
$HISTORY_LENGTH/$MAX_SEQ_LEN; use 2039/2048 to reuse the 2k single-task cache.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: reservation-aware sbatch failover in streaming supervisor

Replace the salloc-based node failover with an sbatch hold job (--wrap "sleep
infinity", bounded by --time), since interactive salloc on meta64 is capped at
240 min and can't hold a multi-day run. Add --reservation so a replacement node
is re-acquired from the same SLURM reservation, plus --acquire-wait-max to
tolerate brief queueing. Provisioning still runs via srun --overlap afterward.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: two-tier reservation-then-open-pool failover in streaming supervisor

acquire_node now prefers the configured --reservation (tier 1, short
RESV_WAIT_MAX wait since a free reservation node starts ~immediately),
then falls back to the open partition pool (tier 2, ACQUIRE_WAIT_MAX).
The pending reservation hold job is scancel'd before the fallback resubmit
so we never end up holding two nodes. Factors the sbatch submit and
RUNNING-wait into _submit_hold_job/_wait_running helpers; adds the
--resv-wait-max knob.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: cap failover at <=1 reservation node + fix trainer-alive self-match

trainer_alive: `pgrep -f generative_recommenders` always matched the probe
shell's own cmdline, so it could never report the trainer dead -- defeating
the stall watchdog and making ATTACH mode falsely "adopt" a nonexistent
trainer. Use the `set -f; pgrep -f [g]enerative_recommenders` self-match guard.

Reservation cap: every node we acquire is an sbatch --job-name=e2e_failover
hold, so reap_failover_holds() reaps strays by name at startup (catching holds
leaked by a prior supervisor that died mid-failover) and before every acquire
(no stacking). wait_for_original_recover() waits --orig-recover-wait (def 600s)
for SLURM to requeue the lost ORIGINAL job and reuses it instead of grabbing a
SECOND reservation node. Together these keep us at <=1 reservation node.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: anchor sparse-eval cadence to absolute window ts (resume-invariant)

_should_eval keyed the every-N-windows cadence off the per-call loop index
`i`, so a mid-run resume (which rebases start_ts and restarts train_ts_list at
the resume window) re-anchored the eval grid -- e.g. evals shifted from
150,160,170,... to 165,175,185,... after resuming at window 165. Capture the
original start_ts as eval_anchor_ts BEFORE the resume block mutates start_ts,
and gate eval on (train_ts_list[i] - eval_anchor_ts) % K == 0, so the eval grid
is identical on cold start and every resume. Final-window eval preserved; the
eval-pool fork is unconditional so dropping the i==0 eval on resume is safe.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: multi-node (N>=1) training over RoCE RDMA via consolidated launch_slurm.sh

Consolidate the SLURM orchestration, container/RDMA provisioning, and
in-container trainer launch into a single self-dispatching scripts/launch_slurm.sh
(phases: orchestrate -> provision -> worker) supporting N>=1 nodes; N=1 keeps the
legacy single-node path byte-for-byte.

Multi-node runs real RDMA over the 8 Broadcom bnxt_re RoCE HCAs. The key fix is
an LD_PRELOAD/LD_LIBRARY_PATH overlay of the host's matched rdma-core (v61/v59):
the container's stock v34 provider faults RCCL's deep create_qp (256 WRs) against
the host kernel uapi -> "ibv_create_qp ... Bad address". The unversioned
libibverbs.so symlink in the overlay is required so torch maps only the host lib.
TCP bootstrap is pinned to the routable fenic0; RDMA data goes over bnxt_re
(GID idx 3, TC 104).

Python: derive the global rank (node_rank*gpus_per_node+local_rank), forward
master_addr, pass local_world_size to the TorchRec planner and live world_size to
metrics. Single-node behavior unchanged.

Docs: add docs/multi_node_config.md (enablement details, lessons, and the
cluster-specific knobs to change per fabric); README + perf_opt updated for the
launch_slurm.sh rename.

Co-authored-by: Cursor <cursoragent@cursor.com>

* local dlrmv4 changes: docker setup, run scripts, walkthrough docs, smoke log path

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: decouple multi-node launch from per-user paths for portable baseline

Run the consolidated launch/streaming flow from any $HOME without editing
another user's tree:
- Derive REPO_MOUNT/DATA_MOUNT/SCRATCH from $HOME with env overrides; keep
  the shared read-only dataset path intact (no data duplication).
- Per-user container name (yambda_$USER) to avoid collisions.
- In-repo provisioning via launch_slurm.sh (drop external _provision script dep).
- chmod log files world-writable so the container (nobody) can tee-append
  under NFS root_squash; fixes spurious pipefail rc=1.
- Neutral in-repo TensorBoard default path.

Validated: 2-node ROCm/RDMA smoke (world_size 16) completed rc=0, 20-batch
train window with metrics logged.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: deterministic in-window shuffle diversity dial + opt-in diagnostics

Add STREAMING_SHUFFLE_FRACTION (0..1) as a config-invariant control over
in-window embedding diversity, replacing the legacy block/buffer knobs. Full
shuffle is the deterministic, seeded default (fraction=1.0). Add an opt-in
unique-embedding diagnostic (DIAG_UNIQUE_EMB) and gate chrome-trace capture
behind OUTPUT_TRACE; both default off to keep production runs overhead-free.
Forward the new env knobs through launch_slurm.sh.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: MLPerf training compliance logging for streaming-train-eval

Wire mlperf_logging (mllog) into the yambda-5b streaming-train-eval path:
rank-0-gated MLPerfLogger facade emitting the full event lifecycle
(cache_clear/init_start -> submission_info + hyperparameters ->
init_stop/run_start -> per-window block_start/stop + eval_start/accuracy/stop
-> run_stop), driven by a configurable convergence target.

Key points:
- AUC_THRESHOLD (gin, env-overridable; default 0.80275) doubles as the MLPerf
  convergence target: rank 0 decides on the global lifetime eval AUC and
  BROADCASTS the stop boolean so all ranks break in lockstep (avoids the
  ALLTOALL collective-timeout deadlock from a per-rank decision).
- MLPerfLogger uses the explicit global rank passed by train_ranker (computed
  pre-dist-init), so only true rank 0 logs and the compliance file has exactly
  one event each. Per-job MLPERF_LOG_PATH avoids stale append accumulation.
- Per-step train_loss POINT_IN_TIME event (global cross-rank mean) with
  samples_count + lr, plus a console/TensorBoard readout.
- cumulative_train_samples counter (global, checkpointed) as the samples_count
  progress unit; lifecycle gated on cold start so e2e-supervisor resumes never
  emit orphaned run boundaries.

Validated end-to-end at 1/2/4 nodes (8/16/32 GPUs): clean compliance log,
single run_stop=success, passes the common/closed_common compliance checks.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: min_history anchor-eligibility floor (decoupled from history_length)

A LISTEN event qualifies as a train/eval anchor once the user has >= min_history prior events, decoupled from history_length (the gather/truncation cap) since jagged attention handles short UIH. Anchor positions/anchor_ts caches are keyed by (history_length, min_history) and built independently of the _READY-gated 150GB flat store, so changing the floor rebuilds only the cheap positions array. Default None preserves the legacy full-history behavior (which dropped ~60% of users).

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: gin defaults — min_history=1, BATCH_SIZE env knob, default to no in-window shuffle

Bind get_dataset.min_history to $MIN_HISTORY (default 1 = ~all users). Make batch_size env-overridable via $BATCH_SIZE (default 1024). Change default streaming_shuffle_fraction 1.0 -> 0.0 (user-major, production streaming order); override per-run via $STREAMING_SHUFFLE_FRACTION (1.0 = full shuffle).

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: launch_slurm container hygiene + readiness gating + env passthroughs

Reap stale foreign GPU containers and restart our own to reclaim leaked HBM before launch; gate the worker exec on container State.Running + a probe with retry to fix container-restart races (which caused NCCL TCPStore 600s timeouts); APPEND_LOG=1 appends the metrics log on resume. Forward MIN_HISTORY/MAX_SEQ_LEN/HISTORY_LENGTH/BATCH_SIZE/CKPT_TIME_INTERVAL_S/DIAG_EMB_STEPS into the container. Drop the hardcoded --time; tidy header comments after launch_smoke_8gpu removal.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: durable per-boundary eval metrics JSONL sink + aggregate emb-diag

Write one JSON line per eval boundary to <LOG>.metrics.jsonl capturing the end-of-pass metrics over the fixed holdout (append-only, rank 0, survives restarts/resumes) — no interim/averaging ambiguity. Rework the unique-embedding diagnostic into an aggregate over DIAG_EMB_STEPS batches covering the cross-feature tables.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: default MIN_HISTORY=0 (include cold-start first events) to match min0 runs

All live min0 runs already export MIN_HISTORY=0; make the gin default match so future runs without an override include each user's cold-start first event (zero prior context) instead of requiring >=1 prior event.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: default NUM_TRAIN_TS=149 to sweep full ts=150..298 streaming range

Matches the long e2e runs (start_ts=150 + 149 daily windows). EVAL_EACH_WINDOW
and EVAL_EVERY_N_WINDOWS already default to 1. Clamped to the dataset's
available window count at runtime; override via $NUM_TRAIN_TS.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: consolidate eval cadence into single EVAL_EVERY_N_WINDOWS knob

Drop the redundant EVAL_EACH_WINDOW on/off boolean and fold "disable eval"
into the cadence int: 0 = eval off (train-only / resume test), 1 = every
window, N>1 = every Nth window (anchored to the absolute ts grid, stable
across resume). Updates streaming_train_eval_loop, the gin bindings, and the
resume test harness accordingly. No behavior change for the live runs
(EVAL_EVERY_N_WINDOWS=1).

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: random per-run seed + graceful teardown + no double-logging

- setup(): init the process group first, then draw a fresh random seed per
  run (rank 0 broadcasts so all ranks agree), export it to $SEED, and log it;
  pin $SEED to reproduce a run exactly. Data order/split are unaffected (still
  time-deterministic + $SPLIT_SALT-governed); the seed governs dense weight
  init. MLPerf SEED event now logs the actual chosen value, not hardcoded 1.
- train_ranker(): move distributed teardown into a finally block so a clean
  finish also barriers + destroy_process_group()s in lockstep, silencing the
  noisy TCPStore "broken pipe" / "should dump" warnings at exit. Best-effort
  so teardown never masks a real error.
- launch_slurm.sh: orchestrate sets WORKER_TEE=0 so the worker points its own
  file sink at /dev/null (stdout is already tee'd upstream), avoiding every log
  line being written twice. Direct single-node (e2e supervisor) keeps WORKER_TEE
  unset and still writes $LOG itself.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: fix single-node-without-slurm via run_docker.sh

run_docker.sh: strip an optional leading `--` so the documented
`run_docker.sh -- bash scripts/launch_slurm.sh` form works (it was
forwarded verbatim to `docker run` and failed exec: "--" not found);
wire run_docker.sh to the launch_slurm worker flow (CONTAINER_NAME,
LOG/MODE/MAX_SEQ_LEN/HISTORY_LENGTH passthrough) and forward
NCCL_SOCKET_IFNAME so the bootstrap NIC is host-overridable.

launch_slurm.sh: default NCCL_SOCKET_IFNAME=lo for single-node
(NNODES==1) instead of the meta64-only fenic0 — loopback is reachable
by all local ranks on any host (data plane is intra-node XGMI/PCIe),
so the single-node path now runs out-of-the-box on dev boxes with no
fenic0. Multi-node keeps the fenic0 default; both stay overridable.

Verified: 8-GPU single-node streaming-train-eval smoke runs clean
(RCCL init over lo, train+eval, MLPerf run_stop) on a lone MI355 node.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: gin-configurable quantized (bf16/fp16) embedding all-to-all

Quantize the bandwidth-bound embedding-shuffle all-to-all via TorchRec
QCommsConfig on the sequence EmbeddingCollectionSharder. Exposed as two
gin knobs on make_optimizer_and_shard (env-overridable):
  sparse_a2a_precision           = fp32 (off, default) | bf16 | fp16
  sparse_a2a_quantize_backward   = 1 (default) | 0 (forward-only)
Default fp32 keeps the path byte-for-byte identical to trunk. launch_slurm
forwards the env overrides and reuses a stopped container instead of
destructively re-provisioning.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: split quantized a2a into independent fwd/bwd precision knobs

Replace the single sparse-a2a precision knob with separate forward and
backward precision settings ($SPARSE_A2A_FWD / $SPARSE_A2A_BWD, each
fp32|bf16|fp16; both fp32 = off, identical to baseline). This enables the
TorchRec golden_training recommended mix (fwd=fp16, bwd=bf16): fp16's
mantissa suits bounded forward activations while bf16's wider exponent
range avoids gradient overflow. 2-node A/B shows fp16/bf16 at perf parity
with bf16/bf16 (both 2-byte wire), so it's a numerical-safety win at zero
perf cost.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: enable GPUDirect RDMA by default in slurm worker

Set NCCL_NET_GDR_LEVEL=5 and NCCL_DMABUF_ENABLE=1 by default so RCCL does
true GPU<->NIC DMA over bnxt_re instead of host-memory staging. The brcmrdma
host kernel ships the inbox peer-memory client, so GDR works with no
container/host changes. Measured ~+22% throughput at 2 nodes (65.7%->79.8%
weak-scaling efficiency). Overridable via NCCL_NET_GDR_LEVEL=0 and non-fatal
(falls back to host staging if peermem is absent).

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: gin-configurable RNG seed ($SEED) + default TRAIN_SPLIT_PERCENTAGE=1.0

Add a gin-configurable global seed so weight init (dense params + embedding
tables) is reproducible run-to-run and runs can be init-matched A/Bs. The seed
is bound via $SEED (seed_everything.seed = @seed/env_int(), default 1) and
applied by a new seed_everything() called in train_ranker right before
make_model() — after the full gin parse, so the binding resolves in the second
parse where env_int is registered. Move the old hardcoded seed=1 out of setup()
(too early to be gin-configurable). Forward $SEED through launch_slurm.sh.

Flip the default TRAIN_SPLIT_PERCENTAGE 0.90 -> 1.0 (all users trained AND
evaluated, matching the alleval/qa2a production runs) in both the gin default
and the launch_slurm.sh fallback.

Validated with two short 1-node runs: SEED=1 and SEED=2 each log their seed on
all ranks, and tsp=1.0 is applied without exporting TRAIN_SPLIT_PERCENTAGE.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: env-configurable dense/sparse LR + optimizer LR logging

Make the dense ($DENSE_LR) and sparse ($SPARSE_LR) optimizer learning rates
overridable per-run via env (defaults unchanged at 0.001), and log the
resolved LR at optimizer construction so runs are self-documenting. Forward
both vars through launch_slurm.sh into the container.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: env-configurable HSTU transformer depth ($HSTU_NUM_LAYERS)

Make the HSTU attention layer count overridable per-run via $HSTU_NUM_LAYERS
(default 5, unchanged), resolved in the full gin parse and forwarded through
launch_slurm.sh. Changing depth alters model shape, so a run with a new depth
must use a fresh CKPT_PATH (incompatible with existing checkpoints).

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: env-overridable dense/sparse LRs for sweeps + holdout default 1.0

Make dense (Adam) and sparse (RowWiseAdagrad) learning rates overridable via
$DENSE_LR / $SPARSE_LR with gin defaults preserved at 0.001, so LR sweeps don't
require editing gin. Resolve gin macro references in the MLPerf param logger so
env-overridden LRs are logged as real numbers. Default TRAIN_SPLIT_PERCENTAGE
to 1.0 (no holdout) and log the resolved LR overrides at orchestration time.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: disable TensorBoard by default (no-op writer)

The shared-NFS tfevents writer was the only metrics sink whose I/O error
was uncaught, and it repeatedly crashed trainers on transient /apps
Errno 121 (Remote I/O) hiccups. Default TENSORBOARD_LOG_PATH is now empty,
which installs a _NoOpSummaryWriter so the metrics path (compute +
text-log + .metrics.jsonl sinks) runs unchanged and never crashes on TB
file I/O. Nothing we consume reads TensorBoard. Re-enable per-run by
setting $TENSORBOARD_LOG_PATH to a non-empty dir.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: default dense/sparse LR=1e-5 and HSTU depth=3

Change the gin defaults to the configuration validated by the recent
power-user (min4086) ht299 runs: dense+sparse LR 0.001 -> 1e-5 and HSTU
attention depth 5 -> 3 (reaches ~0.78-0.81 holdout-299 AUC at ~1.4x
faster training than the 5-layer/5e-5 setup). Both remain env-overridable
via $DENSE_LR / $SPARSE_LR / $HSTU_NUM_LAYERS.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: report EVAL_ACCURACY as per-window AUC (configurable, default window)

The MLPerf EVAL_ACCURACY event and the convergence decision (early SUCCESS
RUN_STOP + end-of-run finalize) now use the per-pass full-holdout "window_auc"
instead of the cumulative "lifetime_auc". Made selectable via a new gin knob
streaming_train_eval_loop.eval_accuracy_auc_mode ($EVAL_ACCURACY_AUC_MODE),
default "window"; set "lifetime" to restore prior behavior. Both AUCs are still
computed and logged to TensorBoard. The rank-0-decides-then-broadcast deadlock
guard is preserved. Forward $EVAL_ACCURACY_AUC_MODE through launch_slurm.sh.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: consolidate streaming e2e supervisor to one sbatch-wrapping script

Replace the single-node docker-exec supervisor with the sbatch-job-level
model (formerly run_streaming_e2e_multinode.sh), which handles 1..N nodes
via launch_slurm.sh. Node replacement is now SLURM's job on resubmit, so
the in-place node-acquisition/provision/exec-sentinel logic is dropped.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: add non-SLURM local launcher + self-healing supervisor

launch_local.sh: single-host, scheduler-free analog of launch_slurm.sh's
worker phase — same train_ranker entry point / yambda_5b.gin config, smoke
or full run on a GPU host with no SLURM/docker/RDMA overlay.

run_streaming_e2e_local.sh: local analog of run_streaming_e2e.sh. Backgrounds
a per-run submit script (so wait $PID yields the trainer's real exit code),
relaunches from the latest checkpoint on crash/nonzero-exit/hang, with a
hang watchdog (frozen log + no trainer proc) and a pre-launch disk guard.
cleanup_container waits for GPU HBM to actually drain before relaunching so an
OOM/crash can't cascade into a dirty-GPU OOM loop.

* dlrmv4: set default-PG timeout to TIMEOUT (1800s) to survive checkpoint skew

The checkpoint DCP collectives run on the default process group created by
init_process_group, which had no explicit timeout and thus used NCCL's stock
600s watchdog. The 560GB sparse-embedding checkpoint is written to shared NFS
with a badly imbalanced sharding plan (per-rank shards ~37GB..~95GB), so the
fastest rank can wait >600s in the post-write allgather/barrier for the slowest
rank, tripping the watchdog and SIGABRTing an otherwise-healthy job (observed
on 3 nodes, always rank 7). Pass timeout=timedelta(seconds=TIMEOUT) so the
default PG tolerates the skew, matching the secondary new_group.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: optional gradient clipping for the streaming path ($GRAD_CLIP_NORM)

Add env-configurable global-norm gradient clipping to streaming_train_eval_loop,
applied to dense params after backward() and before optimizer.step() (sparse
tables use a fused optimizer and are unaffected, matching the non-streaming
path's clip_grad_norm_). Wired via gin to $GRAD_CLIP_NORM and forwarded into the
container by launch_slurm.sh. Default 0.0 = OFF, so existing streaming runs are
unchanged. Eliminates the window-131 eval-AUC dip at LR=1e-5.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: data-fraction eval cadence + lr1e-7/grad-clip-on defaults

Add a data-percentage-based eval cadence for the streaming loop, an
alternative to the per-window cadence. EVAL_EVERY_DATA_PCT>0 runs the
full-holdout eval every fixed FRACTION of the run's total training data,
so eval points are evenly spaced by data volume regardless of per-window
sample counts. The fraction is converted once into a global train-step
interval (round(pct * total_train_anchors / (batch_size*world_size)))
over the original requested window range, and eval fires on
global_step % interval -- mid-window and resume-stable, mirroring
checkpoint_step_frequency. Each eval label carries @step=<global_step>
for plotting against data volume.

- yambda: total_train_anchors(start_ts, num_ts) one-time O(N) count.
- streaming_train_eval_loop: eval_every_data_pct param + interval calc;
  the two cadences are mutually exclusive (ValueError if both >0).
- launch_slurm.sh: forward EVAL_EVERY_DATA_PCT into the container.

Also flip the yambda-5b gin defaults to the validated config: dense and
sparse LR 1e-5 -> 1e-7, and gradient clipping ON by default
(GRAD_CLIP_NORM 0.0 -> 1.0). HSTU depth stays 3.

Verified e2e on an open-pool node: interval computed correctly, two
mid-window evals fired on the step grid + final eval (rc=0), and the
both-enabled config raises the expected ValueError.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: seed embedding init + reproducibility checksum ($SEED)

Make embedding-table init a deterministic function of $SEED (dense init
already was), so runs are a clean init-matched A/B when data order
($STREAMING_SHUFFLE_SEED) and the holdout split ($SPLIT_SALT) are fixed.

- configs.py: attach a per-table seeded uniform init_fn (per-table seed =
  sha256($SEED, table_name)); meta-safe (skips the meta device DMP builds
  the unsharded module on). Init bounds mirror stock (+/-1/sqrt(N) or a
  table's explicit weight_init_min/max), so the distribution -- and thus
  model quality -- is unchanged; only determinism/seeding differs.
- utils.py (make_optimizer_and_shard): re-seed torch/torch.cuda from $SEED
  right before DistributedModelParallel(...) so the fused FBGEMM TBE
  on-device embedding init is reproducible for a FIXED sharding plan
  (Tier 1). Dense params are already built in make_model, so untouched.
- utils.py: log a post-DMP init checksum (per-table count/sum/sumsq +
  a one-line digest; sharded stats are all-reduced so the fingerprint
  covers the whole table regardless of shard layout). Gate via
  INIT_CHECKSUM (default on). Verified on idle nodes: same seed -> same
  digest across reruns and node types; different seed -> different digest.
- yambda_5b.gin: document precisely what $SEED controls and what it does
  not (streaming data order, holdout split).

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: default INIT_CHECKSUM off (fp64 shard copy OOMs the build)

The startup reproducibility checksum computes per-shard sum/norm with
dtype=float64, which materializes a full fp64 copy of each local embedding
shard (>150 GiB for the big tables). After sharding leaves ~95 GiB of fp32
tables resident, that temporary leaves almost no HBM headroom and OOMs the
build during make_optimizer_and_shard on any node with residual memory.

Flip INIT_CHECKSUM to default 0 (opt-in) so normal launches and supervisor
resubmits never run it. Correct the utils.py + yambda_5b.gin comments that
claimed it had no full-size temporaries.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: add last_n UIH history strategy ($HISTORY_STRATEGY)

Adds a configurable Yambda UIH construction strategy alongside the existing
per-pool interleaved scheme. "last_n" takes the last HISTORY_LENGTH events of
any pool (listen+/like/skip) with no per-pool split, raising effective sequence
length (~2.7k -> ~4.1k) and letting the like share fall to its natural rate.
Default stays "interleaved" (no behavior change); strategy is resolved at
sample-build time so it reuses the existing on-disk cache (no rebuild).

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: logging-freeze prep — MIN_HISTORY=4086 default + HISTORY_STRATEGY passthrough

- gin: MIN_HISTORY default 0 -> 4086 (power-users floor at the full history
  budget; maps to the existing positions_L4086.npy cache, no rebuild/no
  shared-dir write). AUC_THRESHOLD left unchanged (0.80275) pending finalization.
- launch_slurm.sh: forward $HISTORY_STRATEGY through the worker docker exec -e
  block (was silently dropped, so the knob never reached the worker); fix the
  stale lr-override echo (gin default is 1e-7, not 0.001).
- README: document MIN_HISTORY default as 4086.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: untrack docs/v4_vs_v2_and_hstu_walkthrough.md

Stop tracking the local walkthrough doc (kept on disk, no longer in the repo).

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: scrub hardcoded username from reference comments

Replace the example username in launch_slurm.sh / streaming_resume_test.sh
comments with a generic <user> placeholder. Runtime defaults already derive
the container name + mounts from $USER, so only doc/example strings changed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: README — add full single/multi-node reference run example

Document a complete sbatch launch (run-shape + data-fraction eval cadence) for
1-node and 2-node, noting the launchers differ only in --nodes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: exclude eval/checkpoint overhead from step_ms timing

MetricsLogger now brackets eval and checkpoint phases with pause/resume
perf timers so the reported step_ms reflects pure train-step latency.
Adds wall_step_ms (inclusive), eval_ms, and ckpt_ms to the perf log line
and TensorBoard scalars (appended for parser backward-compat). Checkpoint
saves and eval windows are wrapped with categorized pause/resume calls.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: README — use AUC_THRESHOLD=0.80275 in example for gin-default consistency

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: make a bare sbatch reproduce the frozen reference run

orchestrate() now defaults to the reference run-shape (START_TS=0,
NUM_TRAIN_TS=299, full windows) + the data-fraction eval cadence
(EVAL_EVERY_DATA_PCT=0.005, per-window off), so `sbatch scripts/launch_slurm.sh`
needs no env knobs. SMOKE=1 restores the previous fast functional defaults
(short window, capped batches, per-window eval). The two eval cadences are
auto-deconflicted (explicit EVAL_EVERY_N_WINDOWS>0 disables data-pct). gin
library defaults + the resume/local smoke paths are unchanged. README updated to
the bare single/multi-node commands.

Co-authored-by: Cursor <cursoragent@cursor.com>

* dlrmv4: address PR review — mlperf_lo…
* Merge checklists in PR template

* Delete .github/PULL_REQUEST_TEMPLATE directory

* Add PR template title

* Split PR templates and auto-append checklists from labels

Use a short default template plus new-benchmark and new-round files.
A pull_request_target workflow appends the matching checklist to the
PR body so it applies for UI, CLI, and fork PRs.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: ShriyaRishab <spalsamudram@nvidia.com>
)

Move the retired implementations under retired_benchmarks, add DLRMv4 to the v6.1 README table, and point historical README links at the new paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ShriyaRishab
ShriyaRishab requested review from a team as code owners August 20, 2026 15:45
@github-actions

Copy link
Copy Markdown

MLCommons CLA bot All contributors have signed the MLCommons CLA ✍️ ✅

@ShriyaRishab ShriyaRishab added the new-benchmark Introducing a new benchmark to MLPerf Training label Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new-benchmark Introducing a new benchmark to MLPerf Training

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants