Skip to content

Commit 8f7c007

Browse files
justinchubyCopilot
andauthored
Fix DynamicNTKRope: use alpha parameter for HunyuanV1 scaling (#293)
## Problem HunyuanV1 models (tencent/HY-MT1.5-1.8B) produce significant logit divergence vs HuggingFace (max_diff=1.637, cosine=0.9962) due to incorrect RoPE frequencies. ## Root Cause HunyuanV1 uses `rope_scaling['alpha']=1000.0` instead of `rope_scaling['factor']=1.0` for the NTK-aware base theta scaling. With `factor=1.0`, the old `DynamicNTKRope` computed `theta * 1.0 = theta` (no scaling), while HF computes `theta * 1000^(dim/(dim-2))`. Max inv_freq difference: 0.206 — divergence grows with position (Pos 0: 0.00004, Pos 4: 1.68). ## Fix 1-line change: `DynamicNTKRope` now uses `rope_scaling.get('alpha') or rope_scaling['factor']` — prefers `alpha` when present. Same formula, different config key. ## Results | Metric | Before | After | |--------|--------|-------| | Max diff | 1.637 | **0.094** | | Mean diff | 0.240 | **0.016** | | Cosine sim | 0.9962 | **0.999982** | | Top-1 token | Paris ✅ | Paris ✅ | All 1205 tests pass. --------- Signed-off-by: Justin Chu <justinchu@microsoft.com> Signed-off-by: Justin Chu <justinchuby@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 088c981 commit 8f7c007

2 files changed

Lines changed: 108 additions & 2 deletions

File tree

src/mobius/components/_rotary_embedding.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,13 +218,19 @@ class DynamicNTKRope(BaseRope):
218218
This spreads frequencies more evenly across the extended context,
219219
preserving the model's positional inductive bias better than linear
220220
scaling for long-context extrapolation.
221+
222+
HunyuanV1 uses ``alpha`` instead of ``factor`` for the scaling
223+
exponent (same formula, different config key). When ``alpha`` is
224+
present in ``rope_scaling``, it takes precedence over ``factor``.
221225
"""
222226

223227
def __init__(self, config: ArchitectureConfig):
224228
dim = int(config.head_dim * config.partial_rotary_factor)
225-
factor = config.rope_scaling["factor"]
229+
# HunyuanV1 uses "alpha" as the scaling factor; standard
230+
# dynamic NTK uses "factor". Prefer alpha when present.
231+
scaling = config.rope_scaling.get("alpha") or config.rope_scaling["factor"]
226232
# NTK-aware base scaling
227-
new_theta = config.rope_theta * (factor ** (dim / (dim - 2)))
233+
new_theta = config.rope_theta * (scaling ** (dim / (dim - 2)))
228234
inv_freq = 1.0 / (new_theta ** (np.arange(0, dim, 2, dtype=np.float32) / dim))
229235
cos_cache, sin_cache = _get_cos_sin_cache(config.max_position_embeddings, inv_freq)
230236
super().__init__(cos_cache, sin_cache, dtype=config.dtype)

src/mobius/components/_rotary_embedding_test.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from mobius.components._rotary_embedding import (
1313
ChunkedMRope,
1414
DefaultRope,
15+
DynamicNTKRope,
1516
InterleavedMRope,
1617
LinearRope,
1718
Llama3Rope,
@@ -127,6 +128,92 @@ def test_long_rope_with_long_cache(self):
127128
assert rope.has_long_cache
128129
assert next(iter(rope.cos_cache.shape)) == 96
129130

131+
def test_dynamic_ntk_rope_with_factor(self):
132+
"""DynamicNTKRope with factor (standard, no alpha) applies NTK scaling."""
133+
config = make_config(
134+
rope_type="dynamic",
135+
rope_scaling={"factor": 4.0},
136+
)
137+
rope = DynamicNTKRope(config)
138+
assert next(iter(rope.cos_cache.shape)) == config.max_position_embeddings
139+
140+
# Verify NTK scaling: new_theta = theta * factor^(dim/(dim-2))
141+
dim = config.head_dim
142+
expected_theta = config.rope_theta * (4.0 ** (dim / (dim - 2)))
143+
expected_inv = 1.0 / (expected_theta ** (np.arange(0, dim, 2, dtype=np.float32) / dim))
144+
expected_cos1 = np.cos(expected_inv)
145+
actual_cos1 = rope.cos_cache.const_value.numpy()[1, : dim // 2]
146+
np.testing.assert_allclose(actual_cos1, expected_cos1, atol=1e-5)
147+
148+
def test_dynamic_ntk_rope_with_alpha(self):
149+
"""DynamicNTKRope with alpha (HunyuanV1) uses alpha instead of factor."""
150+
config = make_config(
151+
rope_type="dynamic",
152+
rope_scaling={"factor": 1.0, "alpha": 1000.0},
153+
)
154+
rope = DynamicNTKRope(config)
155+
156+
# With alpha=1000, scaling should use 1000 not factor=1.0
157+
dim = config.head_dim
158+
expected_theta = config.rope_theta * (1000.0 ** (dim / (dim - 2)))
159+
expected_inv = 1.0 / (expected_theta ** (np.arange(0, dim, 2, dtype=np.float32) / dim))
160+
expected_cos1 = np.cos(expected_inv)
161+
actual_cos1 = rope.cos_cache.const_value.numpy()[1, : dim // 2]
162+
np.testing.assert_allclose(actual_cos1, expected_cos1, atol=1e-5)
163+
164+
# Verify it differs from factor=1.0 (no scaling)
165+
default_inv = 1.0 / (
166+
config.rope_theta ** (np.arange(0, dim, 2, dtype=np.float32) / dim)
167+
)
168+
default_cos1 = np.cos(default_inv)
169+
assert not np.allclose(actual_cos1, default_cos1, atol=1e-3), (
170+
"alpha=1000 should produce different frequencies than default"
171+
)
172+
173+
def test_dynamic_ntk_rope_alpha_matches_hunyuan_hf(self):
174+
"""DynamicNTKRope with HunyuanV1 config matches HF inv_freq exactly."""
175+
# HunyuanV1 HF formula: base = theta * alpha^(dim/(dim-2))
176+
# inv_freq = 1.0 / (base ** (arange(0, dim, 2) / dim))
177+
config = make_config(
178+
rope_theta=10000.0,
179+
head_dim=128,
180+
rope_type="dynamic",
181+
rope_scaling={
182+
"factor": 1.0,
183+
"alpha": 1000.0,
184+
"beta_fast": 32,
185+
"beta_slow": 1,
186+
"mscale": 1.0,
187+
"mscale_all_dim": 1.0,
188+
},
189+
)
190+
rope = DynamicNTKRope(config)
191+
192+
# Reference: HF HunYuanDenseV1RotaryEmbedding
193+
dim = 128
194+
base = 10000.0 * 1000.0 ** (dim / (dim - 2))
195+
hf_inv_freq = 1.0 / (base ** (np.arange(0, dim, 2, dtype=np.float32) / dim))
196+
197+
# Compare cos at position 1 (cos(1 * inv_freq))
198+
hf_cos1 = np.cos(hf_inv_freq)
199+
actual_cos1 = rope.cos_cache.const_value.numpy()[1, : dim // 2]
200+
np.testing.assert_allclose(actual_cos1, hf_cos1, atol=1e-5)
201+
202+
def test_dynamic_ntk_rope_factor_only_backward_compatible(self):
203+
"""DynamicNTKRope without alpha still works with factor alone."""
204+
config = make_config(
205+
rope_type="dynamic",
206+
rope_scaling={"factor": 2.0},
207+
)
208+
rope = DynamicNTKRope(config)
209+
210+
dim = config.head_dim
211+
expected_theta = config.rope_theta * (2.0 ** (dim / (dim - 2)))
212+
expected_inv = 1.0 / (expected_theta ** (np.arange(0, dim, 2, dtype=np.float32) / dim))
213+
expected_cos1 = np.cos(expected_inv)
214+
actual_cos1 = rope.cos_cache.const_value.numpy()[1, : dim // 2]
215+
np.testing.assert_allclose(actual_cos1, expected_cos1, atol=1e-5)
216+
130217

131218
class TestInitializeRope:
132219
def test_default(self):
@@ -156,6 +243,19 @@ def test_longrope(self):
156243
rope = initialize_rope(config)
157244
assert isinstance(rope, LongRope)
158245

246+
def test_dynamic(self):
247+
config = make_config(rope_type="dynamic", rope_scaling={"factor": 2.0})
248+
rope = initialize_rope(config)
249+
assert isinstance(rope, DynamicNTKRope)
250+
251+
def test_dynamic_with_alpha(self):
252+
config = make_config(
253+
rope_type="dynamic",
254+
rope_scaling={"factor": 1.0, "alpha": 1000.0},
255+
)
256+
rope = initialize_rope(config)
257+
assert isinstance(rope, DynamicNTKRope)
258+
159259
def test_unsupported_raises(self):
160260
config = make_config(rope_type="unknown")
161261
with pytest.raises(ValueError, match="Unsupported rope type"):

0 commit comments

Comments
 (0)