Skip to content

Nemotron-H MoE: expert NVFP4 scale2 tensors not loaded — repetitive garbage output #85

Description

@poad42

Nemotron-H MoE: expert NVFP4 scale2 tensors not loaded — repetitive garbage output

Problem

When loading a ModelOpt NVFP4 GGUF for NemotronHForCausalLM (the MoE variant, LLM_ARCH_NEMOTRON_H_MOE), the model generates repetitive garbage:

Prompt: "The capital of France is"
Output: "the capital of the United States.. The capital of the United States is the capital..."

The root cause is that nemotron-h.cpp's load_arch_tensors() creates the expert weight tensors (ffn_down_exps, ffn_up_exps, ffn_down_shexp, ffn_up_shexp) but never creates the corresponding scale tensors (ffn_up_exps_s, ffn_down_exps_s, ffn_up_shexp_s, ffn_down_shexp_s).

The graph code at build_ffn_layer() correctly passes these to build_moe_ffn() and build_ffn():

build_moe_ffn(inp_latent,
    model.layers[il].ffn_gate_inp,
    model.layers[il].ffn_up_exps,
    ...
    model.layers[il].ffn_down_exps,
    ...
    model.layers[il].ffn_up_exps_s,    // <-- NULL (never created!)
    ...
    model.layers[il].ffn_down_exps_s); // <-- NULL (never created!)

Since the pointers are NULL, build_moe_ffn and build_ffn skip the ggml_mul(res, w_s) step, leaving the NVFP4 expert outputs unscaled by the per-tensor scale2 factor. This produces wrong logits and the model degrades to repetitive output.

Model

  • nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4 (ModelOpt W4A16_NVFP4, 128 routed experts + shared expert)
  • Converted via convert_hf_to_gguf.py with PR NVFP4: native load + quantize support, and fix vision with draft-mtp #70 (NVFP4 support) + fixes for W4A16_NVFP4 detection
  • GGUF has the scale tensors written (blk.N.ffn_up_exps.scale, blk.N.ffn_down_exps.scale, etc.)
  • Server loads model without errors but scale tensors are silently missing (NULL)

Fix

Add create_tensor calls for the expert/shared-expert scale tensors in load_arch_tensors() in src/models/nemotron-h.cpp:

// After ffn_up_exps / ffn_down_exps creation:
layer.ffn_up_exps_s   = create_tensor(tn(LLM_TENSOR_FFN_UP_EXPS,   "scale", i), {1}, TENSOR_NOT_REQUIRED);
layer.ffn_down_exps_s = create_tensor(tn(LLM_TENSOR_FFN_DOWN_EXPS, "scale", i), {1}, TENSOR_NOT_REQUIRED);

// After ffn_up_shexp / ffn_down_shexp creation:
layer.ffn_up_shexp_s   = create_tensor(tn(LLM_TENSOR_FFN_UP_SHEXP,   "scale", i), {1}, TENSOR_NOT_REQUIRED);
layer.ffn_down_shexp_s = create_tensor(tn(LLM_TENSOR_FFN_DOWN_SHEXP, "scale", i), {1}, TENSOR_NOT_REQUIRED);

Workaround (converter-side)

I worked around this by baking the scale2 into the E4M3 per-sub-block scales during conversion:

  1. Convert each UE4M3 scale byte → float32
  2. Multiply by scale2 (the global per-tensor scale)
  3. Convert back to nearest UE4M3 byte
  4. Write scale2=1.0 as the separate scale tensor (so the server's ggml_mul is a no-op)

With this workaround, the model generates correctly:

Prompt: "The capital of France is"
Output: " Paris.  \nThe capital of Germany is Berlin"

Environment

Additional converter fixes needed for Nemotron-H NVFP4

Two more bugs in convert_hf_to_gguf.py (already fixed locally):

  1. NVFP4 detection: The code checks quant_algo == "NVFP4" but ModelOpt uses "W4A16_NVFP4" per-layer. Fixed with substring match: "NVFP4" in (v.get("quant_algo") or "").

  2. FP8/NVFP4 routing in modelopt handler: The dequant_simple lambda was applied to ALL .weight_scale tensors, including NVFP4 expert tensors (uint8 weight with 2D E4M3 scale). This causes a broadcast error ([2688, 116] vs [2688, 928]). Fixed by checking w().dtype and skipping non-FP8 (uint8) weights.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions