Skip to content

[Pytorch][Common] Hybrid quantization - #2817

Open
negvet wants to merge 79 commits into
NVIDIA:mainfrom
negvet:hybrid_quantization
Open

[Pytorch][Common] Hybrid quantization#2817
negvet wants to merge 79 commits into
NVIDIA:mainfrom
negvet:hybrid_quantization

Conversation

@negvet

@negvet negvet commented Mar 31, 2026

Copy link
Copy Markdown
Collaborator

Description

Hybrid (per-direction) quantization. Hybrid means rowwise/colwise can use different formats via CustomRecipe(qfactory).
This is an experimental feature.
The main problem that it tries to solve is that precision requirements are non-uniform.

Current recipes set one format for both rowwise and colwise directions.
Hybrid quantization enables, e.g. MXFP8 fwd and NVFP4 bwd (or vice versa) or any other valid combination. No need for a hardcoded recipe for every combination.

Composer-style (Composer 2 paper) grouped GEMM recipe, e.g. row-scaled NVFP4 fwd + MXFP8 bwd:

# CustomRecipe calls quantization_factory(role) for each quantized tensor
# Factory chooses formats

def hybrid_factory(role):
    is_grouped_linear = role is not None and role.module_type == "grouped_linear"
    is_linear = role is not None and role.module_type == "linear"

    if is_grouped_linear and role.tensor_type == "input":
        return HybridQuantizer(
            rowwise_quantizer=NVFP4Quantizer(row_scaled_nvfp4=True, ...),
            columnwise_quantizer=MXFP8Quantizer(...),
        )

    if is_grouped_linear and role.tensor_type == "weight":
        return HybridQuantizer(
            rowwise_quantizer=NVFP4Quantizer(...),
            columnwise_quantizer=MXFP8Quantizer(...),
        )

    if is_grouped_linear and role.tensor_type == "grad_output":
        return MXFP8Quantizer(...)

    if is_linear:
        return MXFP8Quantizer(...)

    return MXFP8Quantizer(...)

recipe = CustomRecipe(qfactory=hybrid_factory)
with autocast(recipe=recipe):
    y = model(x)

By default, the above factory uses columnwise_source="original", so MXFP8 backward operands are quantized from the original high-precision tensor. Use columnwise_source="rowwise_dequantized" when the backward operand should be derived from the dequantized rowwise NVFP4 forward value.

C++ optimizations (fusions, etc.) will come as standalone PRs. cc @kainzhong

TODO:

  • Convergence of base (non-hybrid) recipes
  • HybridFloat8BlockScaling is xfailed under FSDP2 because dim-0 shards can split 128-row block-scale tiles, producing all-gathered scale buffers whose shape does not match the global tensor.
  • Delayed scaling
  • Mid-training recipe change

Follow-up issue tracker #3158.

Integration

Ecosystem integration (all functional, unit-tested):

  • [Done] quantized_model_init
  • [Done] FSDP2 (TODO: optimize communication buffers)
  • [Done] CPU offloading
  • [Done] Activation recomputation
  • [Done] TP/SP (TODO: enable quantized AG)

Megatron-LM integration status:

  • [Done] 1 GPU baseline
  • [Done] DP + distributed optimizer
  • [TODO] quantized_model_init + --fp{4,8}-param-gather + dist opt (persistent low-precision params via quantized_model_init + sharded-master FP32 → quantized cast via quantize_master_weights.)
    - [Done] Per-tensor Float8 hybrid (delayed and/or current, any per-direction combination
    including same-format, cross-format Float8, single-direction)
    - [TODO] Per-block hybrid sub-quantizers (MXFP8, NVFP4, Float8Blockwise) — each rejected per-direction by quantize_master_weights; unblocker is TE-side cast-helper / kernel.
  • [TODO] Megatron-FSDP + --fp{4,8}-param-gather (fix private attribute access)
  • [TODO] Torch FSDP2 + --fp{4,8}-param-gather
    - [Done] TE-side hybrid FSDP2 path works end-to-end for Float8 / MXFP8 / Float8Blockwise sub-storages (TODO: need some minor MLM update)
    - [TODO] NVFP4 sub-storage FSDP2 hooks
  • [Done] Activation recompute
  • [Done] CPU offload
  • [Done] TP/SP/PP
  • [Done] MoE + EP + grouped GEMM (qwen3 MoE; _hybrid_split_quantize under Megatron MoE)

Review

