Skip to content

Add RWKV-7 (Goose) - #47780

Open
Hakureirm wants to merge 10 commits into
huggingface:mainfrom
Hakureirm:add-rwkv7-upstream
Open

Add RWKV-7 (Goose)#47780
Hakureirm wants to merge 10 commits into
huggingface:mainfrom
Hakureirm:add-rwkv7-upstream

Conversation

@Hakureirm

@Hakureirm Hakureirm commented Aug 4, 2026

Copy link
Copy Markdown

CI

Adds RWKV-7 ("Goose") as Rwkv7Model / Rwkv7ForCausalLM.

Follows up on #46984, where the integration was declined because the published checkpoints did not follow Transformers conventions. That objection was correct at the time; it has since been resolved upstream — see below.

Checkpoints (updated 2026-08-07)

On 2026-08-05 the RWKV org published official checkpoints in the standard layout: RWKV/RWKV7-1.5B-20260805, 2.9B, 7.2B and 13.3Bsafetensors only, no auto_map, no remote code, a standard config.json, and the World vocabulary shipped as a plain tokenizer.json that AutoTokenizer loads as an ordinary fast tokenizer.

This implementation loads them unchanged:

  • every key in the official config.json is a declared Rwkv7Config parameter, except model_type, which the base class handles;
  • all 798 tensors match by name and by shape;
  • the release itself is byte-identical to the canonical BlinkDL/rwkv7-g1 .pth it was packed from — 798/798 tensors matched by content hash, a bijection (measured in Add RWKV-7 (Goose) #47787).

The docs and the configuration_rwkv7.py doc checkpoint point at the official repos. (Historical note, since it was the reason #46984 was closed: the older RWKV/*-PTH releases ship no safetensors, and the older RWKV/*-HF ones require trust_remote_code. The 20260805 line is what resolved this.)

Implementation

Portable PyTorch, no third-party runtime dependency:

  • Prefill uses a chunk-parallel form of the recurrence; decode runs the sequential single-token path.
  • Rwkv7Cache is built on LinearAttentionLayer. The WKV state dtype is configurable independently of the model dtype, since the recurrent state is more precision-sensitive than the weights.
  • Parameter names follow the upstream RWKV reference implementation, so converting a native .pth is a prefix rename. The converter also reads the flash-linear-attention layout.

Correctness

Three independent checks, in addition to the common mixins:

  • tests/models/rwkv7/test_modeling_rwkv7.py — an integration test matched token-for-token against BlinkDL's own runtime (the rwkv package at cpu fp32), through this PR's converter, on a checkpoint whose head count (12) and head width (64) differ, so a quantity indexed by the wrong one cannot pass by coincidence.
  • tests/models/rwkv7/test_modeling_rwkv7_numpy_reference.py — a numpy re-derivation of the forward pass that shares no code with the model, so an error in the PyTorch path cannot be masked by the same error in the reference.
  • Cross-runtime numerics on the official 1.5B (protocol and criteria in Add RWKV-7 (Goose) #47787): against the rwkv package's PyTorch path, 24 lambada prompts, same forced tokens, per-position logits and per-layer recurrent states, prefill (T>1) and decode (T=1) legs both. fp32: worst relative deviation 8.2e-6 across everything compared. bf16 vs bf16, same protocol: median deviation 6e-3 (bf16's own precision), with signed bias 20–180× smaller than the deviation itself — symmetric rounding, with no directional offset at the state level, which is where the products the dtype question concerns are formed.

Code Agent Policy

The Transformers repo is currently being overwhelmed by a large number of PRs and issue comments written by
code agents. These often are low-quality, or fix extremely minor issues that occur rarely or never in practice.
As a result, we're instituting a rule that first-time contributors should not use code agents to submit PRs or issues.
We'd also ask autonomous "OpenClaw"-like agents not to open any PRs or issues.

Issues/PRs from first-time contributors that violate this rule will probably just be closed without review, and we
might block you, especially if you open more than one or appear to be deliberately ignoring this. We especially do not
want new contributors to jump in on random issues to contribute an agent-written fix. This creates lots of noise
for reviewers and other users and will almost certainly get you blocked.

For more information, please read CONTRIBUTING.md.

  • (First-time contributors only): I confirm that this PR description and code is not written by an LLM or code agent

Before submitting

Who can review?

@Cyrilvallez — you raised the conventions objection on #46984; the checkpoint section above is meant to answer it directly.
@ArthurZucker @vasqu

@Hakureirm
Hakureirm marked this pull request as ready for review August 4, 2026 21:21
@Hakureirm Hakureirm mentioned this pull request Aug 5, 2026
2 tasks
@Hakureirm

Copy link
Copy Markdown
Author

Update on the ONNX known issue — narrowed, still open, and the evidence now points
away from this model.

Two no-op views in the model are gone (f3c38d5): the chunked recurrence padded to
a multiple of the chunk size and computed in a wider dtype, then unconditionally
sliced back to seq_len and cast back to the input dtype. When the input needed
neither — the common case — both returned views, and a view becomes aten.alias
under export. They are now conditional. 125 tests and 1581 subtests still pass and
modular_rwkv7.py still regenerates byte-identically.

The export test still fails, and that is the interesting part. After the
change, torch.export.export on this model produces zero alias nodes
verified using the export test's own _prepare_export_model_and_inputs fixture
rather than a hand-built one, for Rwkv7Model and Rwkv7ForCausalLM. Running the
ONNX decomposition table over that graph also yields zero detach_. Yet
OnnxExporter().export(...) still raises, in all four combinations of
dynamic={False,True} × optimize={False,True}.

The failing node is %detach__1 = aten.detach_(%alias_1). In the exporter's graph
alias_1's input is _tensor_constant0; in the graph torch.export produces, the
same alias consumed layer_norm_3, a live value. So the alias that fails is
created inside the exporter's own decomposition step and sits on a folded
constant, not on anything this model hands it.

I do not think that is something I can fix from the model side, and I would rather
say so than keep changing model code until the symptom moves. If a maintainer
recognises this shape — an alias synthesised on a constant during
Run decompositions, with mamba and rwkv clean on the same environment — I am
happy to chase it further with a pointer. A minimal reproduction (no test
harness, ~25 lines) is ready to attach.

@Hakureirm

Copy link
Copy Markdown
Author

Correction to my previous comment: this was fixable from the model side, and it is fixed.

I was right that the failing aten.detach_ sits on an alias created inside the exporter's own decomposition step, on a folded constant. I was wrong to conclude it therefore came from nothing this model hands over. It does: aten.cumprod and aten.linalg_solve_triangular each decompose into a graph carrying a scalar tensor constant; lift_fresh on it maps to nop_decomposition (return aten.alias(x)), aten.alias.default has no entry of its own in the ONNX table so it survives, and functionalization rewrites it to the in-place aten.detach_. Two ops, either of which is sufficient — which is why removing one at a time never changed anything, and why three correct changes ended up in my "ruled out" list.

Both are replaced in 6869cd7. Export tests on this branch: 7 failed, 12 passed, 9 subtests passed12 passed, 16 subtests passed. Model suite: 126 passed, 1583 subtests passed. modular_rwkv7.py still regenerates byte-identically.

Worth flagging for anyone reading the diff, because I got it wrong first. The matrix is nilpotent, so the Neumann series terminates and Newton doubling reaches the inverse in ceil(log2(span)) steps — exactly, in exact arithmetic only. On the matrices this model produces, entries are at most 0.977 and the true inverse's at most 1.0, while the 32nd power reaches 1.3e11; float32 cancels away everything and returns entries of 1e4 where the answer is 1, NaN one layer later. A fifth of random initialisations at T=1024, one in forty at T=256, none at T=128 or below. Random triangular matrices cannot show this — their own inverses are as large as the intermediate powers — so it survives exactly the validation one reaches for first. The replacement is block forward substitution; against float64 solve_triangular on the chunk that breaks the series it lands at 6.6e-7. The regression test is end-to-end and seeded, and was run against all three implementations to confirm it fails on the one it is about.

No maintainer input needed on this any more.

@Hakureirm
Hakureirm force-pushed the add-rwkv7-upstream branch from 13aa294 to 9be4ac3 Compare August 6, 2026 19:48
@Hakureirm

Copy link
Copy Markdown
Author

Rebased onto current main and pushed a follow-up commit; summary for reviewers:

  • Aligned with 🚨 [Kernels] Refactor all linear attn models & native kernels fallback #47630: the model-local WKV registry (RWKV7_WKV_FUNCTIONS + wkv_implementation) is gone — the torch reference is called directly, and the optimised-kernel path for this model is the hub-kernels mechanism, once such a kernel exists.
  • Fixed a real generate bug: with a caller-supplied state (allocate_state for compiled decode, or a warm state resuming a turn), input truncation keyed on the state's existence and silently dropped every prompt token but the last. Now gated on is_first_iteration, same as Mamba; covered by new tests.
  • cu_seq_lenscu_seq_lens_q (the ecosystem name, so padding-free collators reach the packed path instead of being swallowed), Rwkv7Cache.crop() refuses loudly, plus smaller convention fixes — the commit message carries the full list.
  • Five new tests: generate-with-state equalities, end-to-end left-padded batch generate, the compiled decode step over a pre-allocated state, the chunked-prefill mask-slicing contract, and a multi-chunk numpy-oracle case.

Re-verified on the official 1.5B after the change: the fp32 and bf16 cross-runtime statistics reported in #47787 reproduce bit-for-bit (the changes are numerics-neutral), the slow integration suite passes, and compiled decode throughput is unchanged (~16× eager on an RTX 3090; reduce-overhead measured faster than max-autotune on this kernel chain — docs updated to match).

Adds the RWKV-7 attention-free recurrent language model as Rwkv7Model /
Rwkv7ForCausalLM: portable PyTorch with no third-party runtime dependency,
a chunk-parallel prefill form of the recurrence, sequential single-token
decode, and an Rwkv7Cache built on LinearAttentionLayer with a separately
configurable WKV state dtype.

Parameter names follow the upstream RWKV reference implementation, so
converting a native .pth checkpoint is a prefix rename; the converter also
reads the flash-linear-attention layout. Tests include the common mixins, an
integration test matched token-for-token against BlinkDL's own runtime, and
an independent numpy re-derivation of the forward pass that shares no code
with the model.
Generated by utils/add_dates.py; fixes the repository-consistency check.
The examples_torch shard failed on a self-hosted runner infrastructure
error ("Executing the custom container implementation failed"), not on
anything in this PR - it does not touch examples/.
The chunked recurrence pads to a multiple of the chunk size and computes in a
wider dtype, then unconditionally slices back to seq_len and casts back. When the
input needed neither, both are no-ops that still return views, and a view becomes
aten.alias under export. The exported graph now has zero alias nodes.
Seven ONNX export subtests failed under RUN_SLOW, and both causes are the same
shape. `aten.cumprod` and `aten.linalg_solve_triangular` each decompose into a
graph carrying a scalar tensor constant; `aten.lift_fresh` on that constant
decomposes to `aten.alias`, functionalization rewrites the alias to the
in-place `aten.detach_`, and aot_autograd rejects a graph that mutates its
input. Neither op is reached by the recurrent path, so decode-only exports were
green and only the prefill ones failed.

`cumprod(exp(x))` is `exp(cumsum(x))`, and the decay is only ever wanted
exponentiated, so the sum is taken first. That is also the better arithmetic:
a cumulative product of factors below one underflows the longer it runs, and
`c_prev` becomes a subtraction rather than a division by a possibly-tiny value.

The solve is replaced by a batched inverse of the same unit lower triangular
matrix, computed by block forward substitution. The tempting alternative is a
series -- the matrix is nilpotent, so `I - x + x^2 - ...` terminates and Newton
doubling reaches it in ceil(log2(span)) steps, exactly. Exactly in exact
arithmetic: the intermediate powers are not bounded by the answer. On a real
chunk the matrix has entries at most 0.977 and its inverse has entries at most
1.0, while the 32nd power reaches 1.3e11, so float32 cancels away every digit
it has and the result comes back with entries of 1e4 where the answer is 1.
Block forward substitution never forms a power above `block`; against float64
`solve_triangular` on that chunk it lands at 1.3e-7 for block 4, 6.6e-7 for
block 8 and 1.9e-5 for 16.

Worth recording that random triangular matrices cannot detect any of this:
their own inverses are as large as the intermediate powers, so a series looks
accurate to 1e-6 on exactly the input a test reaches for first. The test added
here is end-to-end and seeded, at a length of several chunks, because the
failure needs chunks to compound -- a fifth of random initialisations at
T=1024, one in forty at T=256, none at T=128 or below.
The note said a third slower on CPU end to end and left the GPU unmeasured, which
is the number anyone reading it would want. Against the per-chunk solve it
replaces, loop included, on an RTX 5090 at 1.5B shapes: 2.5x at T=1024, where 16
chunks do not fill the card, falling to 1.2x at T=4096 and flat in batch and head
count.
The Checkpoints section opened with "No checkpoint on the Hub loads into this
implementation as-is". That was true when it was written and is not any more:
the RWKV organisation's 20260805 release is a plain safetensors directory whose
config.json maps onto Rwkv7Config key for key, and it loads with no conversion
and no trust_remote_code.

Verified on an A800 against the 1.5B release rather than assumed:

  - config.json: every key is a declared parameter of Rwkv7Config except
    model_type, which the base class handles.
  - 798 tensors, exact name and shape match against the state dict.
  - The release is byte-identical to BlinkDL's source .pth -- 798/798 tensors,
    same bf16 storage dtype, matched by content hash so no naming convention
    has to be agreed first. The conversion is lossless.
  - fp32 greedy output matches BlinkDL's own numpy forward on 24/24 lambada
    prompts, and is invariant to batching (0/24 differing at batch 4 and 8) and
    to left padding (0/8).
  - bf16 differs on 2 of those 24 continuations. Both are near-ties: the first
    divergence is at generated token 9 with a top1-top2 margin of 0.023 logits,
    which several recurrent steps of 8-bit mantissa are enough to tip. The fp32
    arm being exact is what identifies this as precision rather than an error.

The usage example moves to the official 1.5B, which also removes the
trust_remote_code line: these repos ship the World vocabulary as a tokenizer.json,
so AutoTokenizer loads it as an ordinary fast tokenizer.

The slow integration test is deliberately not touched. It converts BlinkDL's raw
0.1B .pth and compares against values from BlinkDL's own runtime, so it does not
certify this implementation with this implementation; and at 768 wide with
head_dim 64 the head count and head width differ, which the 1.5B and larger
releases do not give (7.2B and 13.3B are 64 and 64).
…e, adopt standard names

- Drop the private RWKV7_WKV_FUNCTIONS registry and its config knob: it
  duplicated the kernel-substitution mechanism huggingface#47630 just unified across
  linear-attention models. The torch reference is called directly; an
  optimised kernel arrives via hub-kernels when one exists, not a per-model
  registry. The doc section demonstrating the registry goes with it.
- Gate generate's input truncation on is_first_iteration instead of the
  state's existence: a pre-allocated state (the compile contract) or a warm
  state (a resumed chat turn) silently dropped every prompt token but the
  last one. Same gate Mamba uses; bare callers keep the old behaviour.
- Rename cu_seq_lens to the ecosystem's cu_seq_lens_q, so padding-free
  collators reach the packed path instead of being swallowed by kwargs.
- Refuse Rwkv7Cache.crop() loudly (the state is not invertible), fix
  keys_to_ignore_at_inference to name the output this model actually has,
  add the missing explicit models/__init__ re-export, and drop the
  attentions=None output field and ignored output_attentions parameter.
- Tests: generate-with-state equalities, end-to-end left-padded batch
  generate, compiled decode step over allocate_state, the chunked-prefill
  mask-slicing contract, a multi-chunk numpy-oracle case; pin the numpy
  reference to its external sources and the rwkv package version used for
  the integration expectations; let the shared gradient-checkpointing
  trainings run via test_all_params_have_gradient.
- Docs: compile guidance now reflects measurement (reduce-overhead with an
  eager prefill; max-autotune measured slower on this kernel chain).
utils/add_dates.py derives the expected date from the first commit of the
model's files on main; for a model that is not merged yet there is no such
commit, so it falls back to today and the check fails once the branch is
more than a day old. Re-stamped with the value the script itself produces.
@Hakureirm
Hakureirm force-pushed the add-rwkv7-upstream branch from 9be4ac3 to 86af266 Compare August 8, 2026 08:56
… needs

The time-mix and the channel-mix both have a `key` and a `value`, so a
`target_modules` list carried over from an attention model attaches to twice as
many projections as its author expects, silently. Name both sets and show the
qualified form that selects the time-mix alone.

Also record that a chunked-loss trainer may look for the recurrence under
`past_key_values`: TRL's `SFTTrainer` defaults to `loss_type="chunked_nll"`,
whose head reads that field, and this model returns `state` — the same shape of
mismatch `MambaOutput` has with `cache_params`. `loss_type="nll"` skips it.
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

[For maintainers] Suggested jobs to run (before merge)

run-slow: auto, rwkv7

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

CI recap

Dashboard: View test results in Grafana
Latest run: 31263279678:2
Result: success | Jobs: 16 | Tests: 178,842 | Failures: 0 | Duration: 16h 59m

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants