/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ /* * test_block_bwd.c — Gradient-check for the FULL RWKV-7 BLOCK backward * (rx_block_backward), the counterpart of rwkvx.c `rwkv7_block_forward_fast`: * x -> ln1 -> time-mix -> +x -> ln2 -> channel-mix -> +x1 * * Method (same discipline as test_timemix_bwd.c / docs §5-§6): * - A DOUBLE-PRECISION mirror of the whole block computes the FD reference * loss L = sum(cc * out). fp32 forward noise would swamp the small * a/b/decay/LN grads and false-fail, so the reference is fp64. * - The analytic backward runs the normal fp32 path (rx_block_backward). * - Central finite differences, PER-TENSOR normalised infinity-norm metric * (max|g-fd| / max|fd|). Pass: < 1e-3 for EVERY tensor and the block dx. * * Focus of this chunk: the channel-mix params (ffn_x_k, ffn_key_w, * ffn_value_w), the two LayerNorms (ln1/ln2 gamma/beta) and the whole-block * dx (residual grad threading). The time-mix + WKV7 params are re-checked * for free since the block exercises them end-to-end. */ #include "lib/test.h" #include "rwkvx/rx_pretrain.h" #include #include #include typedef struct { int B, T, H, N, C, Dw, Da, Dg, Dv, layer_id, F; /* F = ffn_dim */ } cfg; /* Every learnable tensor in a block, fp32. */ typedef struct { /* time-mix */ 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, *a0,*a1,*a2, *g1,*g2, *v0,*v1,*v2; float *k_k,*k_a,*r_k,*ln_x_w,*ln_x_b; /* channel-mix */ float *ffn_x_k,*ffn_key_w,*ffn_value_w; /* block LNs */ float *ln1_w,*ln1_b,*ln2_w,*ln2_b; } BP; static float frand(void){ return (float)rand()/(float)RAND_MAX - 0.5f; } static float *rbuf(int n, float scale){ float *p=malloc((size_t)n*sizeof(float)); for (int i=0;iC,Dw=cf->Dw,Da=cf->Da,Dg=cf->Dg,Dv=cf->Dv,F=cf->F; m->x_r=rbuf(C,1.0f); m->x_k=rbuf(C,1.0f); m->x_v=rbuf(C,1.0f); m->x_w=rbuf(C,1.0f); m->x_a=rbuf(C,1.0f); m->x_g=rbuf(C,1.0f); m->receptance_w=rbuf(C*C,0.3f); m->key_w=rbuf(C*C,0.3f); m->value_w=rbuf(C*C,0.3f); m->output_w=rbuf(C*C,0.3f); m->w0=rbuf(C,0.5f); m->w1=rbuf(C*Dw,0.3f); m->w2=rbuf(Dw*C,0.3f); m->a0=rbuf(C,0.5f); m->a1=rbuf(C*Da,0.3f); m->a2=rbuf(Da*C,0.3f); m->g1=rbuf(C*Dg,0.3f); m->g2=rbuf(Dg*C,0.3f); if (cf->layer_id>0){ m->v0=rbuf(C,0.5f); m->v1=rbuf(C*Dv,0.3f); m->v2=rbuf(Dv*C,0.3f); } else { m->v0=m->v1=m->v2=NULL; } m->k_k=rbuf(C,0.5f); m->k_a=rbuf(C,0.5f); m->r_k=rbuf(C,0.5f); m->ln_x_w=malloc((size_t)C*sizeof(float)); m->ln_x_b=rbuf(C,0.2f); for (int i=0;iln_x_w[i]=1.0f+0.2f*frand(); /* channel-mix */ m->ffn_x_k=rbuf(C,1.0f); m->ffn_key_w=rbuf(C*F,0.3f); m->ffn_value_w=rbuf(F*C,0.3f); /* block LNs: gamma near 1, beta small */ m->ln1_w=malloc((size_t)C*sizeof(float)); m->ln1_b=rbuf(C,0.2f); m->ln2_w=malloc((size_t)C*sizeof(float)); m->ln2_b=rbuf(C,0.2f); for (int i=0;iln1_w[i]=1.0f+0.2f*frand(); m->ln2_w[i]=1.0f+0.2f*frand(); } } static void bp_free(BP *m){ float **a=(float**)m; for (size_t i=0;itm; tm->c=cf->C; tm->n_head=cf->H; tm->head_size=cf->N; tm->Dw=cf->Dw; tm->Da=cf->Da; tm->Dg=cf->Dg; tm->Dv=cf->Dv; tm->layer_id=cf->layer_id; tm->x_r=m->x_r;tm->x_k=m->x_k;tm->x_v=m->x_v;tm->x_w=m->x_w;tm->x_a=m->x_a;tm->x_g=m->x_g; tm->receptance_w=m->receptance_w;tm->key_w=m->key_w;tm->value_w=m->value_w;tm->output_w=m->output_w; tm->w0=m->w0;tm->w1=m->w1;tm->w2=m->w2;tm->a0=m->a0;tm->a1=m->a1;tm->a2=m->a2; tm->g1=m->g1;tm->g2=m->g2;tm->v0=m->v0;tm->v1=m->v1;tm->v2=m->v2; tm->k_k=m->k_k;tm->k_a=m->k_a;tm->r_k=m->r_k;tm->ln_x_w=m->ln_x_w;tm->ln_x_b=m->ln_x_b; P->cm.c=cf->C; P->cm.ffn_dim=cf->F; P->cm.ffn_x_k=m->ffn_x_k; P->cm.ffn_key_w=m->ffn_key_w; P->cm.ffn_value_w=m->ffn_value_w; P->ln1_w=m->ln1_w;P->ln1_b=m->ln1_b;P->ln2_w=m->ln2_w;P->ln2_b=m->ln2_b; } /* ---------- fp64 reference forward + scalar loss ---------- */ static double dsig(double z){ return 1.0/(1.0+exp(-z)); } /* fp64 LayerNorm over C (eps=1e-5). */ 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;iB,T=cf->T,H=cf->H,N=cf->N,C=cf->C; int Dw=cf->Dw,Da=cf->Da,Dg=cf->Dg,Dv=cf->Dv,F=cf->F; double L=0.0; size_t tot=(size_t)B*T*C; double *h1=malloc(tot*sizeof(double)); /* ln1(x) */ double *tm=malloc(tot*sizeof(double)); /* time-mix out */ double *x1=malloc(tot*sizeof(double)); double *h2=malloc(tot*sizeof(double)); /* ln2(x1) */ 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; 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)),*tg=malloc(C*sizeof(double)); double *xd=malloc(C*sizeof(double)); double *fk=malloc((size_t)F*sizeof(double)); /* h1 = ln1(x), per token */ for (int b=0;bln1_w,m->ln1_b,&h1[base],C); } /* time-mix over h1 -> tm (sequential state per batch) */ for (int b=0;bx_r); for(int j=0;jreceptance_w[i*C+j]; r[j]=s;} SHIFT(sh,m->x_k); for(int j=0;jkey_w[i*C+j]; k[j]=s;} SHIFT(shv,m->x_v); for(int j=0;jvalue_w[i*C+j]; v[j]=s;} SHIFT(sh,m->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)m->w0[j]+s); w[j]=exp(-0.606531*sg);} SHIFT(sh,m->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)m->a0[j]+s);} SHIFT(sh,m->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 (cf->layer_id==0){ for(int i=0;iv1[i*Dv+d]; lo2[d]=s;} for(int j=0;jv2[d*C+j]; double bl=dsig((double)m->v0[j]+s); vf[j]=v[j]+((double)v_first[base+j]-v[j])*bl;} } for(int h=0;hln_x_w[off+i]+(double)m->ln_x_b[off+i];} for(int h=0;hr_k[off+j]; for(int j=0;joutput_w[i*C+j]; tm[base+j]=s;} #undef SHIFT } } /* x1 = x + tm ; h2 = ln2(x1) */ for (int b=0;bln2_w,m->ln2_b,&h2[base],C); } /* channel-mix over h2 -> cm ; out = x1 + cm ; L += cc*out */ for (int b=0;bffn_x_k[i]; } for(int mm=0;mmffn_key_w[i*F+mm]; double rr=s>0.0?s:0.0; fk[mm]=rr*rr;} for(int j=0;jffn_value_w[mm*C+j]; double out=x1[base+j]+s; L+=(double)cc[base+j]*out;} } free(h1);free(tm);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(tg);free(xd);free(fk); return L; } /* FD-check an array (nv elems) vs analytic grad g — per-tensor rel-err. */ static float fd_array(float *arr, const float *g, int nv, const BP *m, const cfg *cf, const float *x, const float *tmxp, const float *cmxp, const float *vfst, const float *cc, const char *nm, int verbose) { float maxdiff=0.0f, maxfd=0.0f; for (int e=0;emaxdiff) maxdiff=d; if (fabsf(fd)>maxfd) maxfd=fabsf(fd); } float re=maxdiff/(maxfd+1e-9f); if (verbose) printf(" %-14s rel=%.2e (max|g-fd|=%.2e max|fd|=%.2e)\n", nm,re,maxdiff,maxfd); return re; } static int run_case(const char *name, cfg cf, unsigned seed){ srand(seed); cf.C = cf.H*cf.N; int C=cf.C, B=cf.B, T=cf.T; size_t tot=(size_t)B*T*C; BP m; bp_alloc(&m,&cf); rx_block_params P; bp_to_params(&m,&cf,&P); float *x=rbuf((int)tot,1.0f); float *vfst=rbuf((int)tot,0.6f); /* external v_first (layer>0) */ float *cc=rbuf((int)tot,1.0f); float *dy=malloc(tot*sizeof(float)); for (size_t i=0;i dy = cc */ /* analytic grads */ BP gm; memset(&gm,0,sizeof gm); #define GA(f,n) gm.f=calloc((size_t)(n),sizeof(float)) GA(x_r,C);GA(x_k,C);GA(x_v,C);GA(x_w,C);GA(x_a,C);GA(x_g,C); GA(receptance_w,C*C);GA(key_w,C*C);GA(value_w,C*C);GA(output_w,C*C); GA(w0,C);GA(w1,C*cf.Dw);GA(w2,cf.Dw*C); GA(a0,C);GA(a1,C*cf.Da);GA(a2,cf.Da*C); GA(g1,C*cf.Dg);GA(g2,cf.Dg*C); if (cf.layer_id>0){ GA(v0,C);GA(v1,C*cf.Dv);GA(v2,cf.Dv*C); } GA(k_k,C);GA(k_a,C);GA(r_k,C);GA(ln_x_w,C);GA(ln_x_b,C); GA(ffn_x_k,C);GA(ffn_key_w,C*cf.F);GA(ffn_value_w,cf.F*C); GA(ln1_w,C);GA(ln1_b,C);GA(ln2_w,C);GA(ln2_b,C); #undef GA rx_block_grads G; memset(&G,0,sizeof G); rx_tm_grads *tg=&G.tm; tg->x_r=gm.x_r;tg->x_k=gm.x_k;tg->x_v=gm.x_v;tg->x_w=gm.x_w;tg->x_a=gm.x_a;tg->x_g=gm.x_g; tg->receptance_w=gm.receptance_w;tg->key_w=gm.key_w;tg->value_w=gm.value_w;tg->output_w=gm.output_w; tg->w0=gm.w0;tg->w1=gm.w1;tg->w2=gm.w2;tg->a0=gm.a0;tg->a1=gm.a1;tg->a2=gm.a2; tg->g1=gm.g1;tg->g2=gm.g2;tg->v0=gm.v0;tg->v1=gm.v1;tg->v2=gm.v2; tg->k_k=gm.k_k;tg->k_a=gm.k_a;tg->r_k=gm.r_k;tg->ln_x_w=gm.ln_x_w;tg->ln_x_b=gm.ln_x_b; G.cm.ffn_x_k=gm.ffn_x_k;G.cm.ffn_key_w=gm.ffn_key_w;G.cm.ffn_value_w=gm.ffn_value_w; G.ln1_w=gm.ln1_w;G.ln1_b=gm.ln1_b;G.ln2_w=gm.ln2_w;G.ln2_b=gm.ln2_b; float *dx=calloc(tot,sizeof(float)); float *dvf=cf.layer_id>0?calloc(tot,sizeof(float)):NULL; rx_block_backward(&P,x,NULL,NULL,vfst,dy,&G,dx,NULL,NULL,dvf,B,T); printf(" [%s] B=%d T=%d H=%d N=%d C=%d F=%d layer=%d\n", name,B,T,cf.H,cf.N,C,cf.F,cf.layer_id); float wmax=0.0f; #define CK(f,n) do{ float e=fd_array(m.f,gm.f,(n),&m,&cf,x,NULL,NULL,vfst,cc,#f,1); \ if(e>wmax)wmax=e; }while(0) /* channel-mix + block LNs (the new pieces) */ CK(ffn_x_k,C);CK(ffn_key_w,C*cf.F);CK(ffn_value_w,cf.F*C); CK(ln1_w,C);CK(ln1_b,C);CK(ln2_w,C);CK(ln2_b,C); /* time-mix params (re-checked end-to-end through the block) */ CK(x_r,C);CK(x_k,C);CK(x_v,C);CK(x_w,C);CK(x_a,C);CK(x_g,C); CK(receptance_w,C*C);CK(key_w,C*C);CK(value_w,C*C);CK(output_w,C*C); CK(w0,C);CK(w1,C*cf.Dw);CK(w2,cf.Dw*C); CK(a0,C);CK(a1,C*cf.Da);CK(a2,cf.Da*C); CK(g1,C*cf.Dg);CK(g2,cf.Dg*C); if (cf.layer_id>0){ CK(v0,C);CK(v1,C*cf.Dv);CK(v2,cf.Dv*C); } CK(k_k,C);CK(k_a,C);CK(r_k,C);CK(ln_x_w,C);CK(ln_x_b,C); #undef CK { float e=fd_array(x,dx,(int)tot,&m,&cf,x,NULL,NULL,vfst,cc,"dx(block in)",1); if(e>wmax)wmax=e; } printf(" -> worst rel-err = %.2e (%s)\n", wmax, wmax<1e-3f?"PASS":"FAIL"); bp_free(&m); bp_free(&gm); free(x);free(vfst);free(cc);free(dy);free(dx);free(dvf); return wmax<1e-3f; } static void test_block_backward(void){ printf(" RWKV-7 full block backward — fp64 finite differences:\n"); int ok=1; ok &= run_case("layer0 base", (cfg){.B=1,.T=5,.H=2,.N=4,.Dw=3,.Da=3,.Dg=3,.Dv=0,.layer_id=0,.F=12}, 11); ok &= run_case("layer1 vres", (cfg){.B=1,.T=6,.H=2,.N=4,.Dw=3,.Da=3,.Dg=3,.Dv=3,.layer_id=1,.F=16}, 23); ok &= run_case("batch heads", (cfg){.B=2,.T=4,.H=3,.N=6,.Dw=4,.Da=3,.Dg=5,.Dv=0,.layer_id=0,.F=20}, 37); ASSERT_TRUE(ok); } int main(void){ printf("\nRWKV-7 full block backward gradient check\n"); RUN_TEST(block_backward); TEST_REPORT(); return __ft_failed>0 ? 1 : 0; }