Total diff +14000
New hybrid source (hybrid_tensor.py, hybrid_tensor_storage.py, identity_tensor.py, identity_tensor_storage.py) ~1800
Adjacent modifications ~1500
Tests are the rest (~10K)

Suggested reading order

  1. Foundation — 7553e6a: Python containers + quantize/gemm dispatch/unwrap
  • tensor/hybrid_tensor.py — HybridQuantizer + HybridQuantizedTensor
    -columnwise_source controls whether columnwise quantization uses the original input or the rowwise-dequantized value.
  • tensor/storage/hybrid_tensor_storage.py
  • cpp_extensions/gemm.py — _unwrap_hybrid_A/B
  • common/transpose/quantize_transpose_square_blockwise.cu - Block FP8 columnwise-only null-checks
  • Module hooks in module/{base,grouped_linear,layernorm_linear,layernorm_mlp}.py
  • Tests: TestHybridQuantizer*, TestHybridGemmBitwiseIdentical* (proves zero-overhead vs vanilla recipes when both formats match), TestHybridDirectionUnwrap*, TestHybridGroupedLinear*

1.1 Identity passthrough — b99277a

  • tensor/identity_tensor.py and tensor/storage/identity_tensor_storage.py — IdentityQuantizer / IdentityTensor high-precision passthrough
  • custom_recipes/quantization_factory_zoo.py — examples for high-precision fwd/bwd directions and columnwise_source="rowwise_dequantized"
  • Tests: test_identity_quantizer.py plus hybrid tests covering Identity inside HybridQuantizer
  1. quantized_model_init + FusedAdam — f80f5d0
  • hybrid_tensor.py::HybridQuantizer.update_quantized — delegates to each sub-quantizer; unblocks workspace-cache quantize_() and FusedAdam writeback
  • module/base.py workspace-cache invalidation
  • Tests: TestHybridQuantizedModelInit, TestHybridFusedAdam, TestHybridQuantizedParamsEndToEnd, TestHybridCheckpoint, TestQuantizedParamsEquivalence*
  1. FSDP2 support — 2185b30
  • New base FSDP2 buffer protocol on QuantizedTensorStorage: fsdp_buffer_fields / fsdp_extract_buffers / fsdp_assign_gathered. Generic, reusable beyond hybrid.
  • Per-format overrides on Float8TensorStorage (direction-aware) and MXFP8TensorStorage (trips/re-applies scale alignment padding around the gather)
  • hybrid_tensor.py::fsdp_pre/post_all_gather + torch_dispatch for the FSDP2 op set (view, split, as_strided, slice, copy_, new_zeros, clone, detach)
  • Non-safety in float8_tensor.py and mxfp8_tensor.py for single-direction sub-storages (columnwise-only on Hopper/L40)
  • Tests: TestHybridTorchDispatchFSDP2Ops, TestHybridFsdpPreAllGatherProtocol, TestHybridFsdpRoundtrip (bitwise-exact against manual all_gather(dequantize(shard))), plus tests/pytorch/distributed/fsdp2_tests/
  1. CPU offloading — 103fffe
  • hybrid_tensor_storage.py::clear() (v1 path) + prepare_for_saving / restore_from_saved chain (v2 path)
  • hybrid_tensor.py::detach() re-wraps each sub-storage via make_like (required by cpu_offload_v2's detach → prepare_for_saving pattern; sharing sub-storage objects would null-out fields on the original)
  • TestHybridCpuOffloadPushPop, plus updates to test_cpu_offloading*.py
  1. Activation recomputation — 16fb371
  • Uses existing QuantizedTensorStorage::prepare_for_saving / restore_from_saved protocol, preserving ordering across both sub-storages
  • Tests: 20 bitwise tests in TestHybridActivationRecompute
  1. TP/SP — a50fd63
  • hybrid_tensor.py::HybridQuantizer.supports_only_rowwise_all_gather — overrides to handle the NVFP4 columnwise-dequantize gap in the BF16 fallback path
  • distributed.py::gather_along_first_dim — hybrid branch re-quantizes with both directions after AG (since hybrid has no _create_transpose synthesis path)
  • Tests: 9 distributed tests in run_hybrid_tp_sp.py / test_hybrid_tp_sp.py
  1. Megatron-LM integration — a164cd3
  • tensor/utils.py::_route_hybrid_to_buckets — per-direction dispatch for quantize_master_weights: iterates both sub-storages, routes each independently into the per-format bucket matching its own sub-quantizer type
  • Hybrid branches in replace_raw_data and post_all_gather_processing
  • Today: per-tensor Float8 sub-quantizers (delayed + current) work in any per-direction combination. Per-block sub-quantizers raise per-direction with in-code TODOs naming the unblocker.
  • Tests: TestHybridQuantizeMasterWeights, TestHybridPostAllGatherProcessing

Type of change

  • Documentation change (change only to the documentation, either a fix or a new content)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Infra/Build change
  • Code refactoring

Changes

Please list the changes introduced in this PR:

  • Change A
  • Change B

Checklist:

  • I have read and followed the contributing guidelines
  • The functionality is complete
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

@greptile-apps

greptile-apps Bot commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces hybrid quantization for Transformer Engine, enabling weights to hold two independently-quantized representations (rowwise and columnwise) in different formats simultaneously — for example, FP8 rowwise paired with Identity (high-precision) columnwise, or FP8 rowwise paired with MXFP8 columnwise.

  • Core types: HybridQuantizer, HybridQuantizedTensor, and HybridQuantizedTensorStorage compose existing sub-quantizers by pinning each to a single direction via set_usage; IdentityQuantizer/IdentityTensor/IdentityTensorStorage provide a high-precision passthrough that integrates cleanly with the quantized-tensor machinery.
  • GEMM dispatch: _unwrap_tensor in gemm.py selects the correct sub-storage direction (rowwise for non-transposed, columnwise for transposed inputs) before forwarding to the native GEMM kernels.
  • FSDP2 and distopt: Float8TensorStorage gains direction-aware fsdp_buffer_fields/fsdp_extract_buffers/fsdp_assign_gathered for Hopper columnwise-only payloads; _route_hybrid_to_buckets and _validate_per_tensor_fp8_fsdp_hopper_policy extend distopt to handle hybrid sub-storages; per-block formats (MXFP8, NVFP4, Float8Block) raise NotImplementedError in distopt hybrid paths pending future work."

Confidence Score: 4/5

Safe to merge with one P1 to resolve: _unwrap_tensor's silent None return should raise explicitly before reaching the GEMM kernel.

The PR is a large, well-structured addition; FSDP2, distopt, and grouped-linear paths are correctly extended; known limitations are guarded with NotImplementedError. The one P1 (_unwrap_tensor None return) is low blast-radius since it only triggers on misconfigured hybrid tensors, not normal usage.

Files Needing Attention: transformer_engine/pytorch/cpp_extensions/gemm.py (_unwrap_tensor None-return path) and transformer_engine/pytorch/tensor/utils.py (_cast_master_weights_to_identity duplicated bounds logic).

Important Files Changed

Filename Overview
transformer_engine/pytorch/tensor/hybrid_tensor.py New HybridQuantizer and HybridQuantizedTensor; direction-pinning, FSDP2 pre/post all-gather, and torch_dispatch ops all look correct; fsdp_post_all_gather correctly calls _sync_usage after fsdp_assign_gathered.
transformer_engine/pytorch/tensor/storage/hybrid_tensor_storage.py New HybridQuantizedTensorStorage; delegation to sub-storages is correct; update_usage validates before mutating; view identity-check avoids spurious columnwise reshape.
transformer_engine/pytorch/tensor/identity_tensor.py New IdentityQuantizer and IdentityTensor; fsdp_pre_all_gather returns hp_data directly; torch_dispatch ops (view, split, slice, copy_, new_zeros) are implemented.
transformer_engine/pytorch/tensor/storage/identity_tensor_storage.py New IdentityTensorStorage; single _hp_data buffer correctly serves both directions; update_usage is a documented no-op; fsdp_buffer_fields returns _hp_data.
transformer_engine/pytorch/cpp_extensions/gemm.py _unwrap_tensor silently returns None when a HybridQuantizedTensorStorage sub-storage is absent for the requested direction, causing a downstream AttributeError instead of a clear diagnostic.
transformer_engine/pytorch/module/grouped_linear.py _split_quantize_hybrid and _validate_grouped_quantizer_list replace the old _has_hybrid_quantizer/_hybrid_split_quantize with validated, generation-cached logic; cat+re-split for columnwise_source=rowwise_dequantized materializes a transient full-precision copy of all expert weights.
transformer_engine/pytorch/tensor/utils.py _route_hybrid_to_buckets, _validate_per_tensor_fp8_fsdp_hopper_policy, and _update_transpose_only_float8_flat_fragment extend distopt for hybrid sub-storages; _cast_master_weights_to_identity duplicates bounds-checking logic from _validate_flat_fragment.
transformer_engine/pytorch/tensor/storage/float8_tensor_storage.py Direction-aware fsdp_buffer_fields, fsdp_extract_buffers (movedim transport), and fsdp_assign_gathered (movedim restore) for Hopper columnwise-only payloads are correct; _FromFloat8Func dequantization from _transpose restores row-major order.
transformer_engine/pytorch/tensor/storage/mxfp8_tensor_storage.py fsdp_extract_buffers now uses math.ceil for columnwise scale truncation, fixing the previous floor-division under-allocation bug.
transformer_engine/pytorch/module/base.py _is_weight_workspace_valid extended for HybridQuantizedTensorStorage direction drops; bgrad and amax-group guards correctly extended to cover HybridQuantizer and IdentityQuantizer.

Comments Outside Diff (1)

  1. transformer_engine/pytorch/cpp_extensions/gemm.py, line 1-30 (link)

    P1 Silent None return on missing sub-storage

    _unwrap_tensor returns None when a HybridQuantizedTensorStorage sub-storage is absent for the requested direction. The caller assigns the result directly to A or B and passes it to tex.te_gemm / tex.grouped_gemm, which will raise an AttributeError on the first attribute access rather than a clear error pointing back to the missing sub-storage. A missing sub-storage in this context is almost certainly a logic error (e.g. update_usage was not called before GEMM), so raising early here with the direction and tensor type makes debugging much faster.

Reviews (33): Last reviewed commit: "[pre-commit.ci] auto fixes from pre-comm..." | Re-trigger Greptile

Comment thread transformer_engine/pytorch/module/grouped_linear.py Outdated
Comment thread transformer_engine/pytorch/tensor/storage/hybrid_tensor_storage.py
Comment thread transformer_engine/pytorch/tensor/hybrid_tensor.py Outdated

@timmoon10 timmoon10 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall I think this moves us in a good direction. I see some minor bugs, as well as bugs reported by @greptile-apps.

Comment on lines +52 to +53
rowwise_result = self.rowwise_quantizer.quantize(tensor)
columnwise_result = self.columnwise_quantizer.quantize(tensor)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we handle the case where not all usages are needed? I'd expect something like:

Suggested change
rowwise_result = self.rowwise_quantizer.quantize(tensor)
columnwise_result = self.columnwise_quantizer.quantize(tensor)
rowwise_result = self.rowwise_quantizer.quantize(tensor) if self.rowwise_usage else None
columnwise_result = self.columnwise_quantizer.quantize(tensor) if self.columnwise_usage else None

@negvet negvet May 21, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 4858491

requires_grad: bool = False,
pin_memory: bool = False,
) -> HybridQuantizedTensor:
self.rowwise_quantizer.internal = True

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could we just set internal=True in the constructor? I don't think we ever need PyTorch tensor functionality in the per-usage data.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This would not work under FSDP2.

Comment thread transformer_engine/pytorch/tensor/hybrid_tensor.py Outdated
Comment on lines +1339 to +1355
def factory(role):
if role == "linear_weight":
return HybridQuantizer(
rowwise_quantizer=_make_fp8_quantizer(),
columnwise_quantizer=_make_mxfp8_quantizer(),
)
if role == "linear_input":
return HybridQuantizer(
rowwise_quantizer=_make_fp8_quantizer(),
columnwise_quantizer=_make_nvfp4_quantizer(),
)
if role in ("linear_grad_output", "linear_grad_input"):
return HybridQuantizer(
rowwise_quantizer=_make_mxfp8_quantizer(),
columnwise_quantizer=_make_nvfp4_quantizer(),
)
return None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is horrifying. Good test.

negvet and others added 10 commits April 6, 2026 10:26
Signed-off-by: Evgeny <etsykunov@nvidia.com>
Signed-off-by: Evgeny <etsykunov@nvidia.com>
Signed-off-by: Evgeny <etsykunov@nvidia.com>
Signed-off-by: Evgeny <etsykunov@nvidia.com>
Signed-off-by: Evgeny <etsykunov@nvidia.com>
Comment thread transformer_engine/pytorch/module/grouped_linear.py Outdated
Comment thread transformer_engine/pytorch/tensor/hybrid_tensor.py
negvet and others added 2 commits April 29, 2026 16:02
Signed-off-by: Evgeny <etsykunov@nvidia.com>
Comment thread transformer_engine/pytorch/tensor/storage/float8_tensor_storage.py
negvet added 3 commits May 13, 2026 12:34
Signed-off-by: Evgeny <etsykunov@nvidia.com>
Signed-off-by: Evgeny <etsykunov@nvidia.com>
Signed-off-by: Evgeny <etsykunov@nvidia.com>
@negvet
negvet requested a review from ksivaman as a code owner May 21, 2026 13:53
Comment thread transformer_engine/pytorch/tensor/float8_tensor.py
Comment on lines +27 to +30
# DCP serializes ``CustomRecipe`` via ``pickle``; closure-based qfactories
# (lambdas, inner functions referencing captured state) are not picklable,
# so the qfactory must live at module scope. See
# ``run_fsdp2_fused_adam.py::test_hybrid_dcp_output_parity``.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This comment is potentially useful, but I don't think it is in the right place - shouldn't it be closer to the actual implementation?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed

