Skip to content

fix(internlm2): add family rope_scaling tests and NTK rescale log - #1736

Merged
inureyes merged 7 commits into
mainfrom
fix/issue-1320-internlm2-rope-scaling
Sep 10, 2026
Merged

inureyes merged 7 commits into
mainfrom
fix/issue-1320-internlm2-rope-scaling

Conversation

@inureyes

@inureyes inureyes commented Sep 9, 2026 •

Copy link
Copy Markdown
Member

Summary

PR #1389 already wired internlm2::Attention to the shared DynamicNtkRope schedule (issue #1324), so the rope_scaling and rope_traditional plumbing itself was already correct on main. This PR closes the rest of #1320: family-level tests proving that plumbing reaches the helper, a debug log of the rescaled base, and real-checkpoint validation.

What changed

  • src/models/internlm2_tests.rs: tests that ModelArgs::rope(), not just the helper, resolves a linear block to an inverse-factor scale, carries rope_traditional through in both states, reproduces the issue's dynamic table (base_for(40000), base_for(65536)), accepts the rope_type spelling, and rejects a factor-less dynamic block with an error naming the family.
  • src/models/dynamic_ntk_rope.rs: a traditional() accessor, and a debug-level log of base_eff the first time a dynamic schedule crosses max_position_embeddings. The dedup is keyed on the schedule's own parameters rather than a process-wide flag, following rope_utils::report_unusable_rope_scaling_once, so a second checkpoint in the same process (--models-dir routing, pipeline stages, TP ranks) still gets its line. apply asks tracing::enabled! before it takes the dedup lock, so a run with no debug subscriber never touches that shared lock; the rotary arithmetic is unchanged either way.
  • Comments and docs/supported-models.md named a checkpoint directory that does not exist (models/internlm2-7b-4bit); corrected to models/internlm2_5-7b-chat-4bit, and the entry now records the long-context evidence rather than crediting the short run with it, at the figures the closed form actually gives.

Validation

--lib models::internlm2 (8 passed) and --lib models::dynamic_ntk_rope (17 passed) under --profile test-fast --features metal,accelerate; cargo clippy --lib --tests and cargo fmt --all --check clean, which is the scope ci.yml runs. The --workspace gate in issue #1320's acceptance criteria runs in nightly-verify.yml, not on this PR.

Real checkpoint models/internlm2_5-7b-chat-4bit, greedy. Against a binary built from this branch's parent commit, an 89-token prompt at -n 64 and a 34021-token prompt at -n 32 both produce byte-identical generated text (MD5 5a6d6133 and 69a12bff). That pair is a control rather than a parity result: the parent already carries #1389's wiring, so the two arms differ only by the debug diagnostic this PR adds, and byte-identity is what it should show.

The long-context run was repeated against the final binary, after the tracing::enabled! gate was added, so the diagnostic is confirmed to still reach a RUST_LOG=debug run rather than only having done so before that commit. A 34021-token prompt emits exactly one line, seq_len=34021 base_eff=1077737.0, and the generation answers the question the prompt ends with in fluent on-topic text (Rayleigh scattering). 1077737.0 is the issue's closed form at that length: f64 gives 1077736.99 there, so the f32 the code computes agrees to every digit the line prints, and a 34022-token prompt gives base_eff=1077799.125, which the same f32 evaluation reproduces bit for bit. The base moves about 62 per token at that length, which is why the entry in docs/supported-models.md now quotes the value with its length; its previous parenthetical read the closed form at 34020 as an f64-versus-f32 gap at 34021. The same form gives the issue's table at 40000 (1449795.74) and 65536 (3052773.67).

Closes #1320

@inureyes inureyes added status:review Under review type:bug Bug fixes, error corrections, or issue resolutions priority:medium Medium priority area:models Model architectures, weights, loading, metadata area:inference Generation, sampling, decoding (incl. speculative, DRY) modelsize:small 4-bit checkpoint <= 10GB; fast iteration, safe for smoke tests modeltype:text Text-only language model arch:dense Dense transformer decoder labels Sep 9, 2026
PR #1389 already wired internlm2::Attention to the shared DynamicNtkRope schedule (issue #1324), so the rope_scaling and rope_traditional plumbing itself was already correct on main. This closes the remaining gap issue #1320 asked for: family-level tests that prove ModelArgs::rope() reaches the helper (linear scale, rope_traditional, the checkpoint's own dynamic table, the rope_type spelling, and the factor-less load error), a small traditional()/dims() accessor pair on DynamicNtkRope needed by those tests, and a one-time debug-level log of base_eff the first time any dynamic schedule crosses max_position_embeddings, gated by a process-wide AtomicBool so 32 layers times every decode step past the boundary do not spam it.

Also corrected the checkpoint path in existing comments from the nonexistent models/internlm2-7b-4bit to the real models/internlm2_5-7b-chat-4bit, and named that checkpoint as InternLM2's own validation checkpoint in docs/supported-models.md.

Validated against models/internlm2_5-7b-chat-4bit: the main binary built before this change and the rebuilt binary produce byte-identical output (MD5-matched) on a 73-word prompt with -n 64 -t 0, confirming the schedule is unchanged inside max_position_embeddings. cargo test --profile test-fast --features metal,accelerate --lib models::internlm2 and --lib models::dynamic_ntk_rope both pass (8 and 16 tests respectively), and cargo clippy --lib --tests --features metal,accelerate -- -D warnings and cargo fmt --all -- --check are clean.

Refs #1320
@inureyes
inureyes force-pushed the fix/issue-1320-internlm2-rope-scaling branch from 9943dc2 to c26adb2 Compare September 10, 2026 02:09
The process-wide `AtomicBool` guarding the new `base_eff` debug log contradicts the convention `rope_utils::report_unusable_rope_scaling_once` documents: a process can hold more than one model (`--models-dir` routing, pipeline stage executors, tensor-parallel ranks), and a single flag lets the first checkpoint past `max_position_embeddings` silence every later one. The guard is now a `OnceLock<Mutex<BTreeSet<_>>>` keyed on `(dims, base bits, max_position_embeddings, factor bits)`, matching that precedent, and `log_dynamic_rescale_once` returns `bool` so the dedup is testable rather than only observable through stderr. The lock is taken only past the boundary, where a decode step already costs about a second.

Drops `DynamicNtkRope::dims()`, which had no call site in the tree, and corrects `traditional()`'s `// Used by:` line, which named InternLM3 and non-test InternLM2 callers that do not exist.

`docs/supported-models.md` credited the short-prompt run with proving the plumbing, but that A/B was against this branch's parent, which already carries #1389's wiring. It now records the long-context result that does prove it, `base_eff=1077737.0` at 34021 tokens; `internlm2.rs`'s module doc likewise now credits #1389 for the wiring.

Validation: `--lib models::dynamic_ntk_rope` 17 passed (one new dedup test), `--lib models::internlm2` 8 passed, clippy and fmt clean. Rebuilt and re-ran the real-checkpoint A/B on `models/internlm2_5-7b-chat-4bit` against a binary built from this branch's parent: an 89-token prompt at `-n 64 -t 0` and a 34021-token prompt at `-n 32 -t 0` are both byte-identical, and the long run emits exactly one rescale line.

Refs #1320
`apply` called `log_dynamic_rescale_once` unconditionally, so every forward past `max_position_embeddings` took the global dedup set's lock even with no subscriber installed. That is 64 uncontended acquisitions per decode step on a 32-layer model, and the set is shared across every schedule in the process, so tensor-parallel ranks sharing one process serialize on it. Asking `tracing::enabled!` first makes the default run lock-free. The gate sits in `apply` rather than inside the helper because the dedup test calls the helper directly and asserts on its return value.

Also records two properties review surfaced. The logged base is a snapshot taken at the first crossing rather than the base in force later in the run, because the dedup key deliberately omits `seq_len`. And the dedup test asserts on process-global state, so the geometries it depends on are now reserved in a comment beside the shared test constants, where a later test author will see them.
The catalog entry credited the InternLM2 checkpoint with "the second half of the evidence" directly beside InternLM3's corrected mlx-lm parity result, which reads as a comparable claim and is not one. The byte-identity runs recorded in #1320 compare this tree against the same tree with only the schedule's debug diagnostic removed, so the two arms differ by a log line and cannot disagree by construction. That is a control showing the diagnostic is inert, not a result about the rotation, and the repo's own rule against settling for byte-identity applies to it.

What the 34021-token run does establish is that the dynamic branch is entered and computes the documented base. The f32 evaluation is load-bearing there rather than incidental: f64 gives 1077674.9 at that length against the observed 1077737.0. No corrected long-context reference exists for this family the way one was built for InternLM3, and the entry now says so instead of implying otherwise.

Also points the test module header at both issues, since five of its eight tests came from #1320 rather than from #1324's shared helper.
…ents

Comment-only, no line of code changes. Security review read the block closely and found each of these stated something the code does not do.

The key's doc claimed `from_scaling` had already rejected NaN through `is_usable_scalar`. That screen covers `factor` only; `base` arrives from `rope_theta` by way of `rope_overrides::resolve_base` and reaches `to_bits()` unscreened. The key is an identity on bit patterns, not on values, and the comment now says so along with what that would cost: a `-0.0` or NaN base could split one schedule across two log lines, and nothing worse.

The poisoning-recovery comment blamed a subscriber panicking while logging, which cannot happen, because the guard is dropped before the event is emitted. Only a panic inside `insert` could poison this lock, which in practice means nothing since allocation failure aborts rather than unwinds. The recovery stays; only the reason was wrong.

The cost argument cited a decode step costing on the order of a second. That is the 34021-token prefill, not a decode step, which runs tens of milliseconds at these lengths. The argument holds on the real numbers and now uses them, including the part the old comment left out: after the line is emitted the lock is still taken per call for a lookup that can only answer "already present".

The `tracing::enabled!` guard declares no fields while the event it guards declares six, so a field-selecting `EnvFilter` directive could enable the event and not the guard and silently drop the diagnostic. Every documented way in is a level directive, `-v` and `--verbosity 4` both expand to one in `server::logging` and the issue's recipe is `RUST_LOG=debug`, so the gate stays and the limit is now written down beside it.

Refs #1320
@inureyes

Copy link
Copy Markdown
Member Author

PR Finalization Check

Reviewed test coverage, documentation, and lint/format on fix/issue-1320-internlm2-rope-scaling at 1aba962d. No gaps found, no changes made.

Tests

--lib models::internlm2 (8 passed) and --lib models::dynamic_ntk_rope (17 passed) under --profile test-fast --features metal,accelerate both pass locally, matching the counts in the PR description. Coverage already spans the linear/dynamic/absent rope_scaling cases, both type and rope_type spellings, rope_traditional true and the serde-default false, the factor-less dynamic load error naming the family, and a dedicated dedup test for the rescale log (first-crossing fires, repeat and per-layer-rebuilt schedules stay silent, a different base or factor still logs, and both the boundary and non-dynamic modes never log). No real gap to add tests for.

Documentation

docs/supported-models.md already carries the corrected checkpoint path and states plainly what the long-context run does and does not establish. Checked docs/llama-server-compat.md and docs/model-catalog.tsv for stale references to the old internlm2-7b-4bit name; the TSV already tracks it as a documented alias of internlm2_5-7b-chat-4bit, and the benchmark result logs under docs/benchmark_results/ are historical run records that correctly use whatever name was live at the time, so neither needed a change. No docs/ko tree exists in this repository (the mkdocs manual under docs/en and docs/ko is a separate documentation tree per the Makefile comments), so there is no Korean file to update.

Lint / format

cargo fmt --all --check and cargo clippy --lib --tests --features metal,accelerate -- -D warnings both pass clean. cargo check --lib --tests also clean.

All PR CI checks (cargo-clippy, cargo-fmt, cargo-deny, OpenXLA feature compile, crate versions, cross-repo refs, kernel dtype keys, llama-compat manifest) are green. Ready for merge from this angle.

The InternLM2 entry cited 1077674.9 as the f64 value of the dynamic-NTK closed form at 34021 tokens and concluded from the gap to the logged 1077737.0 that the f32 evaluation was load-bearing. It is not: 1077674.9 is the same closed form one position short, at 34020. At 34021 f64 gives 1077736.9875 and the f32 chain the code runs gives 1077737.0, so the two agree to every digit the log prints, and what the number establishes is only that the dynamic branch was entered at the length the line reports.

Re-measured on `models/internlm2_5-7b-chat-4bit` with the branch binary rather than recomputed on paper. A 34021-token prompt under `RUST_LOG=debug` emits exactly one rescale line, `seq_len=34021 base_eff=1077737.0`, and answers the question the prompt ends with in fluent on-topic text. A 34022-token prompt emits `base_eff=1077799.125`, which an f32 evaluation of the same form reproduces bit for bit; the 62-per-token gap between the two is why the length has to be quoted with the value, and it is the gap the old parenthetical mistook for a precision effect.

Refs #1320
@inureyes inureyes added status:done Completed and removed status:review Under review labels Sep 10, 2026
Bilingual report dated today rather than by mergedAt, because `TECHNICAL_REPORTS/.keep-reports` opts this repository into committing the report into the PR branch before the merge.

It records the finding that shaped the branch: #1320's whole implementation plan had already merged under #1389, so what was left was the issue's validation half. It then covers what the five family-level tests pin that a helper test cannot see, the three decisions behind the rescale diagnostic (per-schedule dedup instead of a process-wide flag, the `tracing::enabled!` gate in `apply`, and the field-filter blind spot that gate leaves open), the three documentation corrections, and the limits of the validation: the byte-identity pair is a control rather than a parity result, and no corrected long-context reference exists for this family.

Refs #1320
@inureyes
inureyes merged commit d9d8811 into main Sep 10, 2026
13 checks passed
@inureyes
inureyes deleted the fix/issue-1320-internlm2-rope-scaling branch September 10, 2026 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch:dense Dense transformer decoder area:inference Generation, sampling, decoding (incl. speculative, DRY) area:models Model architectures, weights, loading, metadata modelsize:small 4-bit checkpoint <= 10GB; fast iteration, safe for smoke tests modeltype:text Text-only language model priority:medium Medium priority status:done Completed type:bug Bug fixes, error corrections, or issue resolutions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(internlm2): honor rope_scaling (dynamic NTK, linear) and rope_traditional

1 participant