Skip to content

Fix 4-bit split QKV projection#1502

Merged
jlarson4 merged 3 commits into
TransformerLensOrg:devfrom
abhinav-bellapu:codex/fix-4bit-split-qkv
Jul 15, 2026
Merged

Fix 4-bit split QKV projection#1502
jlarson4 merged 3 commits into
TransformerLensOrg:devfrom
abhinav-bellapu:codex/fix-4bit-split-qkv

Conversation

@abhinav-bellapu

@abhinav-bellapu abhinav-bellapu commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #737.

Summary

  • Keep the fused bitsandbytes matmul for ordinary [batch, pos, d_model] Q/K/V inputs.
  • For split [batch, pos, head, d_model] inputs, dequantize the 4-bit weight once and reuse complex_attn_linear for the head-wise projection.
  • Centralize Params4bit validation in the shared projection helper so Q, K, and V report the same clear error.
  • Add CPU-safe fake-bitsandbytes regression coverage for both ordinary and split inputs.
  • Refresh the affected quarantine line references.

Why

The old 4-bit path reshaped split Q/K/V projections incorrectly, producing the shape error in #737. Projecting every input head through every output head before selecting the diagonal would fix the shape but introduce an n_heads-fold compute and memory cost. Dequantizing once and sharing the established head-wise projection path preserves the intended semantics without that transient.

Validation

  • env UV_PYTHON=3.12 make format
  • env UV_PYTHON=3.12 make check-format
  • uv run --python 3.12 pytest tests/unit/components/test_attention.py -q (15 passed, 4 skipped)
  • uv run --python 3.12 mypy . (300 source files clean)
  • env UV_PYTHON=3.12 make unit-test (3,027 passed, 33 skipped, 9 xfailed)

@abhinav-bellapu abhinav-bellapu marked this pull request as ready for review July 10, 2026 22:07
@abhinav-bellapu

Copy link
Copy Markdown
Contributor Author

@jlarson4 ready for review

@jlarson4 jlarson4 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for this fix! I verified it locally. The unit-tier, fake-bnb approach for CPU-safe coverage is a nice touch.

There are a couple organization changes outlined below that we should address before merging, but altogether the solution is sound.

Outside of the current edits in this PR, it would be appreciated if you could update tests/QUARANTINES.md.

The new test moves the two quarantined entries in tests/unit/components/test_attention.py (cited there as :48 and :83) down by ~72 lines. Please refresh those line references.


return q, k, v

def _project_4bit_qkv(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The 4D path use n_heads times the necessary compute and memory – please dequantize once and reuse complex_attn_linear instead.

_project_4bit_qkv projects every input head through all heads' weights and then keeps only the diagonal. On a 4-bit Llama-7B (32 heads) at pos=2048, that's a ~512 MiB fp16 (~1 GiB fp32) transient per projection, three times per layer, of which only the ~16 MiB diagonal survives. This is a real OOM risk on the memory-constrained setups people use 4bit models to avoid.

Since matmul_4bit dequantizes internally anyway, dequantizing the weight lets the split path share the already-tested einsum instead of maintaining a parallel implementation. Divergence between these two paths is how #737 happened in the first place.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Additionally, if you could possibly add a docstring comment to this function?

@@ -446,59 +446,20 @@ def calculate_qkv_matrices(
)
if self.cfg.load_in_4bit:
W_Q_4bit = cast(Params4bit, self.W_Q)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

W_Q currently gets a static cast while W_K/W_V get the runtime isinstance + ValueError. A non-quantized W_Q under load_in_4bit=True dies with an opaque AttributeError.

Let's move the isinstance guard from the W_K/W_V cases into the helper and drop the Q/K/V asymmetry. Checking isinstance(weight, Params4bit) inside _project_4bit_qkv gives all three weights the clear error and collapses the three near-identical call-site blocks.

@jlarson4 jlarson4 changed the base branch from main to dev July 14, 2026 22:44
@jlarson4

Copy link
Copy Markdown
Collaborator

@abhinav-bellapu Update looks good, once the CI passes I will merge

@abhinav-bellapu

abhinav-bellapu commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

@jlarson4 CI looks good!

@jlarson4 jlarson4 merged commit 9f9fe17 into TransformerLensOrg:dev Jul 15, 2026
25 checks passed
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.

[Bug Report] Q cannot be reshaped correctly when model is loaded in 4bit

2 participants