/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ /* * test_model_bwd.c — WHOLE-MODEL gradient check for the full RWKV-x * forward+backward assembly (rx_pretrain_forward / rx_pretrain_backward). * This is the integration proof: embedding -> interleaved RWKV-7 + sparse- * attention blocks -> final LayerNorm -> head -> cross-entropy. * * Method (same discipline as test_block_bwd.c / test_attn_bwd.c): * - A DOUBLE-PRECISION mirror of the ENTIRE model forward -> CE loss is the * finite-difference reference (fp32 forward noise would swamp the small LN * / decay / a-b grads and false-fail). Every reduction (LN, softmax, * log-sum-exp, WKV state) is accumulated in double. * - The analytic backward runs the normal fp32 path (rx_pretrain_backward). * - Central finite differences, PER-TENSOR normalised infinity-norm metric * (max|g-fd| / max|fd|). Pass: < 1e-3 for EVERY parameter tensor. * - Large matrices are element-SUBSAMPLED (evenly spaced, capped) to keep the * whole-model FD tractable; small tensors are checked exhaustively. * * Toy configs carry BOTH block types (RWKV-x interleave) and, in the 2-RWKV * case, the v_first coupling (block-0 produces it, block-1 consumes it). * MoBA moba_top_k is set >= #candidate chunks => full causal attention, which * is also what foundry's rx_forward runs, so the assembly can be compared to it * (see test_model_forward_matches_rx_forward, guarded by RX_LINK_ENGINE). */ #include "lib/test.h" #include "rwkvx/rx_pretrain.h" #include #include #include #include #ifdef RX_LINK_ENGINE #include "rwkvx/rwkvx.h" /* Bridge implemented in rwkvx.c: build a real rx_model from the ptmodel. */ rx_model* rx_model_from_ptmodel(rx_ptmodel *pt); #endif /* ======================= fp64 model mirror ======================= */ static double dsig(double z){ return 1.0/(1.0+exp(-z)); } static void ln_fp64(const double *x, const float *w, const float *b, double *out, int C){ double mean=0.0; for(int i=0;i0. (Transcribed from test_block_bwd.c loss_fp64.) */ static void block_fwd_fp64(const rx_block_params *P, const double *x, double *vfirst, double *out, int T){ const rx_tm_params *tm=&P->tm; int C=tm->c,H=tm->n_head,N=tm->head_size; int Dw=tm->Dw,Da=tm->Da,Dg=tm->Dg,Dv=tm->Dv,F=P->cm.ffn_dim; int L0=(tm->layer_id==0); double *h1=malloc((size_t)T*C*sizeof(double)); double *tmo=malloc((size_t)T*C*sizeof(double)); double *x1=malloc((size_t)T*C*sizeof(double)); double *h2=malloc((size_t)T*C*sizeof(double)); double *S=calloc((size_t)H*N*N,sizeof(double)); double *sh=malloc(C*sizeof(double)),*shv=malloc(C*sizeof(double)); double *r=malloc(C*sizeof(double)),*k=malloc(C*sizeof(double)),*v=malloc(C*sizeof(double)); double *w=malloc(C*sizeof(double)),*a=malloc(C*sizeof(double)),*g=malloc(C*sizeof(double)); double *kk=malloc(C*sizeof(double)),*kmod=malloc(C*sizeof(double)),*vf=malloc(C*sizeof(double)); int maxD=Dw>Da?(Dw>Dg?Dw:Dg):(Da>Dg?Da:Dg); if(Dv>maxD)maxD=Dv; if(maxD<1)maxD=1; double *lo=malloc((size_t)(maxD+1)*sizeof(double)); double *lo2=malloc((size_t)((Dv>0?Dv:1))*sizeof(double)); double *y=malloc(C*sizeof(double)),*ygn=malloc(C*sizeof(double)); double *xd=malloc(C*sizeof(double)); double *fk=malloc((size_t)F*sizeof(double)); for (int t=0;tln1_w,P->ln1_b,&h1[base],C); } for (int e=0;ex_r); for(int j=0;jreceptance_w[i*C+j]; r[j]=s;} SHIFT(sh,tm->x_k); for(int j=0;jkey_w[i*C+j]; k[j]=s;} SHIFT(shv,tm->x_v); for(int j=0;jvalue_w[i*C+j]; v[j]=s;} SHIFT(sh,tm->x_w); for(int d=0;dw1[i*Dw+d]; lo[d]=tanh(s);} for(int j=0;jw2[d*C+j]; double sg=dsig((double)tm->w0[j]+s); w[j]=exp(-0.606531*sg);} SHIFT(sh,tm->x_a); for(int d=0;da1[i*Da+d]; lo[d]=s;} for(int j=0;ja2[d*C+j]; a[j]=dsig((double)tm->a0[j]+s);} SHIFT(sh,tm->x_g); for(int d=0;dg1[i*Dg+d]; lo[d]=dsig(s);} for(int j=0;jg2[d*C+j]; g[j]=s;} for(int i=0;ik_k[i]; for(int h=0;hk_a[i]); if (L0){ for(int i=0;iv1[i*Dv+d]; lo2[d]=s;} for(int j=0;jv2[d*C+j]; double bl=dsig((double)tm->v0[j]+s); vf[j]=v[j]+(vfirst[base+j]-v[j])*bl;} } for(int h=0;hln_x_w[off+i]+(double)tm->ln_x_b[off+i];} for(int h=0;hr_k[off+j]; for(int j=0;joutput_w[i*C+j]; tmo[base+j]=s;} #undef SHIFT } for (int t=0;tln2_w,P->ln2_b,&h2[base],C); } for (int t=0;tcm.ffn_x_k[i]; } for(int mm=0;mmcm.ffn_key_w[i*F+mm]; double rr=s>0.0?s:0.0; fk[mm]=rr*rr;} for(int j=0;jcm.ffn_value_w[mm*C+j]; out[base+j]=x1[base+j]+s;} } free(h1);free(tmo);free(x1);free(h2);free(S);free(sh);free(shv); free(r);free(k);free(v);free(w);free(a);free(g);free(kk);free(kmod);free(vf); free(lo);free(lo2);free(y);free(ygn);free(xd);free(fk); } /* Sparse-attention block fp64 forward: out[T*C]. (Transcribed from * test_attn_bwd.c loss_fp64; full causal when moba_top_k >= #candidates.) */ static void attn_fwd_fp64(const rx_attn_params *P, const double *x, double *out, int T){ int C=P->c,H=P->n_head,N=P->head_size,F=P->cm.ffn_dim; int cs=P->chunk_size<1?1:P->chunk_size; double scale=1.0/sqrt((double)N); double *h1=malloc((size_t)T*C*sizeof(double)); double *q=malloc((size_t)T*C*sizeof(double)); double *k=malloc((size_t)T*C*sizeof(double)); double *v=malloc((size_t)T*C*sizeof(double)); double *y=malloc((size_t)T*C*sizeof(double)); double *x1=malloc((size_t)T*C*sizeof(double)); double *h2=malloc((size_t)T*C*sizeof(double)); double *xd=malloc((size_t)C*sizeof(double)); double *sh=malloc((size_t)C*sizeof(double)); double *fk=malloc((size_t)F*sizeof(double)); for (int t=0;tln1_w,P->ln1_b,&h1[base],C); for(int j=0;jreceptance_w[i*C+j]; sk+=hv*P->key_w[i*C+j]; sv+=hv*P->value_w[i*C+j]; } q[base+j]=sq; k[base+j]=sk; v[base+j]=sv; } } for (int h=0;h0?T:1),1); int keepall=(P->moba_top_k<=0 || P->moba_top_k>=ncand); if (keepall){ for(int cc=0;cc0?T:1)*sizeof(double)); for(int c2=0;c2moba_top_k;it++){ int arg=-1; double best=-1e300; for(int c2=0;c2best){best=gate[c2];arg=c2;} if(arg<0)break; csel[arg]=1; } free(gate); } double *sc=malloc((size_t)(T>0?T:1)*sizeof(double)); double maxs=-1e300; for(int s=0;s<=tq;s++){ int c2=s/cs; int inc=(c2==local_chunk)||(c2maxs)maxs=d; } double sum=0; for(int s=0;s<=tq;s++){ if(sc[s]<=-1e299)continue; double e=exp(sc[s]-maxs); sc[s]=e; sum+=e; } for(int j=0;joutput_w[i*C+j]; x1[base+j]=x[base+j]+s; } ln_fp64(&x1[base],P->ln2_w,P->ln2_b,&h2[base],C); } for (int t=0;tcm.ffn_x_k[i]; } for(int mm=0;mmcm.ffn_key_w[i*F+mm]; double rr=s>0.0?s:0.0; fk[mm]=rr*rr; } for(int j=0;jcm.ffn_value_w[mm*C+j]; out[base+j]=x1[base+j]+s; } } free(h1);free(q);free(k);free(v);free(y);free(x1);free(h2);free(xd);free(sh);free(fk); } /* Whole-model fp64 forward -> mean CE loss. Reads (possibly perturbed) weights * straight from the model's parameter views. logits_out optional [T*vocab]. */ static double model_loss_fp64(rx_ptmodel *m, const int *tokens, const int *targets, int T, int ignore, double *logits_out){ rx_pt_config pc=rx_ptmodel_config(m); int C=pc.n_embd, V=pc.vocab_size; int n_blocks=pc.n_rwkv_layer+pc.n_attn_layer; /* rebuild the interleave order (same rule as rx_model_new_random) */ int *btype=malloc(n_blocks*sizeof(int)), *bidx=malloc(n_blocks*sizeof(int)); if (pc.n_attn_layer==0){ for(int i=0;i=0 && tg==ignore)){ double mx=-1e300; for(int j=0;jmx)mx=lg[j]; double se=0; for(int j=0;j0?loss/nv:0.0; } /* ======================= FD driver ======================= */ #define FD_CAP 40 /* max element samples per tensor */ static float fd_tensor(rx_ptmodel *m, const int *tok, const int *tgt, int T, int ignore, float *data, const float *grad, int n, const char *nm, int verbose){ int cap = n=n) break; float o=data[e]; float hh=1e-3f*(1.0f+fabsf(o)); data[e]=o+hh; double Lp=model_loss_fp64(m,tok,tgt,T,ignore,NULL); data[e]=o-hh; double Lm=model_loss_fp64(m,tok,tgt,T,ignore,NULL); data[e]=o; float fd=(float)((Lp-Lm)/(2.0*(double)hh)); float d=fabsf(grad[e]-fd); if (d>maxdiff) maxdiff=d; if (fabsf(fd)>maxfd) maxfd=fabsf(fd); if (fabsf(grad[e])>maxg) maxg=fabsf(grad[e]); } float re=maxdiff/(maxfd+1e-9f); if (verbose) printf(" %-22s rel=%.2e (max|g-fd|=%.2e max|fd|=%.2e)\n", nm,re,maxdiff,maxfd); return re; } static int run_case(const char *name, rx_pt_config pc, uint64_t seed, int T){ printf(" [%s] C=%d H=%d N=%d rwkv=%d attn=%d vocab=%d F=%d " "Dw=%d Da=%d Dg=%d Dv=%d T=%d chunk=%d topk=%d\n", name, pc.n_embd,pc.n_head,pc.head_size,pc.n_rwkv_layer,pc.n_attn_layer, pc.vocab_size,pc.ffn_dim,pc.Dw,pc.Da,pc.Dg,pc.Dv,T,pc.chunk_size,pc.moba_top_k); rx_ptmodel *m=rx_model_new_random(&pc,seed); if (!m){ printf(" model_new_random FAILED\n"); return 0; } /* tokens + targets */ int *tok=malloc(T*sizeof(int)), *tgt=malloc(T*sizeof(int)); srand((unsigned)seed); for (int t=0;tlogits[(size_t)t*pc.vocab_size+j]; float d=fabsf(a-(float)lg64[(size_t)t*pc.vocab_size+j]); if (d>maxlog) maxlog=d; if (fabsf(a)>reflog) reflog=fabsf(a); } printf(" fp32-vs-fp64 logits: rel=%.2e (max|d|=%.2e max|logit|=%.2e)\n", maxlog/(reflog+1e-9f), maxlog, reflog); /* sanity: the assembly forward must match foundry's rx_forward on identical * weights (full-causal attention == topk>=candidates). */ int fwd_ok = 1; #ifdef RX_LINK_ENGINE { rx_model *rm = rx_model_from_ptmodel(m); rx_state *st = rx_state_new(rm); float maxd=0.0f, maxref=0.0f; for (int t=0;tlogits[(size_t)t*pc.vocab_size+j]; float d=fabsf(a-ld[j]); if(d>maxd)maxd=d; if(fabsf(a)>maxref)maxref=fabsf(a); } tm_free(lg); } rx_state_free(st); rx_model_free(rm); float rel=maxd/(maxref+1e-9f); printf(" assembly-vs-rx_forward logits: rel=%.2e (max|d|=%.2e)\n", rel, maxd); fwd_ok = rel < 5e-3f; if (!fwd_ok) printf(" rx_forward MISMATCH\n"); } #else printf(" assembly-vs-rx_forward: (skipped; build with RX_LINK_ENGINE)\n"); #endif /* analytic backward */ double loss=rx_pretrain_backward(m,acts,tgt,T,-1); int finite = isfinite(loss) && isfinite(loss64); printf(" loss(fp32 CE)=%.6f loss(fp64 mirror)=%.6f finite=%s\n", loss, loss64, finite?"yes":"NO"); /* per-tensor FD check */ float wmax=0.0f; const char *wname=""; int np=rx_ptmodel_num_params(m); for (int i=0;iwmax){ wmax=e; wname=nm; } } int pass = wmax<1e-3f && finite && fwd_ok; printf(" -> worst rel-err = %.2e (%s) %s\n", wmax, wname, pass?"PASS":"FAIL"); free(lg64); free(tok); free(tgt); rx_pt_acts_free(acts); rx_ptmodel_free(m); return pass; } static void test_model_backward(void){ printf(" Whole-model RWKV-x forward+backward — fp64 finite differences:\n"); int ok=1; /* Toy A: 1 RWKV + 1 sparse-attention (the minimal RWKV-x interleave). */ ok &= run_case("1R+1A", (rx_pt_config){ .n_embd=64,.n_head=2,.head_size=32,.n_rwkv_layer=1,.n_attn_layer=1, .vocab_size=48,.Dw=8,.Da=8,.Dg=8,.Dv=8,.ffn_dim=32, .chunk_size=2,.moba_top_k=999}, 1234, 7); /* Toy B: 2 RWKV + 1 sparse-attention — exercises v_first coupling * (block-0 produces, block-1 consumes) AND the interleave [R0,R1,A0]. */ ok &= run_case("2R+1A", (rx_pt_config){ .n_embd=48,.n_head=3,.head_size=16,.n_rwkv_layer=2,.n_attn_layer=1, .vocab_size=40,.Dw=6,.Da=6,.Dg=6,.Dv=6,.ffn_dim=24, .chunk_size=2,.moba_top_k=999}, 777, 6); ASSERT_TRUE(ok); } int main(void){ printf("\nWhole-model RWKV-x gradient check (from-scratch trainer)\n"); RUN_TEST(model_backward); TEST_REPORT(); return __ft_failed>0 ? 1 : 0; }