Comment on lines +1177 to +1184
for param in model.parameters():
state = optimizer.state[param]
assert state["exp_avg"].dtype == torch.float32
assert state["exp_avg_sq"].dtype == torch.float32
if "master_param" in state:
assert state["master_param"].dtype == torch.float32

assert losses[-1] < losses[0], f"Loss did not decrease: {losses[0]:.4f} -> {losses[-1]:.4f}"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That's not a very strict test, is there a way for us to do some numerical correctness comparisons?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Enabled check for the monotonic loss decrease (still mostly sanity), and also enabled hybrid vs vanilla bitwise recipe comparizon, see e.g. test_fused_adam_hybrid_vs_base_recipe_parity.

negvet added 2 commits July 17, 2026 17:59
Signed-off-by: Evgeny <etsykunov@nvidia.com>
Signed-off-by: Evgeny <etsykunov@nvidia.com>
@negvet

negvet commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

/te-ci pytorch L1

@negvet

negvet commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

/te-ci pytorch L1

negvet added 3 commits July 21, 2026 13:08
Signed-off-by: Evgeny <etsykunov@nvidia.com>
Signed-off-by: Evgeny <etsykunov@nvidia.com>
Signed-off-by: Evgeny <etsykunov@nvidia.com>

@vthumbe1503 vthumbe1503 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.

