Skip to content

fix(reshard): map global QKV intervals when KV heads are below TP - #650

Closed
KavinKrishnan wants to merge 1 commit into
mainfrom
kavink/reshard-gqa-global-qkv
Closed

fix(reshard): map global QKV intervals when KV heads are below TP#650
KavinKrishnan wants to merge 1 commit into
mainfrom
kavink/reshard-gqa-global-qkv

Conversation

@KavinKrishnan

@KavinKrishnan KavinKrishnan commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

The bug

Publishing a Megatron fused QKV tensor computed a local KV head count as num_kv_heads // tp_size. Nemotron Ultra has 64 query heads but only 2 KV heads, so at TP8 that is 2 // 8 = 0 — either a ZeroDivisionError or invalid local Q/KV head geometry, depending on the path.

The arithmetic wasn't the problem. The premise was.

Why the premise was wrong

Megatron does not hand every TP rank a whole KV head. It slices the globally interleaved fused QKV tensor by raw rows. When KV heads are fewer than TP ranks, most ranks legitimately own query rows and no K or V rows at all. There is no local KV head count to compute, so no amount of fixing the division helps.

Megatron's row order is one block per KV group — that group's query rows, then its single K head, then its single V head — repeated per group. Q, K and V therefore interleave rather than forming three contiguous regions:

flowchart LR
    subgraph G0["KV group 0"]
      Q0["Q rows"] --> K0["K head"] --> V0["V head"]
    end
    subgraph G1["KV group 1"]
      Q1["Q rows"] --> K1["K head"] --> V1["V head"]
    end
    G0 --> G1
Loading

The fix

Map each rank's real row interval through that layout instead of inventing a local head count.

flowchart LR
    E[Global per-layer Q/KV metadata] --> L[_QkvLayout]
    L --> B["bands(): projection runs<br/>in global row order"]
    S[This rank's raw fused-row interval] --> X[Interval intersection]
    B --> X
    X --> Q[q_proj shards]
    X --> K[k_proj shards]
    X --> V[v_proj shards]
Loading

A rank that owns no K/V rows simply matches no K/V band and publishes Q only. The sparse per-rank offers merge into complete tensors downstream.

Review guide

Two files, and they are the whole change:

File Lines What to look at
modelexpress_rl/train/engines/megatron/aliases.py +182 the mapping itself
tests/test_reshard_megatron_gqa.py +390 reconstruction and planner coverage

In aliases.py, the code is deliberately arranged so the layout is a named thing rather than index arithmetic inside a loop:

  • _QkvLayout — holds the head counts, derives the group geometry.
  • bands() — states Megatron's row order exactly once, as a sequence of runs. If our understanding of Megatron's layout is ever wrong, or upstream changes it, this is the single place to correct.
  • _read_qkv_layout — validates the published metadata; half-specified metadata fails closed rather than guessing.
  • _build_global_qkv_aliases — a plain interval intersection against those runs.

Worth scrutinising: the per-group overlap math, that every source row is mapped exactly once (there's an explicit coverage assertion), that empty K/V tensors are omitted rather than published as zero-row tensors, and that divisible layouts still take the legacy path unchanged.

Compatibility

Descriptors carrying only the old divisible local-head fields keep working through the legacy path, so this is not a breaking change for existing topologies. The new path activates only when global head metadata is present.

Requires the companion NeMo-RL change that publishes global, per-layer QKV geometry: NVIDIA-NeMo/RL#3632. Neither half is useful alone — NeMo-RL publishes the geometry, ModelExpress maps the intervals.

Evidence, and what is still missing

Qualified on CPU and representative CUDA tensors:

  • Q=64, KV=2, head_dim=128 at logical TP8 reconstructs byte-exact Q/K/V with no gaps or overlaps
  • ranks without K/V rows publish Q only, and sparse tables merge into complete sources with zero fallback
  • divisible layouts remain byte-for-byte identical to the legacy local-head path
  • 24Q/6KV/TP4 and heterogeneous per-layer geometry are covered
  • missing or invalid global geometry fails closed
  • BF16 CUDA same-weight and changed-weight refits agree on parameters and projection outputs

Not yet covered: real Megatron TP8 to vLLM TP8 end-to-end for Q=64/KV=2, and full Nemotron Ultra qualification. I'd rather state that plainly than imply this is fully qualified.

Known limitation

This reverse-engineers Megatron's fused-QKV row order rather than obtaining it from a supported interface, so it is sensitive to upstream Megatron changes. bands() exists to keep that assumption in one auditable place. Asking Megatron to expose the layout properly is the right long-term fix and is worth a follow-up.

Test plan

  • PYTHONPATH=. python3 -m pytest tests/test_reshard_megatron_gqa.py -q — 15 passed
  • Full reshard suite on this branch — no regressions
  • Real Megatron TP8 to vLLM TP8 Q=64/KV=2 E2E
  • Full Nemotron Ultra qualification

Context

Split out of #635 at review request, to grow this surface gradually rather than land it all at once. Independent of the other split PRs — it touches only aliases.py and its test.

Summary by CodeRabbit

  • New Features

    • Improved support for Megatron models using grouped-query attention (GQA), including interleaved head layouts and uneven query/key-value distribution across devices.
    • Added reliable reconstruction of Q, K, and V weights across supported parallel configurations.
  • Bug Fixes

    • Prevented incomplete or inconsistent model metadata from producing incorrect weight mappings.
    • Improved handling of sparse key/value assignments and missing tensors with safe failure behavior.
    • Verified refits preserve model outputs across supported GPU configurations.

Computing num_kv_heads // tp_size gives zero for a model with fewer KV heads
than trainer TP ranks -- Nemotron Ultra has 2 KV heads at TP8 -- so publishing
a local KV head count either divided by zero or rejected the layout outright.

The premise was wrong rather than the arithmetic. Megatron does not hand every
TP rank a whole KV head. It slices the globally interleaved fused QKV tensor by
raw rows, so most ranks legitimately own query rows and no K or V rows at all.

Map each rank's real row interval instead of inventing a local head count.
_QkvLayout derives the group geometry from the global head counts and bands()
states Megatron's row order once, as a sequence of runs; the mapping is then an
interval intersection, and a rank owning no K/V rows simply matches no K/V band.
The sparse per-rank offers merge into complete tensors downstream.

Requires the companion NeMo-RL change that publishes global, per-layer QKV
geometry. Descriptors carrying only the old divisible local-head fields keep
working through the legacy path, and half-specified metadata fails closed.

Qualified on CPU and representative CUDA tensors: Q=64/KV=2/head_dim=128 at
logical TP8 reconstructs byte-exact Q/K/V with no gaps or overlaps, divisible
layouts stay byte-identical to the legacy path, 24Q/6KV/TP4 and heterogeneous
per-layer geometry are covered, and BF16 CUDA same-weight and changed-weight
refits match on parameters and projection outputs. Real Megatron TP8 to vLLM
TP8 end-to-end is still outstanding.

Split out of #635 so the interval mapping can be reviewed on its own.

Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 9f8a6885-03c0-4bdf-8c50-cc5f5b3a6606

📥 Commits

Reviewing files that changed from the base of the PR and between 535898a and f7e0dd5.

📒 Files selected for processing (2)
  • modelexpress_client/python/modelexpress_rl/train/engines/megatron/aliases.py
  • modelexpress_client/python/tests/test_reshard_megatron_gqa.py

Included review availability: Your plan provides up to 12 included reviews per hour; 10 remain after this review.


Walkthrough

Megatron QKV aliasing now supports global interleaved head metadata, sparse KV ownership, validation, and legacy compatibility. New tests verify reconstruction, transfer planning, CUDA refits, logits, and fail-closed behavior.

Changes

Megatron GQA QKV aliasing

Layer / File(s) Summary
Global QKV geometry and dispatch
modelexpress_client/python/modelexpress_rl/train/engines/megatron/aliases.py
QKV alias construction validates global metadata, source row intervals, head geometry, and hidden dimensions. It selects global-layout or legacy handling based on available metadata.
Interleaved shard publication
modelexpress_client/python/modelexpress_rl/train/engines/megatron/aliases.py
The global path maps source intervals into Q, K, and V destination shards, verifies complete coverage, omits empty projections, and preserves legacy alias construction.
Resharding and refit coverage
modelexpress_client/python/tests/test_reshard_megatron_gqa.py
Tests cover varied QKV geometries, sparse KV tables, legacy equivalence, bounded transfer plans, CUDA refits, logit equivalence, and invalid metadata.

Estimated code review effort: 4 (Complex) | ~60 minutes

Merge Risk: 🔵 Low · up to f7e0d

The new global QKV mapping requires the companion emitter to provide complete compatible metadata; incomplete metadata can cause descriptor rejection rather than legacy fallback. The PR is mergeable with explicit owner awareness and follow-up on that integration contract.

Poem

I’m a rabbit with shards in a row,
Watching Q, K, and V align just so.
Global heads guide every hop,
Gaps and overlaps now must stop.
Legacy paths still safely go.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main fix: mapping global QKV intervals when KV heads are below the tensor-parallel size.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@KavinKrishnan

Copy link
Copy Markdown
Contributor Author

Closing — folding this back into #635 rather than reviewing it separately.

The split would have needed three separate approvals, and reviewer availability is the real constraint here, not diff size. One PR with a clear review guide is the faster path.

Nothing is lost: the change is in #635 unchanged, and the readability concern that prompted the split is addressed there by giving the fused QKV row layout a name (_QkvLayout / bands()) instead of leaving it as index arithmetic inside a nested loop.

@KavinKrishnan
KavinKrishnan deleted the kavink/reshard-gqa-global-qkv branch August 18, 2026 23:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant