/* SPDX-License-Identifier: AGPL-3.0-or-later AND Apache-2.0 */ /* Copyright (C) 2026 David (Corey) Herrera */ /* * Portions derived from wkv7_cuda_fp32.cu in RWKV-LM * (https://github.com/BlinkDL/RWKV-LM), by Bo Peng (BlinkDL), * licensed under the Apache License, Version 2.0. Modified by * David (Corey) Herrera, 2026. See NOTICE and LICENSES/Apache-2.0.txt. */ /* * rx_pretrain.h — RWKV-7 pretraining kernels (backward-through-time) * * Prefix: rx_ (RWKV-X). This is the start of the pretrain module; for now it * contains ONLY the WKV7 backward-through-time (the counterpart to the forward * `rx_wkv7_one` in rwkvx.c) plus a full-sequence forward used to drive it and * the gradient checks. The CE loop / base-unfreeze / full pretrain loop come * later and are intentionally NOT here. * * WKV7 inner recurrence (per head, per timestep; identical to rx_wkv7_one): * sa[i] = sum_j a[j] * S_prev[i][j] (a = nkk = -kk) * S[i][j] = S_prev[i][j]*w[j] + v[i]*k[j] + sa[i]*b[j] (b = kka = kk*iclr) * y[i] = sum_j S[i][j] * r[j] * State element convention matches foundry `s_d[(h*N+i)*N + j]`: row i is the * value/output dim, col j is the key dim. * * Backward equations transcribed from BlinkDL's Apache-2.0 CUDA * wkv7_cuda_fp32.cu backward_kernel via the verified fp64 PyTorch oracle * (Ternary_scale_1-10m/reference/wkv7/wkv7_reference.py). * * IMPORTANT chain-rule hand-offs (see wkv7_backward_reference.md §5): * 1. `dw` here is the grad w.r.t. the EFFECTIVE decay w_vec (already in (0,1)), * NOT the log-space pre-activation. We emit bare dw; the surrounding * time-mix backward chains it through exp(-0.606531*sigmoid(...)). We do * NOT apply the CUDA kernel's `dw*wi*wi_fac` factor. * 2. `da`/`db` are grads w.r.t. the composites nkk(=a) and kka(=b). The * recombination onto shared kk and a_vec (and a_vec's second path into * k_mod) is the time-mix backward's job, out of scope here. * 3. `dS` after the reverse scan is the grad w.r.t. the initial state carry * S_{-1}. Zero it for full-sequence-from-zero; it is the true exerciser * of the reverse scan and is checked via finite differences. */ #ifndef RWKVX_PRETRAIN_H #define RWKVX_PRETRAIN_H #include #ifdef __cplusplus extern "C" { #endif /* * Full-sequence WKV7 forward. Layout for the per-timestep vectors is * [B][T][H][N] flattened row-major: index = (((b*T + t)*H + h)*N + n). * State layout is [B][H][N][N]: index = (((b*H + h)*N + i)*N + j). * * s_init : initial state carry [B*H*N*N], or NULL for a zero start. * y_out : forward output [B*T*H*N], or NULL if not needed. * s_final: final state after T steps [B*H*N*N], or NULL if not needed. * * Returns 0 on success, -1 on allocation failure. */ int rx_wkv7_forward_seq( const float *r, const float *w, const float *k, const float *v, const float *a, const float *b, const float *s_init, float *y_out, float *s_final, int B, int T, int n_head, int head_size); /* * WKV7 backward-through-time (reverse scan). Same layouts as above. * * dy : upstream grad dL/dy [B*T*H*N]. * s_init : initial state carry used in the forward [B*H*N*N], or NULL (zero). * dr..db : output grads [B*T*H*N] each (dw is w.r.t. effective decay, see §5). * dS_out : output grad w.r.t. the initial state carry S_{-1} [B*H*N*N], or * NULL if training full-sequence-from-zero (grad discarded). * * The forward is recomputed internally and every pre-step state S_prev is * cached (correctness-first; the CUDA kernel reconstructs to save memory). * Returns 0 on success, -1 on allocation failure. */ int rx_wkv7_backward( const float *r, const float *w, const float *k, const float *v, const float *a, const float *b, const float *dy, const float *s_init, float *dr, float *dw, float *dk, float *dv, float *da, float *db, float *dS_out, int B, int T, int n_head, int head_size); /* ================================================================ * RWKV-7 TIME-MIX BLOCK backward (wraps the WKV7 backward above). * * This is the counterpart of rwkvx.c `rwkv7_time_mix_fast`: it covers the * WHOLE time-mix block (token-shift/lerp, the R/K/V/output projections, the * low-rank w/a/g/v LoRA gates, key L2-norm + key-modification, value-residual * gating, the WKV7 recurrence, group-norm, the r*k*r_k bonus and the g gate), * EXCLUDING the pre-block ln1 and the residual add. The block input is the * (already LN'd) hidden state; the output feeds the residual. * * It computes dL/d(block input) and grads for every learnable time-mix param, * internally recomputing the fp32 forward (caching activations) and delegating * the recurrence to rx_wkv7_backward. The two §5 hand-offs live here: * - `dw` (effective decay) is chained through exp(-0.606531*sigmoid(...)). * - `da`/`db` (grads w.r.t. nkk=-kk and kka=kk*a_vec) are recombined onto the * shared kk and a_vec (which also feeds k_mod). * * Layouts: block input x, output, dy, dx are [B][T][C] row-major (C=n_head* * head_size). Projection weights are row-major [in,out] exactly as * rwkvx.c's fast_matvec consumes them. v_first is [B][T][C] (per-token; for * layer_id==0 it is an OUTPUT copy of v and its grad is ignored). */ typedef struct { int c; /* n_head*head_size */ int n_head, head_size; int Dw, Da, Dg, Dv; /* LoRA inner dims (Dv ignored when layer_id==0) */ int layer_id; const float *x_r, *x_k, *x_v, *x_w, *x_a, *x_g; /* [c] lerps */ const float *receptance_w, *key_w, *value_w, *output_w; /* [c*c] [in,out] */ const float *w0, *w1, *w2; /* w0[c], w1[c*Dw], w2[Dw*c] */ const float *a0, *a1, *a2; /* a0[c], a1[c*Da], a2[Da*c] */ const float *g1, *g2; /* g1[c*Dg], g2[Dg*c] */ const float *v0, *v1, *v2; /* v0[c], v1[c*Dv], v2[Dv*c] (layer_id>0) */ const float *k_k, *k_a; /* [c] */ const float *r_k; /* [c] */ const float *ln_x_w, *ln_x_b;/* [c] group-norm affine */ } rx_tm_params; /* Grad accumulators — same shapes as rx_tm_params, non-const. Every pointer * must be caller-allocated; rx_timemix_backward ZEROES them at entry. Pointers * for params absent in the configuration (v0/v1/v2 at layer 0) may be NULL. */ typedef struct { float *x_r, *x_k, *x_v, *x_w, *x_a, *x_g; float *receptance_w, *key_w, *value_w, *output_w; float *w0, *w1, *w2; float *a0, *a1, *a2; float *g1, *g2; float *v0, *v1, *v2; float *k_k, *k_a; float *r_k; float *ln_x_w, *ln_x_b; } rx_tm_grads; /* fp32 time-mix forward over a full sequence (per-batch, T tokens). * x : block input [B*T*c] (LN'd hidden state) * x_prev_init : token-shift carry at t=0 [B*c], or NULL for zero. * v_first : [B*T*c]. layer_id==0: WRITTEN (copy of v). layer_id>0: READ. * out : block output [B*T*c] (pre-residual). * Returns 0 on success, -1 on allocation failure. */ int rx_timemix_forward_seq( const rx_tm_params *P, const float *x, const float *x_prev_init, float *v_first, float *out, int B, int T); /* Time-mix backward. dy is dL/d(out) [B*T*c]. Produces dx [B*T*c], all param * grads in G, and (optional) d_xprev_init [B*c] / d_v_first [B*T*c]. NULLs for * unused outputs. Returns 0 on success, -1 on allocation failure. */ int rx_timemix_backward( const rx_tm_params *P, const float *x, const float *x_prev_init, const float *v_first, const float *dy, rx_tm_grads *G, float *dx, float *d_xprev_init, float *d_v_first, int B, int T); /* ================================================================ * RWKV-7 CHANNEL-MIX (ReLU^2 FFN) backward. * * Counterpart of rwkvx.c `rwkv7_channel_mix_fast`: token-shift/lerp, the key * projection [C,ffn_dim], the squared-ReLU nonlinearity (k = relu(fk)^2), and * the value projection [ffn_dim,C]. EXCLUDING the pre-block ln2 and the * residual add (those live in the block). Input x is the (already LN'd) * hidden state [B][T][c]; output feeds the residual. * * Layouts match the time-mix block: x/out/dy/dx are [B][T][c] row-major. * ffn_key_w is row-major [c,ffn_dim]; ffn_value_w is [ffn_dim,c] (both as * fast_matvec consumes them). The token-shift carry (ffn_x_prev) at t=0 is * x_prev_init [B][c] or NULL for a zero start; its grad is d_xprev_init. */ typedef struct { int c; int ffn_dim; const float *ffn_x_k; /* [c] token-shift lerp */ const float *ffn_key_w; /* [c*ffn_dim] [in,out] */ const float *ffn_value_w; /* [ffn_dim*c] [in,out] */ } rx_cm_params; typedef struct { float *ffn_x_k; float *ffn_key_w; float *ffn_value_w; } rx_cm_grads; /* fp32 channel-mix forward over a full sequence. Returns 0 / -1(alloc). */ int rx_channelmix_forward_seq( const rx_cm_params *P, const float *x, const float *x_prev_init, float *out, int B, int T); /* Channel-mix backward. dy is dL/d(out) [B*T*c]. Produces dx [B*T*c], all * param grads in G (ZEROED at entry), and optional d_xprev_init [B*c]. * Returns 0 / -1(alloc). */ int rx_channelmix_backward( const rx_cm_params *P, const float *x, const float *x_prev_init, const float *dy, rx_cm_grads *G, float *dx, float *d_xprev_init, int B, int T); /* ================================================================ * LayerNorm backward (the block's ln1/ln2; foundry `fast_layer_norm`, * eps = 1e-5, normalised over the full C channel). Standard LN backward. * * ⚠ The mean/variance reductions cancel in fp32 exactly as the L2-norm did; * they are accumulated in DOUBLE locally so gamma/beta/dx stay clean. * * x/dy/dx are [B][T][C] row-major. dw/db are [C] and are ZEROED at entry * then accumulated across all B*T tokens. dx is written (=), not accumulated. * Returns 0 / -1(alloc). * ================================================================ */ int rx_layernorm_forward_seq( const float *x, const float *w, const float *b, float *out, int B, int T, int C); int rx_layernorm_backward( const float *x, const float *w, const float *b, const float *dy, float *dx, float *dw, float *db, int B, int T, int C); /* ================================================================ * RWKV-7 BLOCK forward/backward — the full residual block. * Mirrors rwkvx.c `rwkv7_block_forward_fast`: * x -> ln1 -> time-mix -> +x (residual) -> ln2 -> channel-mix -> +x1 * Backward walks it in reverse, threading residual grads to BOTH the block * path and the skip. Produces dL/d(block input) + every block param grad * (time-mix + channel-mix + ln1 + ln2). * ================================================================ */ typedef struct { rx_tm_params tm; rx_cm_params cm; const float *ln1_w, *ln1_b; /* [c] */ const float *ln2_w, *ln2_b; /* [c] */ } rx_block_params; typedef struct { rx_tm_grads tm; rx_cm_grads cm; float *ln1_w, *ln1_b, *ln2_w, *ln2_b; } rx_block_grads; /* Block forward. v_first [B*T*c]: WRITTEN at layer 0 (copy of v), READ at * layer>0. tm_xprev_init / cm_xprev_init are the two token-shift carries * [B*c] (over ln1-out and ln2-out respectively) or NULL for zero. Returns * 0 / -1(alloc). */ int rx_block_forward_seq( const rx_block_params *P, const float *x, const float *tm_xprev_init, const float *cm_xprev_init, float *v_first, float *out, int B, int T); /* Block backward. dy is dL/d(out) [B*T*c]. Produces dx [B*T*c], all grads in * G (sub-grads ZEROED by their sub-backwards; ln grads zeroed here), and the * optional carry / v_first grads. Returns 0 / -1(alloc). * * d_v_first is dual-purpose (see rx_timemix_backward): for a layer>0 block it is * an OUTPUT (grad w.r.t. the v_first input, zeroed then written); for the * layer-0 block (which PRODUCES v_first) it is an optional INPUT carrying the * accumulated downstream grad on v_first, injected into the value path. Pass * NULL when there is no v_first coupling (single RWKV layer / legacy). */ int rx_block_backward( const rx_block_params *P, const float *x, const float *tm_xprev_init, const float *cm_xprev_init, const float *v_first, const float *dy, rx_block_grads *G, float *dx, float *d_tm_xprev_init, float *d_cm_xprev_init, float *d_v_first, int B, int T); /* ================================================================ * SPARSE (MoBA-style) ATTENTION BLOCK forward/backward — BASE weights. * * Counterpart of rwkvx.c `attn_block_forward_fast` for the full residual * attention block (RWKV-x interleaves these with the RWKV-7 blocks above): * h1 = ln1(x) * q,k,v = h1 @ {Wq,Wk,Wv} (base projections, [c,c]) * att = MoBA-sparse causal-softmax attention(q,k,v) scale = 1/sqrt(head_size) * att_out = att @ Wo ; x1 = x + att_out (residual) * h2 = ln2(x1) * cm = channel-mix(h2) ; out = x1 + cm (residual) * * This computes dL/d(block input) and grads for EVERY learnable attention-block * base parameter (Q/K/V/O projections, ln1/ln2 affine, and the channel-mix * params) — the full base, not just the LoRA of rx_train.c. The softmax scale * is the fixed constant 1/sqrt(head_size) (not learnable); the forward has no * RoPE, so there is nothing to invert there. * * MoBA top-k CHUNK SELECTION IS HARD / NON-DIFFERENTIABLE. The selected chunk * set is treated as FIXED and the backward runs the standard sparse-attention * gradient over exactly the selected keys (keys of unselected chunks get * attention weight 0, so they drop out of every reduction automatically). For * a finite-difference check, perturbations must not flip the selection — test * in a regime where it is stable (e.g. moba_top_k >= #candidate chunks so all * are selected => plain causal attention). See tests/test_attn_bwd.c. * * Selection rule (per query t, per head): keys are partitioned into chunks of * `chunk_size`. The chunk containing t is always attended (causally, keys * s<=t). Each earlier complete chunk gets a gate = ; the * top `moba_top_k` earlier chunks are selected. moba_top_k <= 0 or * >= #candidate chunks both mean "keep all" (full causal). * ================================================================ */ typedef struct { int c; /* n_head*head_size */ int n_head, head_size; int chunk_size; /* MoBA chunk size (>=1) */ int moba_top_k; /* # earlier chunks kept; <=0 => keep all */ const float *ln1_w, *ln1_b; /* [c] pre-attention LN affine */ const float *receptance_w; /* [c*c] Q proj [in,out] */ const float *key_w; /* [c*c] K proj */ const float *value_w; /* [c*c] V proj */ const float *output_w; /* [c*c] O proj */ const float *ln2_w, *ln2_b; /* [c] pre-FFN LN affine */ rx_cm_params cm; /* channel-mix (ffn_x_k, ffn_key_w, ffn_value_w) */ } rx_attn_params; typedef struct { float *ln1_w, *ln1_b; float *receptance_w, *key_w, *value_w, *output_w; float *ln2_w, *ln2_b; rx_cm_grads cm; } rx_attn_grads; /* fp32 forward over a full sequence (per batch, causal, MoBA-sparse). * x : block input [B*T*c] * cm_xprev_init : channel-mix token-shift carry at t=0 [B*c] or NULL(zero) * out : block output [B*T*c] * Returns 0 / -1(alloc). */ int rx_attn_forward_seq( const rx_attn_params *P, const float *x, const float *cm_xprev_init, float *out, int B, int T); /* Attention-block backward with BASE-weight gradients. dy is dL/d(out) * [B*T*c]. Produces dx [B*T*c] and every param grad in G (all ZEROED at * entry), plus optional d_cm_xprev_init [B*c]. The forward is recomputed * internally; the MoBA selection is recomputed from the base weights and held * fixed through the attention backward. Returns 0 / -1(alloc). */ int rx_attn_backward_full( const rx_attn_params *P, const float *x, const float *cm_xprev_init, const float *dy, rx_attn_grads *G, float *dx, float *d_cm_xprev_init, int B, int T); /* Exported test hooks: the ISOLATED MoBA attention core (scores->softmax->AV), * mirrored on GPU in cuda_kernels/moba_attn.cu and parity-checked by * cuda_kernels/tests/test_moba_attn_cuda.cu. Inputs are the already-projected * per-token q,k,v [B*T*c]; forward fills softmax weights p [B*n_head*T*T] and * attention output y [B*T*c]; backward recomputes the MoBA selection + p from * q,k,v and accumulates dq,dk,dv [B*T*c] (all zeroed at entry) from dy=d(y). */ int rx_attn_score_av_forward(const rx_attn_params *P, const float *q, const float *k, const float *v, int B, int T, float *p, float *y); int rx_attn_score_av_backward(const rx_attn_params *P, const float *q, const float *k, const float *v, const float *dy, int B, int T, float *dq, float *dk, float *dv); /* ================================================================ * FULL RWKV-x MODEL: embedding, head/unembedding, cross-entropy, from-scratch * init, and the whole-model forward/backward assembly. Single sequence * (B=1); the underlying block kernels are called per-sequence. * * Architecture mirrors rwkvx.c rx_forward: * x0 = emb[token] (raw embedding; foundry fuses ln0 into * emb at load, so a from-scratch random * emb absorbs it — no separate ln0 here) * for each block (RWKV-7 and sparse-attention, interleaved per the RWKV-x * pattern — interval = n_rwkv/n_attn, attention after each RWKV group): * x = block(x) (residual stream) * h = ln_out(x) (final LayerNorm) * logits = h @ head_w ([C,vocab]; head is NOT weight-tied to * emb — foundry stores rwkv.head.weight * separately) * loss = mean_t CE(logits[t], target[t]) * * v_first coupling: the RWKV-7 block at rwkv-array index 0 produces v_first; * later RWKV-7 blocks consume it. The reverse pass accumulates the grad on * v_first from the consumers and injects it into producer block 0 via the * rx_timemix_backward d_v_first-as-input path (see above). * ================================================================ */ /* ---- Embedding (gather / scatter-add) ---- */ void rx_embedding_forward(const float *emb, const int *tokens, int T, int C, float *out); /* out[T*C] */ void rx_embedding_backward(const int *tokens, int T, int C, int vocab, const float *d_out, float *d_emb); /* d_emb ZEROED then scatter-add */ /* ---- Head / unembedding. head is [C,vocab] row-major (as fast_matvec eats). ---- */ void rx_head_forward(const float *x, const float *head, int T, int C, int vocab, float *logits); /* logits[T*vocab] */ void rx_head_backward(const float *x, const float *head, const float *dlogits, int T, int C, int vocab, float *dx, float *d_head); /* dx written(=); d_head ZEROED then accumulate */ /* ---- Fused log-softmax cross-entropy over the vocab. Mean over valid tokens * (targets != ignore_index; pass ignore_index<0 to disable). log-sum-exp is * double-accumulated for numerical cleanliness. ---- */ double rx_cross_entropy_forward(const float *logits, const int *targets, int T, int vocab, int ignore_index); /* Returns loss and writes dlogits = (softmax - onehot)/n_valid (ignored token * rows are zero). */ double rx_cross_entropy_backward(const float *logits, const int *targets, int T, int vocab, int ignore_index, float *dlogits); /* dlogits[T*vocab] */ /* ---- From-scratch model ---- */ typedef struct { int n_embd, n_head, head_size; /* head_size*n_head == n_embd */ int n_rwkv_layer, n_attn_layer; int vocab_size; int Dw, Da, Dg, Dv; /* time-mix LoRA inner dims */ int ffn_dim; /* channel-mix hidden dim */ int chunk_size, moba_top_k; /* attention MoBA config (>=candidates => full causal) */ } rx_pt_config; typedef struct rx_ptmodel rx_ptmodel; /* opaque; owns all params + grads */ /* Deterministic seeded init of ALL params (splitmix64 PRNG). Init scheme: * linear projections & LoRA matrices : uniform(-0.5,0.5) * 0.3 * token-shift / gate lerps, w0/a0/v0/k_k/k_a/r_k : uniform(-0.5,0.5) * 0.5 * LayerNorm / group-norm gamma : 1 + 0.2*uniform(-0.5,0.5) * LayerNorm / group-norm beta : 0.2*uniform(-0.5,0.5) * embedding : uniform(-0.5,0.5) * 0.5 * head : uniform(-0.5,0.5) * 0.3 * Small-scale, non-degenerate, keeps CE finite; a production trainer would * refine these. Returns NULL on bad config / alloc failure. */ rx_ptmodel* rx_model_new_random(const rx_pt_config *pc, uint64_t seed); void rx_ptmodel_free(rx_ptmodel *m); /* Introspection for the gradient check / reporting. */ int rx_ptmodel_num_params(const rx_ptmodel *m); /* Per-tensor accessor: name, mutable data, matching analytic grad, element count. */ int rx_ptmodel_param(rx_ptmodel *m, int i, const char **name, float **data, float **grad, int *n); rx_pt_config rx_ptmodel_config(const rx_ptmodel *m); /* Direct views onto a block's live (possibly-perturbed) params — for an fp64 * mirror / gradient check. idx is the index within the RWKV / attn array. */ const rx_block_params* rx_ptmodel_rwkv(rx_ptmodel *m, int idx); const rx_attn_params* rx_ptmodel_attn(rx_ptmodel *m, int idx); /* ---- Activation cache for backward ---- */ typedef struct { int T, C, vocab, n_blocks; int *tokens; /* [T] */ float *block_in; /* [(n_blocks+1)*T*C]: entry residual per block; [n_blocks]=final */ float *v_first; /* [T*C] */ float *lnout; /* [T*C] ln_out output (head input) */ float *logits; /* [T*vocab] */ } rx_pt_acts; rx_pt_acts* rx_pt_acts_new(const rx_ptmodel *m, int T); void rx_pt_acts_free(rx_pt_acts *a); /* Full forward: gather emb -> interleaved blocks -> ln_out -> head -> logits. * Fills acts (incl. logits). Returns 0 / -1(alloc). */ int rx_pretrain_forward(rx_ptmodel *m, const int *tokens, int T, rx_pt_acts *acts); /* Full backward: CE -> head -> ln_out -> reverse blocks (interleaved) -> * embedding scatter-add. Accumulates every param grad into the model's grad * buffers (zeroed by the sub-backwards / here) and returns the scalar loss. */ double rx_pretrain_backward(rx_ptmodel *m, const rx_pt_acts *acts, const int *targets, int T, int ignore_index); /* Zero all analytic grad buffers (call before a backward if reusing). */ void rx_ptmodel_zero_grad(rx_ptmodel *m); #ifdef __cplusplus } #endif #endif /* RWKVX_PRETRAIN_H */