Have a few comments on the tests. Apart from that LGTM from FSDP2 side of things

Comment thread tests/pytorch/distributed/fsdp2_tests/run_fsdp2_model.py Outdated
Comment thread tests/pytorch/distributed/test_hybrid_tp_sp.py Outdated
Comment thread tests/pytorch/distributed/fsdp2_tests/fsdp2_utils.py Outdated
Signed-off-by: Evgeny <etsykunov@nvidia.com>
Comment thread transformer_engine/pytorch/tensor/utils.py
@negvet

negvet commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

/te-ci L0 L1

negvet and others added 3 commits July 22, 2026 12:32
Signed-off-by: Evgeny <etsykunov@nvidia.com>
Signed-off-by: Evgeny <etsykunov@nvidia.com>
@negvet

negvet commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

/te-ci L0 L1

Comment on lines +1788 to +1796
# Hybrid (CustomRecipe) needs no SP amax-reduction setup today: its SP
# activations are gathered in high precision and re-quantized whole, so
# every rank already sees the same global amax.
# TODO(#3158): once native quantized all-gather lands (see
# supports_only_rowwise_all_gather / gather_along_first_dim) the SP path
# quantizes per-shard, needing a hybrid branch here that mirrors the
# current-scaling / NVFP4 SP reduction above:
# elif recipe.custom():
# ... # enable SP amax reduction on the hybrid input/grad quantizer

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These comments are adding a lot of noise and will very easily become stale.

I get that this is a guide for future TP support. However, in my experience Claude is extremely timid about deleting code and will prefer to let comments become wrong than to fix them. If we do keep these comments, we should make sure future PRs remove them as intended.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I added this explicitly because it is easy to miss amax reduction when enabling low-precision comm, and this can be difficult to debug.

But I can see that this comment is stale since PR #3104. SP amax reduction is now configured at point of use through set_quantizer_amax_reduction_group(). HybridQuantizer already forwards with_amax_reduction and amax_reduction_group to its sub-quantizers. So no need for additional branches in set_meta_tensor(). So removed it completely from all modules.

Comment on lines +1767 to +1783
# Hybrid override: callers drop columnwise before AG, expecting to
# synthesize it post-AG via ``update_usage(columnwise_usage=True)``
# (native FP8's ``_create_transpose``). Hybrid has no synthesis path —
# that update_usage is a no-op — so re-quantize with both directions,
# mirroring what the planned native hybrid AG dispatch would produce
# (see the TODO in
# :meth:`HybridQuantizer.supports_only_rowwise_all_gather`); once
# native AG lands, hybrid won't reach this fallback.
if isinstance(quantizer, HybridQuantizer):
prev_row, prev_col = quantizer.rowwise_usage, quantizer.columnwise_usage
quantizer.set_usage(rowwise=True, columnwise=True)
try:
out = quantizer(out)
finally:
quantizer.set_usage(rowwise=prev_row, columnwise=prev_col)
else:
out = quantizer(out)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This override is unnecessary:

  • Native hybrid AG is not supported: The correct behavior is to AG in high precision and then call the quantizer directly.
  • Native hybrid AG is supported: The correct behavior is to add a custom AG implementation above, the same pattern as FP8/MXFP8/NVFP4.
Suggested change
# Hybrid override: callers drop columnwise before AG, expecting to
# synthesize it post-AG via ``update_usage(columnwise_usage=True)``
# (native FP8's ``_create_transpose``). Hybrid has no synthesis path —
# that update_usage is a no-op — so re-quantize with both directions,
# mirroring what the planned native hybrid AG dispatch would produce
# (see the TODO in
# :meth:`HybridQuantizer.supports_only_rowwise_all_gather`); once
# native AG lands, hybrid won't reach this fallback.
if isinstance(quantizer, HybridQuantizer):
prev_row, prev_col = quantizer.rowwise_usage, quantizer.columnwise_usage
quantizer.set_usage(rowwise=True, columnwise=True)
try:
out = quantizer(out)
finally:
quantizer.set_usage(rowwise=prev_row, columnwise=prev_col)
else:
out = quantizer(out)
out = quantizer(out)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I removed this HybridQuantizer special case, now high precision fallback calls the quantizer directly. This required updating the module wgrad AG calls to configure the required output explicitly (introduced set_quantizer_usage_for_wgrad_all_gather() in _common.py). Native hybrid AG is tracked in #3158.

Comment thread transformer_engine/pytorch/ops/basic/layer_norm.py Outdated
"""Returns True if the quantizer supports only rowwise all-gather"""
return False

def allows_save_original_input_for_backward(self) -> bool:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm very suspicious of this. We're spilling out internal logic from the linear module into the quantized tensor. Why should the quantizer be aware of how it happens to be used within the linear autograd function?

Thinking through this logically, the real thing we are checking is whether the quantizer is deterministic on repeated calls. A function name like is_stateless would capture the desired behavior while keeping the logic properly scoped.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

replaced with is_requantization_safe(). Repeated quantization must reproduce every requested representation. save_original_input decision for hybrid: columnwise_source="original" repeats neither sub-quantizer, so reconstruction is always valid, "rowwise_dequantized" repeats only the rowwise quantizer, so only that quantizer must be safe to recall.

Comment on lines +307 to +319
if (
save_original_input
and backward_needs_input
and input_quantizer is not None
and not input_quantizer.allows_save_original_input_for_backward()
):
warnings.warn(
"Ignoring save_original_input=True because the input quantizer requires "
"the forward quantized activation for backward "
f"({input_quantizer}).",
stacklevel=2,
)
save_original_input = False

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this necessary? If the user has specified save_original_input=True, then presumably they know what they're getting themselves in to. We document that it doesn't work with FP8 DS, so expanding that comment with other stateful quantizers seems sufficient:

Cannot work with FP8 DelayedScaling recipe.

As a stylistic nit, I also don't like how we split up the save_original_input logic into multiple places. If we do add a run-time check, it's more readable to put it here:

if backward_override == "high_precision":
save_original_input = True
elif backward_override == "dequantized":
save_original_input = False

@negvet negvet Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This runtime fallback is necessary because Megatron enables save_original_input automatically for proj, so this is not always an informed user opt-in. Moved Linear’s save_original_input policy below backward_override, and moved the reconstruction eligibility check into a shared module helper used by Linear and GroupedLinear.

Comment thread transformer_engine/pytorch/module/base.py Outdated
Comment thread transformer_engine/pytorch/cpp_extensions/gemm.py Outdated
Comment thread transformer_engine/pytorch/cpp_extensions/gemm.py Outdated
Comment thread transformer_engine/pytorch/module/grouped_linear.py Outdated
negvet and others added 3 commits July 24, 2026 13:59
Signed-off-by: Evgeny <etsykunov@nvidia.com>
Signed-off-by: Evgeny <etsykunov@nvidia.com>
@negvet

negvet commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

/te-ci L0 L1

@negvet

negvet commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

/te-ci L0 L1

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.

5 participants