Skip to content

fix: use explicit head dim for Puzzletron KV cache - #2485

Open
devxMani wants to merge 1 commit into
NVIDIA:mainfrom
devxMani:main
Open

devxMani wants to merge 1 commit into
NVIDIA:mainfrom
devxMani:main

Conversation

@devxMani

@devxMani devxMani commented Sep 20, 2026

Copy link
Copy Markdown

What does this PR do?

Type of change: Bug fix

Fixes an incorrect Puzzletron KV-cache memory calculation for models where
head_dim differs from hidden_size // num_attention_heads.

The change:

  • Allows calculate_kv_dim() to accept an explicit head_dim.
  • Uses the model's configured head dimension when calculating KV-cache size.
  • Preserves n_embd // n_head as a fallback for backward compatibility.
  • Adds regression tests covering both explicit head_dim and the fallback behavior.

Usage

No new API or usage pattern is introduced. Existing callers continue to work
without providing head_dim.

Testing

Added unit tests covering:

  • Explicit head_dim when it differs from n_embd // n_head.
  • The existing fallback behavior when head_dim is not provided.

Tests were not run locally; the changes were made in github.dev.

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: N/A
  • Did you get Claude approval on this PR?: ❌

Additional Information

Fixes #2482

Summary by CodeRabbit

  • Bug Fixes

    • Improved KV-cache memory estimates for models with model-specific query/key head dimensions.
    • Added fallback handling to preserve accurate sizing when an explicit head dimension is unavailable.
  • Tests

    • Added coverage for both explicit head dimensions and calculated fallback values.

@devxMani
devxMani requested review from a team as code owners September 20, 2026 13:31
@copy-pr-bot

copy-pr-bot Bot commented Sep 20, 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 Sep 20, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: NVIDIA/Model-Optimizer/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 3cc2d736-6628-4446-8055-3e5d1cad93ec

📥 Commits

Reviewing files that changed from the base of the PR and between b311c05 and 459c6c5.

📒 Files selected for processing (3)
  • modelopt/torch/puzzletron/subblock_stats/calc_subblock_params_and_memory.py
  • modelopt/torch/puzzletron/utils/misc.py
  • tests/unit/torch/puzzletron/test_misc.py

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


📝 Walkthrough

Walkthrough

The KV-dimension calculation now accepts an optional model-specific head dimension. Attention memory resolves this value from configuration and passes it to the calculation. Unit tests cover explicit head dimensions and fallback derivation.

Changes

KV-cache dimension correction

