Skip to content

Precision loss in MRoPE inv_freq during FP8 pipeline loading causes spatial artifacts #24

Description

@jrd-rocks

Disclosure: I used an AI assistant to help isolate and document this bug. The bug is very real, however, see the image comparisons at the bottom; before and after the fix in my toy code

Description

When loading an FP8 or NF4 checkpoint (e.g., ideogram-ai/ideogram-4-fp8 or ideogram-ai/ideogram-4-nf4) via Ideogram4Pipeline.from_pretrained(..., dtype=torch.bfloat16), the _build_transformer function downcasts the inv_freq buffer inside the Ideogram4MRoPE module from float32 to bfloat16.

Because bfloat16 only has 7 bits of mantissa precision, and the pipeline uses a large position offset for image tokens (IMAGE_POSITION_OFFSET = 10000), computing pos * inv_freq with a bfloat16 inverse frequency results in massive rounding errors. Neighboring spatial tokens end up receiving the exact same positional encodings, which completely corrupts the model's understanding of space and results in severe visual and spatial mismatch artifacts.

Note: I personally experienced and debugged this on the FP8 branch, but upon reviewing the code, it appears the exact same bug exists in the BNB 4-bit (NF4) loading path as well.

The Root Cause

In src/ideogram4/pipeline_ideogram4.py (and quantized_loading.py), the quantized loading branches execute .to(dtype) casts that inadvertently degrade the non-persistent inv_freq buffer.

For FP8:

  elif is_fp8_state_dict(state_dict):
    model.to(dtype) # <--- BUG: Downcasts the float32 inv_freq buffer to bfloat16
    swap_linears_to_fp8(model, state_dict, compute_dtype=dtype)
    load_fp8_state_dict(model, state_dict, device=device, dtype=dtype)

For BNB 4-bit (NF4):
In src/ideogram4/quantized_loading.py, load_bnb4bit_state_dict iterates through all buffers and downcasts them:

  for name, b in list(model.named_buffers()):
    if b.is_floating_point() and b.dtype != dtype:
      # ...
      parent.register_buffer(
        leaf,
        b.to(device=device, dtype=dtype), # <--- BUG: Downcasts the float32 inv_freq buffer to bfloat16
        persistent=leaf not in parent._non_persistent_buffers_set,
      )

Symptoms

Because spatial attention is broken, the model's understanding of position is highly degraded.

  1. The canvas appears disjointed, and the positions of rendered objects do not match the positions specified in bounding boxes or positional prompts (e.g., the canvas feels "bigger" than the expected positions).
  2. The overall generated images look visually off and distorted.

Proposed Fix

The fix is to simply re-calculate and overwrite inv_freq in float32 immediately after the .to(dtype) casts in src/ideogram4/pipeline_ideogram4.py.

def _build_transformer(
  transformer_config: "Ideogram4Config",
  state_dict: dict[str, torch.Tensor],
  device: torch.device,
  dtype: torch.dtype,
) -> "Ideogram4Transformer":
  model = Ideogram4Transformer(transformer_config)
  
  if is_bnb4bit_state_dict(state_dict):
    if device.type != "cuda":
      raise ValueError(f"bnb 4-bit weights require a CUDA device, got device={device}")
    swap_linears_to_bnb4bit(model, compute_dtype=dtype)
    load_bnb4bit_state_dict(model, state_dict, device=device, dtype=dtype)
    
    # FIX: Restore inv_freq precision lost during load_bnb4bit_state_dict
    inv_freq = 1.0 / (
        transformer_config.rope_theta ** (torch.arange(0, model.rotary_emb.head_dim, 2, dtype=torch.float32) / model.rotary_emb.head_dim)
    )
    model.rotary_emb.register_buffer("inv_freq", inv_freq, persistent=False)
    
  elif is_fp8_state_dict(state_dict):
    model.to(dtype)
    swap_linears_to_fp8(model, state_dict, compute_dtype=dtype)
    load_fp8_state_dict(model, state_dict, device=device, dtype=dtype)

    # FIX: Restore inv_freq precision lost during model.to(dtype)
    inv_freq = 1.0 / (
        transformer_config.rope_theta ** (torch.arange(0, model.rotary_emb.head_dim, 2, dtype=torch.float32) / model.rotary_emb.head_dim)
    )
    model.rotary_emb.register_buffer("inv_freq", inv_freq, persistent=False)
    
  else:
    model.load_state_dict(state_dict)
    model.to(device=device, dtype=dtype)
    
  model.eval()
  return model
Image Image

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