Repository navigation
fix(tensor): fail closed when batch concat or DP dispatch sees inconsistent shards - #582
Conversation
…he differing shared path A TensorRef in a CONCAT field whose rows did not match the item batch size fell into the shared-value path, which compares proxies by shape and dtype only, so concat silently kept item 0's rows. It now raises. Shared-value mismatches now report the first differing path and values (e.g. Part.sampling_params.sde_indices[1]: 1 != 2, or sigmas.flat[10] for tensors), skip dataclass fields declared compare=False, and compare numpy arrays by value.
pytree_chunk goes back to replicating a Batch argument whose batch_size differs from the split size. Every DP_SCATTER method takes either a single Batch, which defines the split size itself, or the sync diffusion window tuple, whose rollouts all have prompts_per_rollout * samples_per_prompt rows from fixed-size drop_last loaders. The shared-value walk no longer skips dataclass fields declared compare=False; no dataclass in the repo declares one.
…nizer_output off the wire Guided t2i and t2ti stored the tokenizer's [cond; uncond] fused batch (2*B rows) unsplit in Part.conditions, so a DP merge or select interleaved the halves ([c0,u0,c1,u1]) and paired one sample's cond with another's uncond. it2i already split it into B-row fused / fused_uncond; that split is now split_cfg_fused and all three modes use it. _expand_cfg_for_forward restacks the halves, so sampling sees the same tensor as before. The raw tokenizer_output also has 2*B rows under CFG, so the stricter Batch.concat raised on any merge of those parts. Only the in-process sampling KV cache reads it; replay and predict_noise_at_step never do. HunyuanImage3DiffusionConditions.to_dict no longer emits it and from_dict no longer reads it.
c289983 to
0777c21
Compare
|
Rebased onto main Interaction with the new main commits. After #586, anchored AR and two-engine unified rollout outputs stay Checks on
GPU smoke (8×H20, 2–4 rollouts each). These ran on
Notes:
|
The numpy branch in the shared-value walk and the equality check on non-batched tensors in CONCAT fields were kept for the HunyuanImage3 tokenizer output, which comes from remote code. The diffusion conditions no longer carry it, and the AR conditions' copy is batched on dim 0 (cfg_factor=1) with no numpy fields. No other producer puts a numpy array in a Part field or a non-batched tensor in a CONCAT field. Non-batched tensors in a CONCAT field go back to keeping item 0, as on main. The TensorRef row check stays: TensorRef.concat joins spans regardless of row count.
…ur of Tencent-Hunyuan#582 Tencent-Hunyuan#582 makes Batch.concat raise when a non-empty item lacks a per-sample dict key, so a Part.concat that mixes samples with and without the keyframes key now raises in the generic path, in every sample order. The MiniMaxH3KeyframedText binding guarded exactly that case, so it is removed: to_dict and from_dict return to main's one-liners, and a producer may build Part.conditions by hand instead of through to_dict. MiniMaxH3Conditions.__post_init__ still rejects a keyframes sample count that differs from text, which Tencent-Hunyuan#582 does not check, and build_layout keeps its anchor, tag and row checks. Signed-off-by: MinhaoLi0318 <lminhao039@gmail.com>
Summary
Batch.concatcould silently produce a wrong batch when shards disagreed. Several cases were already caught by per-trainer guards that each patched one symptom; others were not. This PR makes the container fail closed in one place, removes those guards, and fixes the producers that the stricter rule exposed.Batch.concatused to keepvalues[0]for everySHAREDfield. It now requires structural equality across items and names the first differing path and values on mismatch (e.g.Part.sampling_params.sde_indices[1]: 1 != 2; tensors aspath.flat[i]: x != y). ATensorRefcompares by shape and dtype, because its rows live in the transport; the per-rollout SDE schedule is still caught exactly throughPart.sampling_params.CONCAT/PACKEDfield (and packedcu_seqlens) that isNoneor a missing dict key in some shards used to be dropped, leaving the merged lists shorter thanbatch_size. It may now beNoneonly in zero-row items.TensorRefin aCONCATfield must have one row per sample.TensorRef.concatjoins spans regardless of row count, so a ref with any other row count (e.g. a 2×B CFG batch) used to merge into misaligned rows; it now raises.Samplerejects a part with a blank, non-string, or repeated sample id, replacing the checks inPart.forkand the SGLang seed derivation.Part.concatoverride (primitive_metadataequality),Part.validated_sample_idsand its two callers, and the async trainer'srequire_single_rollout_id/_rollout_id/_require_single_generationplus its separate mixed-version check.combine_rollout_promptsnow concatenates first, then checks a single non-Noneoutput_versionacross generated parts.Nonefor rows lacking that media; every consumer already filtersNonerows.AgenticTrainerclears the per-trajectoryharness_statuson train parts before concatenating, as it already did forprimitive_metadata.[cond; uncond]fused batch (2×B rows) unsplit, so a DP merge orselectpaired one sample's cond with another's uncond; it2i already split it, and that split is nowsplit_cfg_fused, used by all three modes (_expand_cfg_for_forwardrestacks it bit-exactly). The rawtokenizer_output(also 2×B under CFG, read only by the in-process sampling KV cache) is no longer written toPart.conditions.Related Issue
N/A
Test Plan
SKIP=no-commit-to-branch pre-commit run --from-ref 0fe4f99d --to-ref HEAD→ all hooks passed; re-run after the rebase onto89ac5b03as--from-ref 89ac5b03one3d7f0a5→ all hooks passed.raystubbed → 32/32 checks passed. It covers:SampleDP chunk/cat round trip,split/select/slice, packedTextSegmentconcat,TensorRefconcat, and sharedsigmasasTensorRefs from two workers.output_version,sampling_paramswith differentsde_indices,LatentSegment.sde_indices,primitive_metadata, andharness_statusmismatches;Nonerewards, a missing condition key, and aNonepacked field in one shard; differing non-batched tensors; duplicate and empty sample ids.sampling_params,Broadcast(Sample), a mismatchedTensorRef, andNonein zero-row items.combine_rollout_promptswith same, mixed, and missing versions; agentic train parts after clearingharness_status; Qwen3-Omni image + video shards and Qwen3.5 text-only + image shards stay row-aligned after concat and microbatchselect.c2899833: one-off harnesses on Python 3 + torch 2.13 CPU withraystubbed → 27/27, plus two HunyuanImage3 harnesses (all pass). The HunyuanImage3 ones cover:split_cfg_fused+_expand_cfg_for_forwardreproducing the tokenizer fused bit-exactly,cfg=Falsepassthrough, row-count mismatch raising, merged and selected guided parts keeping each cond row with its own uncond (worker-side and driver-sideTensorRef), and guided it2i parts with a 2×Btokenizer_outputmerging (the parent commit raised). The 27 general checks cover aTensorRefCONCATrow mismatch (raises), batched and zero-rowTensorRefconcat, a tensor mixed with aTensorRef(raises), equal vs differing non-batched tensors, path-named errors (sampling_params.sde_indices,output_version, dict keys, tensor shape, a middle-only tensor element, 2-D, NaN, numpy), all dataclass fields compared, sharedTensorRefby shape/dtype,Noneonly in zero-row items, theSampleDP chunk/cat round trip, and duplicate sample ids.experiments/trajectory_ir/run_cpu_evidence.py --allow-dirtyagainst07dec547) →PASS: 10 correctness checks. Not re-run on the follow-up commits; reason: harness not present in that checkout.e3d7f0a5(drops the numpy branch and the non-batchedCONCATtensor equality check, which have no in-repo producer; non-batched tensors keep item 0 as on main): general harness → 26/26, both HunyuanImage3 harnesses pass.c2899833(8×H20, 2–4 rollouts each), all passed: SD3.5 FlowGRPO sync DP merge andaccumulate_rollouts=2(ratio 1.0000), BAGEL async with weight sync every rollout, Qwen3-Omni mixed image + video rows (single engine replica, so no cross-replica merge), HunyuanImage3 t2i guided + KV cache (ratio 0.9997) and it2i guided (ratio 0.9999), agentic default and tight. Not re-run after the rebase or one3d7f0a5; reason: patches unchanged by the rebase, ande3d7f0a5removes only branches no in-repo path reaches. Details in the PR comment.Compatibility / Risk
max_inflight=1; agentic now clearsharness_status; every DP_SCATTER method takes a singleBatchor a tuple of equal-size parts, sopytree_chunkis left unchanged.harness_status(now cleared) and guided HunyuanImage3 t2i/t2ti cond/uncond mis-pairing (fixed). Already guarded before, now unified: mixedoutput_version,primitive_metadata, multi-rollout async diffusion batches, root sample-id uniqueness. Data-dependent: producers dropping a media key per shard (Qwen3-Omni / Qwen3.5 fixed here). The remaining checks guard values from remote-code objects whose types cannot be verified in-repo.sde_indicesand documents that the merged shared fields "are not per-sample truth". After this PR that concat raises, so feat(tempflowgrpo): add TempFlow-GRPO with per-step branching #552 should normalise those shared fields (per-sample truth already lives in itssde_index_per_sampleCONCAT field) before merging.output_versionbefore concatenating.SHAREDfield holding an object without a meaningful__eq__would now raise after Ray deserialization. Todaysampling_paramscarries only plain values (scheduleris set toNonein requests, andsde_strategyis unset in every recipe).Reviewer Notes
.claude/skills/development/pr-review): two rounds with a fresh-context subagent on the same model as the author (no different model was available to request). Round 1 found that the trainside Qwen3-Omni / Qwen3.5 chat templates still dropped media keys; that is fixed here. Round 2 was clean.9ca249d0andc2899833with a fresh-context Grok 4.7 subagent (different model family), two rounds on the full branch diff. Fixed: unbatchedTensorRefin aCONCATfield now raises; shared mismatches name the differing path and values (tensor diffs report the first differing element, since truncated reprs could print identical text for both sides); numpy compared by value; an unreachabletorch.is_tensorfallback removed. Rejected: a finding that matching-rowTensorRefs with different dtype orshape[1:]merge with item 0's metadata.cat_rowspads a ragged dim 1 on purpose, and the metadata comes fromTensorRef._from_spans, which this PR does not touch; the base reached the sameTensorRef.concat. The reviewer withdrew it. A later audit removed two guards no in-repo path can reach (pytree_chunkraising on a mismatchedBatch; skippingcompare=Falsedataclass fields) and found the HunyuanImage3 guided cond/uncond mis-pairing, fixed and re-reviewed clean. After the rebase onto89ac5b03, a fresh-context Grok 4.7 review of the interaction with perf(trainer): stop driver-hydrating anchored rollout outputs before DP scatter #586 (driver-sideTensorRefs) found nothing; a further audit dropped the numpy branch and the non-batchedCONCATtensor equality check, whose only justification (HunyuanImage3 diffusiontokenizer_output) left with0777c210, reviewed clean by Grok 4.7.unirl/distributed/tensor/batch.py:_first_difference,_shared_value,_any_present,_concat_value; thenunirl/models/hunyuan_image3/conditions.py:split_cfg_fused.Checklist