Layer / File(s) Summary
Head-dimension resolution and validation
modelopt/torch/puzzletron/utils/misc.py, modelopt/torch/puzzletron/subblock_stats/calc_subblock_params_and_memory.py, tests/unit/torch/puzzletron/test_misc.py
calculate_kv_dim uses the supplied head_dim when present and otherwise derives it from n_embd // n_head. calculate_attention_memory resolves the configured head dimension and passes it to the calculation. Tests cover both paths.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 change: using the explicit head dimension for Puzzletron KV-cache calculations.
Linked Issues check ✅ Passed The changes satisfy [#2482]. calculate_kv_dim() accepts head_dim and computes 2 * num_key_value_heads * head_dim. calculate_attention_memory() resolves attention_config.qk_head_dim, then the…
Out of Scope Changes check ✅ Passed The changed production code and tests directly support [#2482]. The signature extension, KV-cache integration, fallback behavior, and regression tests are within the issue scope. No unrelated change i…
Security Anti-Patterns ✅ Passed PASS. The authoritative diff contains only Puzzletron dimension arithmetic and unit tests. Added Python lines do not contain torch.load(..., weights_only=False), numpy.load(..., allow_pickle=True), tr…
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

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

@kevalmorabia97

Copy link
Copy Markdown
Collaborator

/claude review

@kevalmorabia97
kevalmorabia97 removed the request for review from AAnoosheh September 22, 2026 08:19

kv_dim = calculate_kv_dim(attention_config.num_key_value_heads, n_head, n_embd)
lm_config = descriptor.get_language_model_config(model_config)
head_dim = getattr(attention_config, "qk_head_dim", None) or getattr(lm_config, "head_dim", None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[IMPORTANT Correctness] attention_config.qk_head_dim does not exist, so this half of the expression is permanently dead.

AttentionConfig (modelopt/torch/puzzletron/block_config.py:201) is a @dataclass(frozen=True, kw_only=True) whose full field set — including the SubblockConfig base at line 126 — is:

no_op, replace_with_linear, sparsify, weights_precision,   # SubblockConfig
num_key_value_heads, llama4, mamba                          # AttentionConfig

There is no qk_head_dim anywhere in modelopt/torch/puzzletron/ (a repo-wide grep finds only this new line), and because the dataclass is frozen it cannot be injected at runtime either. So getattr(attention_config, "qk_head_dim", None) always evaluates to None, and the resolved head_dim always comes from lm_config.head_dim.

Why it matters: it reads as if per-subblock head dims are honored during the Puzzletron search, which is the case a reviewer would most want to trust here. Anyone later adding a real per-subblock head dim will assume this line already wires it up. It is also silently unreachable — no error surfaces, the value is just wrong-by-omission if a subblock ever does override the head dim.

Suggested fix: drop the getattr on attention_config and read the head dim from the language-model config only (see my top-level comment for the version that reuses the existing helper). If honoring a per-subblock override is actually intended, add the field to AttentionConfig and plumb it through to_dict/from_dict so it round-trips.

Separately, prefer is not None over or when chaining these: or treats a legitimately falsy 0 as "unset". Not reachable today, but it is the kind of guard that breaks quietly once a real field lands.

@@ -0,0 +1,18 @@
from modelopt.torch.puzzletron.utils.misc import calculate_kv_dim

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[IMPORTANT Compatibility] This new file is missing the Apache-2.0 license header, which will fail the repo's pre-commit gate.

.pre-commit-config.yaml:91 runs the insert-license hook (insert-license-py) over types_or: [python, shell], and its exclude list names only a handful of specific files that carry third-party headers — tests/ is not excluded. The hook will rewrite this file to insert the header and exit non-zero, so CI pre-commit fails until the header is committed.

Every sibling in this directory already has it (tests/unit/torch/puzzletron/conftest.py:1, test_checkpoint_utils_hf.py:1). Prepend the standard block, matching the 2026 form used by the newer files:

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

Running pre-commit run --all-files locally will add this for you (the PR description notes the change was made in github.dev without running anything locally).

seq_len = min(seq_len, attention_chunk_size)

kv_dim = calculate_kv_dim(attention_config.num_key_value_heads, n_head, n_embd)
lm_config = descriptor.get_language_model_config(model_config)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[CRITICAL Algorithm] Reading head_dim from the teacher config overrides the swept candidate hidden size, regressing KV-cache estimates for every hidden-size pruning candidate.

n_embd here is not the teacher's hidden size — it is a swept candidate value. Tracing upstream:

  • calc_subblock_stats.py:319-321 builds model_hidden_sizes = model_hidden_sizes + [lm_config.hidden_size] ("add a teacher model hidden size"), i.e. a list of candidate hidden sizes plus the teacher's.
  • calc_subblock_stats.py:326 iterates product(batch_sizes, data_types, model_hidden_sizes).
  • calc_subblock_stats.py:345-346 passes n_embd=model_hidden_size (varies per iteration) and n_head=lm_config.num_attention_heads (fixed).
  • model_config stays the fixed teacher config throughout, so lm_config.head_dim is a constant across the whole sweep.

The old head_size = n_embd // n_head correctly tracked each candidate hidden size. The new code pins head_size to the teacher's head_dim for all candidates.

Concrete failure: teacher with hidden_size=5120, num_attention_heads=40, head_dim=128, num_key_value_heads=8. For the candidate model_hidden_size=2560, the correct head size is 2560 // 40 = 64kv_dim = 2*8*64 = 1024. This code yields head_dim=128kv_dim = 2048, a 2× overestimate of kv_cache_size, which flows straight into kv_cache_memory_mib and memory_mib. Those are the memory costs the Puzzletron search optimizes against, so every hidden-size-reduced candidate is now penalized as twice as KV-expensive as it is, skewing candidate selection. Note this hits plain Llama teachers too: modern LlamaConfig always populates head_dim, so getattr(lm_config, "head_dim", None) returns a value and the old derivation is never reached.

The fix for #2482 needs to distinguish the two cases: head_dim is an independent model property only when it is not simply hidden_size // num_attention_heads (Qwen3, gpt-oss, etc.). When it is derived, it must keep tracking n_embd:

lm_config = descriptor.get_language_model_config(model_config)
configured_head_dim = getattr(lm_config, "head_dim", None)
# Only an explicitly decoupled head_dim is a fixed model property; a derived one must
# track the candidate hidden size being swept in n_embd.
if configured_head_dim == lm_config.hidden_size // lm_config.num_attention_heads:
    configured_head_dim = None

kv_dim = calculate_kv_dim(
    attention_config.num_key_value_heads, n_head, n_embd, head_dim=configured_head_dim
)

A regression test that pins this would be valuable: assert kv_cache_memory_mib still halves when n_embd halves for a teacher whose head_dim == hidden_size // num_heads. The current unit tests only exercise calculate_kv_dim in isolation, so they cannot catch this.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

On the CRITICAL finding in the automated review, I think the premise doesn't hold for how Puzzletron prunes width. EmbeddingPruningSpec.update_config_object rewrites only hidden_size; head_dim, num_attention_heads and num_key_value_heads are left as in the teacher. A hidden-size-pruned child therefore builds k_proj/v_proj with unchanged output dimensions, and its KV cache per token is 2 * num_key_value_heads * head_dim, independent of the candidate hidden size. Using the teacher's head_dim for every candidate in the sweep is the correct value, not an overestimate. The previous n_embd // n_head understated KV for width candidates too, since it made KV shrink with hidden size. The suggested workaround (fall back to n_embd // n_head when head_dim == hidden_size // num_attention_heads) would bring that back for Llama-style teachers. A useful regression test would be the opposite of the one proposed: kv_cache_memory_mib stays constant when n_embd is reduced with heads unchanged.

Separately, qk_head_dim does not exist on AttentionConfig on main but does on feature/puzzletron_v2, where calculate_additive_metrics already reads it, so a port of this fix to v2 could honour it there.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude review — changes requested

Reviewed all 3 changed files (modelopt/ first, then tests/). Full scope, no files skipped.

Findings: CRITICAL: 1 · IMPORTANT: 2 · SUGGESTION: 2

Most impactful

1. [CRITICAL Algorithm] The fix regresses KV-cache estimates for hidden-size pruning candidates. n_embd in calculate_attention_memory is a swept candidate hidden size, not the teacher's — calc_subblock_stats.py:319-346 iterates product(batch_sizes, data_types, model_hidden_sizes) and passes n_embd=model_hidden_size while model_config stays the fixed teacher. Pinning head_size to the teacher's lm_config.head_dim makes it constant across the sweep, where the old n_embd // n_head correctly tracked each candidate. A teacher with hidden_size=5120, n_head=40, head_dim=128 evaluating candidate hidden_size=2560 now gets kv_dim=2048 instead of 1024 — a 2x KV-cache overestimate feeding the search objective. This hits plain Llama teachers, since modern LlamaConfig always populates head_dim, so the old derivation is never reached. Fix sketch in the inline comment: treat head_dim as a fixed model property only when it is not hidden_size // num_attention_heads.

2. [IMPORTANT Correctness] attention_config.qk_head_dim does not existAttentionConfig is a frozen dataclass whose fields are no_op, replace_with_linear, sparsify, weights_precision, num_key_value_heads, llama4, mamba, and qk_head_dim appears nowhere else in modelopt/torch/puzzletron/. That branch is permanently dead and cannot be populated at runtime.

3. [IMPORTANT Compatibility] tests/unit/torch/puzzletron/test_misc.py is missing the Apache-2.0 header, so the insert-license pre-commit hook (.pre-commit-config.yaml:91, applies to all Python outside a short exclude list) will rewrite it and fail CI.

Suggestions

[SUGGESTION] Reuse the existing head-dim helper instead of adding a third copy. _lm_head_dim at modelopt/torch/puzzletron/pruning/pruning_utils.py:70 already implements exactly this resolution, and _get_head_dim at tools/bypassed_training/child_init.py:1228 is a second variant. CONTRIBUTING.md asks for a single source of truth. Since calc_subblock_stats.py:345-346 passes precisely hidden_size and lm_config.num_attention_heads, the two fallbacks coincide at the live call site — so consolidating is behavior-preserving. Consider promoting one canonical helper into puzzletron/utils/misc.py and having pruning_utils and this call site both use it. Note that consolidation alone does not fix finding 1: the sweep-vs-teacher distinction has to be handled at this call site either way.

[SUGGESTION] pre-commit will reformat three spots. Flagging only because the PR notes nothing was run locally:

  • calc_subblock_params_and_memory.py:363-368 — the calculate_kv_dim(...) arguments are indented at the statement's own level and the closing ) sits in column 0; valid Python, but ruff-format will rewrite it.
  • utils/misc.py:61 — only one blank line before raise_unknown_subblock_config_error (E302).
  • tests/unit/torch/puzzletron/test_misc.py — no trailing newline.

pre-commit run --all-files clears all three plus the license header.

Risk

Moderate. The diff is small and the stated bug (#2482) is real, but as written the change trades one wrong KV-cache estimate for another that affects a broader set of models — anything driven through the model_hidden_sizes sweep, including the common Llama case. Since these numbers feed the Puzzletron search's memory objective, the errors are silent: no exception, just skewed candidate rankings. Worth resolving finding 1 before merge.

Also flagging that the unit tests, while correct, only cover calculate_kv_dim in isolation and so cannot catch the regression above — a test at the calculate_attention_memory level asserting kv_cache_memory_mib halves when n_embd halves would pin the intended behavior.

@codecov

codecov Bot commented Sep 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.00000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.14%. Comparing base (b311c05) to head (459c6c5).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
.../subblock_stats/calc_subblock_params_and_memory.py 0.00% 3 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2485   +/-   ##
=======================================
  Coverage   71.14%   71.14%           
=======================================
  Files         602      602           
  Lines       66397    66398    +1     
=======================================
+ Hits        47239    47242    +3     
+ Misses      19158    19156    -2     
Flag Coverage Δ
unit 58.12% <50.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kevalmorabia97

Copy link
Copy Markdown
Collaborator

@devxMani please fix the code quality errors

This branch has not been deployed

No deployments
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.

[Puzzletron] KV-cache memory computes head size as n_embd // n_head instead of reading head_dim

3 participants