Skip to content

Commit 51c197c

Browse files
authored
Unify CosSinRoPE and ComplexRoPE YaRN computation (#3787)
### Background `ComplexRoPE` and `CosSinRoPE` are the two RoPE variants in torchtitan: `ComplexRoPE` uses an interleaved complex cache (DeepSeek V3, LLaMA 3, Qwen3.5); `CosSinRoPE` uses a concatenated cos/sin cache with `rotate_half` application (GPT-OSS, Qwen3). Both implement YaRN ("NTK-by-parts") for context-length extension, but they drifted apart in following ways: 1. **`beta_fast`/`beta_slow` assignment** — `CosSinRoPE` mapped `low ← beta_slow`, `high ← beta_fast`, **the inverse of** the YaRN paper convention and of `ComplexRoPE`. GPT-OSS's config compensated with swapped values, hiding the inconsistency from users. 2. **Embedded rope `mscale`** — `CosSinRoPE` multiplied the YaRN attention-magnitude factor (`0.1·log(factor)+1`) into its cos/sin cache, coupling the rope to the attention. GPT-OSS needed this; other `rotate_half` consumers may not. The rope should be a pure rotation, with mscale applied where each model's attention expects it. This PR makes the two classes share one YaRN implementation, agreeing exactly when both enter their respective YaRN (or non-YaRN) branches. ### Commits **1. Align `CosSinRoPE` `beta_fast`/`beta_slow` to the YaRN paper/HF convention** Swap `low ← beta_slow` to `low ← beta_fast` (extrapolation), `high ← beta_fast` to `high ← beta_slow` (interpolation). Un-swap GPT-OSS config to the official `beta_fast=32.0, beta_slow=1.0`. Numerically neutral (the two un-swaps cancel). **2. Extract a shared `_yarn_inv_freq` helper + `truncate` flag** A single source-of-truth function called by both classes. Add a `truncate` config flag (floor/ceil for DeepSeek; fractional for GPT-OSS, which uses `truncate=False`). Always clamps to `[0, dim-1]`. **3. Make `CosSinRoPE` a pure rotation; let GPT-OSS handle mscale in its attention** Remove `mscale` from `CosSinRoPE._precompute_cache` (and the unused `mscale` field from `RoPE.Config`). GPT-OSS folds `mscale²` into its `softmax_scale` — mathematically equivalent. The rope is now modular: each model's attention owns its own mscale. ### References - YaRN paper: [arXiv:2309.00071](https://arxiv.org/abs/2309.00071) (§3.2 NTK-by-parts, §3.3 attention temperature) - DeepSeek-V3.2: [`precompute_freqs_cis`](https://huggingface.co/deepseek-ai/DeepSeek-V3.2/blob/c69397e/inference/model.py#L324-L402) — `low = floor(fcd(beta_fast))`, `high = ceil(fcd(beta_slow))` - GPT-OSS official: [`ntk_alpha=1, ntk_beta=32`](https://github.com/openai/gpt-oss/blob/d21f34e/gpt_oss/torch/model.py#L28-L29); [`low ← ntk_beta` in rope](https://github.com/openai/gpt-oss/blob/d21f34e/gpt_oss/torch/model.py#L85-L131)
1 parent 0d7f4ab commit 51c197c

3 files changed

Lines changed: 79 additions & 78 deletions

File tree

torchtitan/models/common/rope.py

Lines changed: 65 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,49 @@ def _maybe_check_max_pos(positions: torch.Tensor, *, max_valid_pos: int) -> None
3939
)
4040

4141

42+
def _yarn_inv_freq(
43+
dim: int,
44+
base: float,
45+
rope_factor: float,
46+
beta_fast: float,
47+
beta_slow: float,
48+
original_seq_len: int,
49+
truncate: bool,
50+
) -> torch.Tensor:
51+
"""Shared YaRN ("NTK-by-parts") inverse-frequency computation.
52+
53+
Single source of truth for both ``ComplexRoPE`` and ``CosSinRoPE`` so the
54+
two cache formats are guaranteed to agree. Follows the YaRN paper / HF
55+
convention: ``low <- beta_fast`` (extrapolation boundary), ``high <-
56+
beta_slow`` (interpolation boundary). ``truncate`` floors/ceils the cutoffs
57+
(DeepSeek style); ``truncate=False`` keeps fractional cutoffs (gpt-oss
58+
style). The range is always clamped to ``[0, dim/2 - 1]``. The YaRN
59+
attention "mscale" is intentionally NOT applied here -- the rope stays a
60+
pure rotation and the model folds mscale into its softmax scale.
61+
"""
62+
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim))
63+
64+
def find_correction_dim(num_rotations: float) -> float:
65+
return (dim * math.log(original_seq_len / (num_rotations * 2 * math.pi))) / (
66+
2 * math.log(base)
67+
)
68+
69+
low = find_correction_dim(beta_fast)
70+
high = find_correction_dim(beta_slow)
71+
if truncate:
72+
low = math.floor(low)
73+
high = math.ceil(high)
74+
low, high = max(low, 0), min(high, dim - 1)
75+
assert (
76+
0 < low < high < dim - 1
77+
), f"Invalid YaRN params: 0 < {low} < {high} < {dim - 1}"
78+
79+
ramp = ((torch.arange(dim // 2, dtype=torch.float32) - low) / (high - low)).clamp(
80+
0, 1
81+
)
82+
return inv_freq / rope_factor * ramp + inv_freq * (1 - ramp)
83+
84+
4285
class RoPE(Module):
4386
"""Shared Rotary Position Embedding module.
4487
@@ -63,7 +106,7 @@ class Config(Module.Config):
63106
beta_fast: float = 32.0
64107
beta_slow: float = 1.0
65108
original_seq_len: int = 4096
66-
mscale: float = 0.0
109+
truncate: bool = True
67110

68111
def __init__(self, config: Config):
69112
super().__init__()
@@ -175,47 +218,15 @@ def _precompute_cache(self) -> torch.Tensor:
175218
freqs = torch.where(is_medium_freqs, smoothed_freqs, freqs)
176219
elif cfg.scaling == "yarn" and end > cfg.original_seq_len:
177220
# YaRN (DeepSeek V3 style)
178-
beta_fast = cfg.beta_fast
179-
beta_slow = cfg.beta_slow
180-
base = theta
181-
original_seq_len = cfg.original_seq_len
182-
factor = cfg.rope_factor
183-
184-
def find_correction_dim(
185-
num_rotations: float, dim: int, base: float, max_seq_len: int
186-
) -> float:
187-
return (
188-
dim
189-
* math.log(max_seq_len / (num_rotations * 2 * math.pi))
190-
/ (2 * math.log(base))
191-
)
192-
193-
def find_correction_range(
194-
low_rot: float,
195-
high_rot: float,
196-
dim: int,
197-
base: float,
198-
max_seq_len: int,
199-
) -> tuple[int, int]:
200-
low = math.floor(find_correction_dim(low_rot, dim, base, max_seq_len))
201-
high = math.ceil(find_correction_dim(high_rot, dim, base, max_seq_len))
202-
return max(low, 0), min(high, dim - 1)
203-
204-
def linear_ramp_factor(
205-
min_val: float, max_val: float, dim: int
206-
) -> torch.Tensor:
207-
if min_val == max_val:
208-
max_val += 0.001
209-
linear_func = (torch.arange(dim, dtype=torch.float32) - min_val) / (
210-
max_val - min_val
211-
)
212-
return torch.clamp(linear_func, 0, 1)
213-
214-
low, high = find_correction_range(
215-
beta_fast, beta_slow, dim, base, original_seq_len
221+
freqs = _yarn_inv_freq(
222+
dim,
223+
theta,
224+
cfg.rope_factor,
225+
cfg.beta_fast,
226+
cfg.beta_slow,
227+
cfg.original_seq_len,
228+
cfg.truncate,
216229
)
217-
smooth = 1 - linear_ramp_factor(low, high, dim // 2)
218-
freqs = freqs / factor * (1 - smooth) + freqs * smooth
219230

220231
t = torch.arange(end, device=freqs.device)
221232
freqs = torch.outer(t, freqs).float()
@@ -270,48 +281,30 @@ def _precompute_cache(self) -> torch.Tensor:
270281
max_seq_len = cfg.max_seq_len
271282
base = cfg.theta
272283

273-
freq = base ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim)
274-
mscale = 1.0
275-
276284
if cfg.scaling == "llama":
277285
raise NotImplementedError("Cos/sin RoPE does not support Llama scaling.")
278286

279287
if cfg.scaling == "yarn" and cfg.rope_factor > 1.0:
280-
rope_factor = cfg.rope_factor
281-
# YaRN mscale for attention magnitude preservation
282-
mscale = 0.1 * math.log(rope_factor) + 1.0
283-
284-
# Compute correction range (NTK by parts)
285-
d_half = dim / 2
286-
low = (
287-
d_half
288-
* math.log(cfg.original_seq_len / (cfg.beta_slow * 2 * math.pi))
289-
/ math.log(base)
290-
)
291-
high = (
292-
d_half
293-
* math.log(cfg.original_seq_len / (cfg.beta_fast * 2 * math.pi))
294-
/ math.log(base)
288+
inv_freq = _yarn_inv_freq(
289+
dim,
290+
base,
291+
cfg.rope_factor,
292+
cfg.beta_fast,
293+
cfg.beta_slow,
294+
cfg.original_seq_len,
295+
cfg.truncate,
295296
)
296-
assert (
297-
0 < low < high < d_half - 1
298-
), f"Invalid YaRN params: 0 < {low} < {high} < {d_half - 1}"
299-
300-
ramp = (torch.arange(d_half, dtype=torch.float32) - low) / (high - low)
301-
mask = 1 - ramp.clamp(0, 1)
302-
303-
interpolation = 1.0 / (rope_factor * freq)
304-
extrapolation = 1.0 / freq
305-
inv_freq = interpolation * (1 - mask) + extrapolation * mask
306297
else:
307-
inv_freq = 1.0 / freq
298+
inv_freq = 1.0 / (
299+
base ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim)
300+
)
308301

309302
t = torch.arange(max_seq_len, dtype=inv_freq.dtype, device=inv_freq.device)
310303
freqs = torch.outer(t, inv_freq).float()
311304
theta = torch.cat([freqs, freqs], dim=-1)
312305

313-
cos = theta.cos() * mscale
314-
sin = theta.sin() * mscale
306+
cos = theta.cos()
307+
sin = theta.sin()
315308
return torch.cat([cos, sin], dim=-1)
316309

317310
def _reshape_cache(

torchtitan/models/gpt_oss/__init__.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,9 @@ def _debugmodel(
283283
theta=150000.0,
284284
scaling="yarn",
285285
rope_factor=32,
286-
beta_slow=32.0,
287-
beta_fast=1.0,
286+
beta_fast=32.0,
287+
beta_slow=1.0,
288+
truncate=False,
288289
original_seq_len=4096,
289290
),
290291
),
@@ -326,8 +327,9 @@ def _20b(
326327
theta=150000.0,
327328
scaling="yarn",
328329
rope_factor=32,
329-
beta_slow=32.0,
330-
beta_fast=1.0,
330+
beta_fast=32.0,
331+
beta_slow=1.0,
332+
truncate=False,
331333
original_seq_len=4096,
332334
),
333335
),
@@ -369,8 +371,9 @@ def _120b(
369371
theta=150000.0,
370372
scaling="yarn",
371373
rope_factor=32,
372-
beta_slow=32.0,
373-
beta_fast=1.0,
374+
beta_fast=32.0,
375+
beta_slow=1.0,
376+
truncate=False,
374377
original_seq_len=4096,
375378
),
376379
),

torchtitan/models/gpt_oss/model.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ def __init__(self, config: Config):
104104

105105
# Standard attention softmax scale (1/sqrt(head_dim))
106106
self.softmax_scale = 1.0 / math.sqrt(self.head_dim)
107+
if config.rope.scaling == "yarn" and config.rope.rope_factor > 1.0:
108+
mscale = 0.1 * math.log(config.rope.rope_factor) + 1.0
109+
# Merge YaRN attention mscale into softmax_scale, with
110+
# m**2 being equivalent to scaling q / k each by mscale.
111+
self.softmax_scale *= mscale * mscale
107112

108113
self.qkv_linear = config.qkv_linear.build()
109114
self.wo = config.wo.build()

0 commit comments

Comments
 (0)