Skip to content

GroupedLinear(single_grouped_weight=True) produces no weight gradient (wgrad) #3234

Description

@wujingyue

Description

With single_grouped_weight=True (fused single-parameter GroupedLinear, enabled via NVTE_GROUPED_LINEAR_SINGLE_PARAM=1), the backward pass runs and produces the input gradient (dgrad), but the weight gradient (wgrad) is never emitted anywhere:

  • fuse_wgrad_accumulation=Falseweight.grad stays None
  • fuse_wgrad_accumulation=True → a preallocated weight.main_grad is left all-zero (never written)

This happens in both bf16 and fp32. The per-expert path (single_grouped_weight=False) emits wgrad normally, which isolates the failure to the fused single-group weight. Because the fused weight receives no gradient, it cannot be trained (the optimizer sees no update) nor sharded/reduced by a data-parallel wrapper.

Environment

  • TransformerEngine: 2.17.0+2e559f06
  • PyTorch: 2.13.0a0+8145d630e8.nv26.06
  • Container: nvcr.io/nvidia/pytorch:26.06-py3
  • GPU: NVIDIA H100

Minimal reproduction

import os
os.environ["NVTE_GROUPED_LINEAR_SINGLE_PARAM"] = "1"
import torch
import transformer_engine.pytorch as te

layer = te.GroupedLinear(
    num_gemms=2, in_features=16, out_features=32, bias=False,
    single_grouped_weight=True, params_dtype=torch.bfloat16, device="cuda",
)
x = torch.randn(8, 16, device="cuda", dtype=torch.bfloat16, requires_grad=True)
layer(x, [4, 4]).sum().backward()

print("input dgrad present:", x.grad is not None)   # True  -> backward ran
print("weight wgrad:", layer.weight.grad)            # None  -> BUG (expected a gradient)

Expected behavior

layer.weight.grad is populated after backward() (as with nn.Linear and with single_grouped_weight=False); or, under fuse_wgrad_accumulation=True, the wgrad is accumulated into weight.main_grad.

Actual behavior

The backward runs (input dgrad is always produced), but the fused weight receives no gradient. Full matrix:

single_grouped_weight fuse_wgrad_accumulation dtype dgrad (x.grad) wgrad
False False bf16 set weight0/1.grad set
False True bf16 set ✅ present
True False bf16 set no gradient anywhere
True True bf16 set main_grad all-zero
False False fp32 set ✅ set
False True fp32 set ✅ present
True False fp32 set no gradient anywhere
True True fp32 set main_grad all-zero
Self-contained script covering all eight cases
import os
os.environ.setdefault("NVTE_GROUPED_LINEAR_SINGLE_PARAM", "1")

import torch
import transformer_engine
import transformer_engine.pytorch as te

NUM_GEMMS, IN_FEATURES, OUT_FEATURES, TOKENS_PER_EXPERT = 2, 16, 32, 4


def weight_grad_state(weight):
    if weight.grad is not None:
        return f".grad set {tuple(weight.grad.shape)}"
    main_grad = getattr(weight, "main_grad", None)
    if main_grad is not None:
        return f".main_grad {'nonzero' if bool(main_grad.abs().sum() > 0) else 'ALL-ZERO (never written)'}"
    return "NO GRAD ANYWHERE"


def run(single_grouped_weight, fuse_wgrad_accumulation, dtype):
    torch.manual_seed(0)
    layer = te.GroupedLinear(
        num_gemms=NUM_GEMMS, in_features=IN_FEATURES, out_features=OUT_FEATURES, bias=False,
        single_grouped_weight=single_grouped_weight, params_dtype=dtype, device="cuda",
        fuse_wgrad_accumulation=fuse_wgrad_accumulation,
    )
    weights = [layer.weight] if single_grouped_weight else [layer.weight0, layer.weight1]
    if fuse_wgrad_accumulation:
        for weight in weights:
            weight.main_grad = torch.zeros(weight.shape, device="cuda", dtype=torch.float32)
            weight.grad_added_to_main_grad = False
    x = torch.randn(NUM_GEMMS * TOKENS_PER_EXPERT, IN_FEATURES, device="cuda", dtype=dtype, requires_grad=True)
    layer(x, [TOKENS_PER_EXPERT] * NUM_GEMMS).sum().backward()
    states = ", ".join(weight_grad_state(w) for w in weights)
    print(f"single_grouped_weight={single_grouped_weight!s:5}  fuse_wgrad={fuse_wgrad_accumulation!s:5}  "
          f"dtype={str(dtype).split('.')[-1]:8} | dgrad={'set' if x.grad is not None else 'None'} | wgrad -> [{states}]")


print(f"TransformerEngine {transformer_engine.__version__}, torch {torch.__version__}\n")
for dtype in (torch.bfloat16, torch.float32):
    for sgw in (False, True):
        for fuse in (False, True):
            run(sgw, fuse, dtype)
    print()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions