CRITICAL FIX: FP32 RoPE cache + FP32 arithmetic for inverse RoPE round-trip
BF16 cos/sin cache destroys cos²+sin²=1 identity (can be 0.996 in BF16). This causes ~3% error per RoPE→inverse RoPE round-trip, accumulating across 61 layers into garbage output. FP32 cache + FP32 arithmetic gives exact round-trip (diff < 1e-7). Also fixes: MoE expert loop indentation (was only running last expert).
This commit is contained in:
@@ -167,37 +167,44 @@ class mHCBlock:
|
||||
|
||||
def build_rope_cache(max_pos, rope_dim, device, theta=10000.0):
|
||||
"""Build cos/sin caches for partial RoPE.
|
||||
Returns: (cos_cache, sin_cache) each (max_pos, rope_dim//2) BF16
|
||||
|
||||
CRITICAL: FP32, not BF16! BF16 quantization destroys cos²+sin²=1
|
||||
identity needed for inverse RoPE. BF16 cos²+sin² can be 0.996,
|
||||
causing ~3% round-trip error that accumulates across 61 layers.
|
||||
|
||||
Returns: (cos_cache, sin_cache) each (max_pos, rope_dim//2) FP32
|
||||
"""
|
||||
half = rope_dim // 2
|
||||
freqs = 1.0 / (theta ** (torch.arange(0, rope_dim, 2, dtype=torch.float32) / rope_dim))
|
||||
angles = torch.outer(torch.arange(max_pos, dtype=torch.float32), freqs)
|
||||
return torch.cos(angles).bfloat16().to(device), torch.sin(angles).bfloat16().to(device)
|
||||
return torch.cos(angles).to(device), torch.sin(angles).to(device)
|
||||
|
||||
|
||||
def apply_rope_partial(x, positions, cos_cache, sin_cache, head_dim, rope_dim):
|
||||
"""Apply partial GPT-J interleaved RoPE to the last rope_dim dims of each head."""
|
||||
"""Apply partial GPT-J interleaved RoPE to the last rope_dim dims of each head.
|
||||
Computes in FP32 for numerical stability (inverse RoPE requires cos²+sin²=1)."""
|
||||
T, n_h, hd = x.shape
|
||||
nope = hd - rope_dim
|
||||
cos = cos_cache[positions].unsqueeze(1) # (T, 1, half) BF16
|
||||
cos = cos_cache[positions].unsqueeze(1) # (T, 1, half) FP32
|
||||
sin = sin_cache[positions].unsqueeze(1)
|
||||
out = x.clone()
|
||||
x_rope = x[:, :, nope:]
|
||||
out[:, :, nope:][..., 0::2] = x_rope[..., 0::2] * cos - x_rope[..., 1::2] * sin
|
||||
out[:, :, nope:][..., 1::2] = x_rope[..., 0::2] * sin + x_rope[..., 1::2] * cos
|
||||
x_rope = x[:, :, nope:].float() # FP32 for accurate rotation
|
||||
out[:, :, nope:] = (x_rope[..., 0::2] * cos - x_rope[..., 1::2] * sin).to(torch.bfloat16)
|
||||
out[:, :, nope:][..., 1::2] = (x_rope[..., 0::2] * sin + x_rope[..., 1::2] * cos).to(torch.bfloat16)
|
||||
return out
|
||||
|
||||
|
||||
def apply_inverse_rope(o, positions, cos_cache, sin_cache, head_dim, rope_dim):
|
||||
"""Apply inverse RoPE (conjugate rotation) to attention output."""
|
||||
"""Apply inverse RoPE (conjugate rotation) to attention output.
|
||||
Computes in FP32 for numerical stability."""
|
||||
T, n_h, hd = o.shape
|
||||
nope = hd - rope_dim
|
||||
cos = cos_cache[positions].unsqueeze(1)
|
||||
sin = sin_cache[positions].unsqueeze(1)
|
||||
out = o.clone()
|
||||
o_rope = o[:, :, nope:]
|
||||
out[:, :, nope:][..., 0::2] = o_rope[..., 0::2] * cos + o_rope[..., 1::2] * sin
|
||||
out[:, :, nope:][..., 1::2] = -o_rope[..., 0::2] * sin + o_rope[..., 1::2] * cos
|
||||
o_rope = o[:, :, nope:].float()
|
||||
out[:, :, nope:] = (o_rope[..., 0::2] * cos + o_rope[..., 1::2] * sin).to(torch.bfloat16)
|
||||
out[:, :, nope:][..., 1::2] = (-o_rope[..., 0::2] * sin + o_rope[..., 1::2] * cos).to(torch.bfloat16)
|
||||
return out
|
||||
|
||||
class SimpleKVCache:
|
||||
|
||||
@@ -54,31 +54,41 @@ class RMSNorm:
|
||||
return (x_f * rms * self.weight).to(torch.bfloat16)
|
||||
|
||||
def build_rope_cache(max_pos, rope_dim, device, theta=10000.0):
|
||||
"""Build FP32 cos/sin caches for RoPE.
|
||||
|
||||
CRITICAL: Must be FP32, not BF16! BF16 quantization destroys
|
||||
cos²+sin²=1 identity needed for inverse RoPE round-trip.
|
||||
BF16 cos²+sin² can be as low as 0.996, causing ~3% error.
|
||||
"""
|
||||
half = rope_dim // 2
|
||||
freqs = 1.0 / (theta ** (torch.arange(0, rope_dim, 2, dtype=torch.float32) / rope_dim))
|
||||
angles = torch.outer(torch.arange(max_pos, dtype=torch.float32), freqs)
|
||||
return torch.cos(angles).bfloat16().to(device), torch.sin(angles).bfloat16().to(device)
|
||||
return torch.cos(angles).to(device), torch.sin(angles).to(device)
|
||||
|
||||
def apply_rope_partial(x, positions, cos_cache, sin_cache, head_dim, rope_dim):
|
||||
"""Apply partial GPT-J interleaved RoPE. Computes in FP32 for accuracy."""
|
||||
T, n_h, hd = x.shape
|
||||
nope = hd - rope_dim
|
||||
cos = cos_cache[positions].unsqueeze(1)
|
||||
cos = cos_cache[positions].unsqueeze(1) # (T, 1, half) FP32
|
||||
sin = sin_cache[positions].unsqueeze(1)
|
||||
out = x.clone()
|
||||
x_rope = x[:, :, nope:]
|
||||
out[:, :, nope:][..., 0::2] = x_rope[..., 0::2] * cos - x_rope[..., 1::2] * sin
|
||||
out[:, :, nope:][..., 1::2] = x_rope[..., 0::2] * sin + x_rope[..., 1::2] * cos
|
||||
# Compute in FP32 for numerical stability
|
||||
x_rope = x[:, :, nope:].float()
|
||||
out[:, :, nope:] = (x_rope[..., 0::2] * cos - x_rope[..., 1::2] * sin).to(torch.bfloat16)
|
||||
# Second pass for odd elements (need original even values)
|
||||
out[:, :, nope:][..., 1::2] = (x_rope[..., 0::2] * sin + x_rope[..., 1::2] * cos).to(torch.bfloat16)
|
||||
return out
|
||||
|
||||
def apply_inverse_rope(o, positions, cos_cache, sin_cache, head_dim, rope_dim):
|
||||
"""Apply inverse RoPE (conjugate rotation). Computes in FP32 for accuracy."""
|
||||
T, n_h, hd = o.shape
|
||||
nope = hd - rope_dim
|
||||
cos = cos_cache[positions].unsqueeze(1)
|
||||
sin = sin_cache[positions].unsqueeze(1)
|
||||
out = o.clone()
|
||||
o_rope = o[:, :, nope:]
|
||||
out[:, :, nope:][..., 0::2] = o_rope[..., 0::2] * cos + o_rope[..., 1::2] * sin
|
||||
out[:, :, nope:][..., 1::2] = -o_rope[..., 0::2] * sin + o_rope[..., 1::2] * cos
|
||||
o_rope = o[:, :, nope:].float()
|
||||
out[:, :, nope:] = (o_rope[..., 0::2] * cos + o_rope[..., 1::2] * sin).to(torch.bfloat16)
|
||||
out[:, :, nope:][..., 1::2] = (-o_rope[..., 0::2] * sin + o_rope[..., 1::2] * cos).to(torch.bfloat16)
|
||||
return out
|
||||
|
||||
def load_weights_to_cpu(checkpoint_dir):
|
||||
@@ -182,6 +192,7 @@ def test_layer0():
|
||||
|
||||
# Build mHC + norms for layer 0
|
||||
li = 0
|
||||
from single_shot_inference import mHCBlock
|
||||
attn_mhc = mHCBlock(hidden_dim=H, n_hc=n_hc, device=device)
|
||||
attn_mhc.load_from_checkpoint(
|
||||
all_weights[f"model.layers.{li}.attn_hc.fn"],
|
||||
|
||||
Reference in New Issue
Block a user