On-device RWKV-7 training and inference for the Apple ecosystem (iOS · iPadOS · macOS · visionOS), built on MLX and a custom Metal WKV-7 kernel.
RWKV-7 is a linear-attention (RNN-style) language model: constant memory per
generated token and no quadratic attention, which makes it a good fit for running
and adapting models directly on a phone or Mac. SwiftRWKV exposes one canonical
backbone (X070Backbone, a faithful port of official RWKV-7 "Goose" x070) and
builds inference, generation, embeddings, reranking and several ways of
adapting a model on top of it — over a dense base or a quantised .rwkvq one.
import RWKVGen
import MLX
let weights = try loadArrays(url: modelURL) // x070-named safetensors
let bb = X070Backbone(weights: weights,
cfg: X070Config(nLayer: 12, nEmbd: 768, headSize: 64, vocab: 65536))
let tok = WorldTokenizer(vocabURL: vocabURL)!
// Greedy by default — a package that answers differently on every run
// cannot be debugged.
let out = bb.generate(prompt: "The capital of France is",
maxTokens: 64, tokenizer: tok)
print(out.text)- Text generation —
generate(prompt:maxTokens:config:onToken:)with temperature, top-k, top-p and repetition penalties, seeded and reproducible, streaming with stop strings and cancellation; the sampler can run entirely on the GPU. Greedy by default. - Tokenizers — the RWKV World vocabulary, and HuggingFace
tokenizer.json(byte-level BPE, checked id for id againsttokenizers) for models with their own vocabulary; everything takesany RWKVTokenizer. - Batched decode (opt-in) — several generations in one pass per token,
up to 3.4× the throughput at 8 rows (
generateBatch). - Inference — a parallel pass for whole sequences and a recurrent one with O(1) memory per token; prefill uses the parallel pass and is measured 94× faster than folding the prompt token by token (1024 tokens, debug build).
- Reranking — a cross-encoder head over backbone state, with a state cache, listwise training (resumable after an interruption, bit-identically) and an indexed serving path; caches record the attached initial state and refuse another one.
- Embeddings — pooling, a trainable head, contrastive losses, GradCache, retrieval metrics, and a curriculum runner that writes a report after every stage and resumes after a crash; an attached state can apply to every text or to queries only.
- Quantised
.rwkvqbase — the format from rwkv-quant, opened directly withX070Backbone.load(fromRwkvq:)— no dense checkpoint needed — with fused Metal dequantisation (anexport_mlxsidecar is an optional cache, and self-contained only when it is a full export); inference, reranking and embeddings run on it unchanged, and it can be trained over — adapters, the top layers (unpacked to dense, the bottom stays compressed) or a state. - LoRA / QLoRA fine-tuning — for larger models (≈600M+); 4/8-bit quantized
base or a
.rwkvqone. - Top-layer training — full-weight training of the top N layers for smaller
models (≤400M), where adapters carry too few parameters; also over a
.rwkvqbase. The classic path (X070PartialFinetune) caches the frozen lower stack on disk and trains on the shared trainer, reproducibly with a seed. - State-tuning — the base frozen, only the initial state of the recurrence
trained (0.1B: 0.59 M numbers), kept in its own file with a fingerprint of the
base; a trained state and a folded prompt are one pluggable object. Early
stopping on held-out loss, export to and import from RWKV-PEFT
.pth, and weight streaming for training in little memory (1.5B over.rwkvq: 1.58 GB). - Pre-training from scratch — x070 initialisation,
.bintoken streams, a shared trainer with LR schedules and resumable checkpoints. - Custom Metal WKV-7 kernel — forward + a hand-written differentiable (gradient-checkpointed) backward, verified bit-exact against a reference; dedicated decode kernels for one token.
- Memory-bounded training — backward block by block (one block in memory,
the same gradient), rows of a batch through the body in pieces, block inputs
recorded with a stride; fp32 trainable weights over a bf16 (dense) or fp16
(
.rwkvq) base.
iOS 17 / iPadOS 17 / macOS 14 / visionOS 1 · Apple Silicon GPU · Swift 5.10+ ·
mlx-swift ≥ 0.31.4 (resolved automatically). The kernel runs on the Metal GPU —
use a real device or macOS, not the iOS Simulator.
Swift Package Manager:
dependencies: [
.package(url: "https://github.com/RafaelUI/SwiftRWKV", from: "0.4.0"),
],
targets: [
.target(name: "YourApp", dependencies: [
.product(name: "RWKVGen", package: "SwiftRWKV"),
// and, if you need them:
// .product(name: "RWKVRerank", package: "SwiftRWKV"),
// .product(name: "RWKVEmbedding", package: "SwiftRWKV"),
// .product(name: "RWKVQuant", package: "SwiftRWKV"),
]),
]Or in Xcode: File → Add Package Dependencies… → paste the repo URL.
CI builds the library, executables and tests on a clean machine with Xcode 26.3 (Swift 6.2); development is on newer Xcode. Tests need Apple Silicon with Metal.
The latest tag is 0.4.0. Changes since 0.3.0 that need attention when
upgrading:
- The WKV output is cast to the compute type by default on a dense base
(
castWKVOutputToComputeDType; on the.rwkvqnorm path it stays off): decode ×2.8 on 0.1B for +0.09 % perplexity, numbers move by ~1e-2 relative. Set the flag tofalsefor the previous arithmetic. - Checkpoints and caches record their base (
base_fingerprint): a reranker or embedding head, LoRA adapters, a pair cache or a serving index made on another base is refused (RerankerError.otherBase,EmbeddingCheckpointError.otherBase,LoRA.LoadError.otherBase;allowOtherBase: trueto load anyway). Files without the key are not checked. A reranker training checkpoint (checkpoint:) from before no longer resumes. - Tokenizers are a protocol (
RWKVTokenizer): everything that took aWorldTokenizertakesany RWKVTokenizer— passing aWorldTokenizerstill compiles; code that read.tokenizeras aWorldTokenizerneeds a cast. - State-tuning defaults: gradient clipping at 1.0 and the WKV-output cast during training (see docs/state-tuning.md).
- New:
BPETokenizer,generateBatch, early stopping, RWKV-PEFT.pth, weight streaming.
Sources/
├── RWKVKernel/ # custom Metal WKV-7 kernels (low-level)
│ ├── WKV7.swift # forward + chunk forward + constants (CHUNK=16, HEAD_SIZE=64)
│ ├── WKV7Step.swift # single-token step, boundary state
│ ├── WKV7DecodeKernels.swift # decode: one-token WKV step and tmix tail in one launch
│ ├── WKV7Train.swift # differentiable checkpointed backward (CustomFunction)
│ ├── WKV7Reference.swift # naive recurrent DPLR — autograd ground-truth for tests
│ └── Ablation.swift # subtractive ablation of a training step (instrument)
├── RWKVQuant/ # .rwkvq quantised base (independent of the backbone)
│ ├── RwkvqFile.swift # reading .rwkvq directly
│ ├── RwkvqSidecar.swift # the opened base (from .rwkvq or an export_mlx sidecar), manifest, naming
│ ├── RwkvqFullConvert.swift # the whole x070 weight dictionary from a full export
│ ├── RwkvqDequant.swift # fused Metal dequantisation, one launch
│ ├── RwkvqNative.swift # repacking into MLX's native quantised container
│ └── RwkvqSb6GEMV / SymGEMV / SymRows.swift # projection kernels per preset
├── RWKVGen/ # backbone, inference, generation, training
│ ├── X070Backbone.swift # canonical RWKV-7 backbone + split + training hooks
│ ├── RWKVBlock.swift # one block, shared by the backbone and the rerank head
│ ├── RWKVBatchState.swift # batched boundary state, masks, right-padding
│ ├── InitialState.swift # pluggable initial state, state-tuning, state files
│ ├── X070Generation.swift # RWKVState + prefill / step (streaming)
│ ├── Sampling.swift # SamplingConfig, seeded sampler, penalties
│ ├── GPUSampler.swift # the same sampler without a CPU read per token
│ ├── Generate.swift # generation loop: stop conditions, byte-safe streaming
│ ├── WorldTokenizer.swift # RWKV World trie tokenizer
│ ├── BPETokenizer.swift # HuggingFace tokenizer.json (byte-level BPE), RWKVTokenizer protocol
│ ├── RwkvqBase.swift # attaching a .rwkvq base to an already-loaded dense backbone
│ ├── RwkvqStandaloneLoad.swift# a backbone from one .rwkvq file, no dense file needed
│ ├── LoRA.swift # LoRA/QLoRA spec, add/merge/save/load, base quantization
│ ├── LoRAFinetune.swift # LoRA training loop (LM objective)
│ ├── X070PartialFinetune.swift# N-layer partial fine-tune (boundary cache + train)
│ ├── Checkpoint.swift # gradient-checkpoint primitive
│ └── Training/ # shared trainer, LR schedules, trainable sets,
│ # backward block by block, x070 init, token
│ # streams, pre-training
├── RWKVEmbedding/ # text vectors: pooling, head, contrastive losses,
│ # GradCache, curriculum, metrics, checkpoints
├── RWKVRerank/ # cross-encoder reranker: head over state, state
│ # cache, listwise training, indexed serving
├── RerankRun/ # executable rerank-run: data → cache → training → report
├── EmbedRun/ # executable embed-run: an embedding stage or a resumable curriculum, evaluation, A/B instruments
└── DecodeBench/ # executable decode-bench: decode and training-step benchmarks
Four library products. import RWKVGen covers inference, generation and every
training path — it links RWKVKernel and RWKVQuant itself. Import
RWKVRerank or RWKVEmbedding for those tasks, and RWKVQuant alone only to
read a .rwkvq or a sidecar without a model. RWKVKernel is an internal target, not a
product: the kernels are reached through RWKVGen. Three executables —
rerank-run, embed-run, decode-bench — are the runners and instruments the
numbers in the docs come from.
Guides live in docs/, each with examples that are compiled on every test
run (Scripts/check_docs.sh) so they cannot silently rot:
- docs/Inference.md — the two passes, generation and
sampling, streaming and stop conditions, the quantised
.rwkvqbackend, and the measured numbers with the conditions they were measured under. - docs/Train.md — the shared trainer and trainable sets,
LoRA/QLoRA (also over
.rwkvq), N-layer partial fine-tuning, pre-training. - docs/state-tuning.md — training the initial state,
attaching it and folding prompts, the state file and base fingerprint, and
what it buys: quality, transfer between a model and its
.rwkvqfiles, cost. - docs/reranker.md — data, state cache, training the head, layer sweeps, serving.
- docs/Embedding.md — pooling, heads, losses,
GradCache, curriculum, metrics, memory of a training step, training over a
.rwkvqbase, a state in place of the instruction.
Design decisions and their reasoning are in ARCHITECTURE.md; the working log of what is measured, what is missing and what was tried and rejected is in NEXT_SESSION.md.
Experimental, but validated end to end on real models rather than on toys —
RWKV-7 World 0.1B, g1d 0.1B / 0.4B dense and as .rwkvq, and 1.5B / 2.9B for
inference; state-tuning on 1.5B. 694 tests, plus mutation suites that deliberately break the code and
require the tests to notice — the interesting failures in this problem are
silent, so "the tests pass" is not by itself evidence.
What is checked: WKV-7 kernel parity (bit-exact against a reference), the
hand-written backward against autograd, logits parity with the Python
implementation on real weights, the recurrent decode against the parallel pass
(7.4e-7 relative), the partial-fine-tune split (Δ = 0), .rwkvq dequantisation
bit-for-bit against a Python reference, gradients of the block-by-block
backward and of the state against autograd, state files and base fingerprints
against the rwkv-metal reference, and end-to-end fine-tuning runs.
API may still change before a stable release.
Apache 2.0 — see LICENSE.
SwiftRWKV — ImpulseLeap / Alexei Goncharov · https://www.impulseleap.com