/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ /* * test_timemix_bwd.c — Gradient-check for the RWKV-7 TIME-MIX BLOCK backward * (rx_timemix_backward), the counterpart of rwkvx.c `rwkv7_time_mix_fast`. * * Method (docs/wkv7_backward_reference.md §5/§6, "use an fp64 reference"): * - A DOUBLE-PRECISION mirror of the time-mix forward computes the FD * reference loss L = sum(cc * out). fp32 forward noise would swamp the * small a/b/decay-related grads, so the reference forward is fp64. * - The analytic backward runs in the normal fp32 path (rx_timemix_backward). * - Central finite differences per-parameter and for the block input. * Pass: max relative error < 1e-3 for EVERY parameter grad and the input. * * Prints per-parameter rel-err so a miss is localised. Several shapes are * exercised (T, heads, head-dim, batch) plus a layer>0 case (value-residual * gating path) and a non-trivial T so token-shift + the WKV recurrence run. */ #include "lib/test.h" #include "rwkvx/rx_pretrain.h" #include #include #include /* ---------- config + parameter bag ---------- */ typedef struct { int B, T, H, N, C, Dw, Da, Dg, Dv, layer_id; } cfg; /* Every learnable time-mix tensor, fp32. Sizes derived from cfg. */ 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, *a0,*a1,*a2, *g1,*g2, *v0,*v1,*v2; float *k_k,*k_a,*r_k,*ln_x_w,*ln_x_b; } TMP; 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; 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); /* group-norm affine: weight near 1, bias small */ 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(); } static void tmp_free(TMP *m){ float **a=(float**)m; for (size_t i=0;ic=cf->C; P->n_head=cf->H; P->head_size=cf->N; P->Dw=cf->Dw; P->Da=cf->Da; P->Dg=cf->Dg; P->Dv=cf->Dv; P->layer_id=cf->layer_id; P->x_r=m->x_r;P->x_k=m->x_k;P->x_v=m->x_v;P->x_w=m->x_w;P->x_a=m->x_a;P->x_g=m->x_g; P->receptance_w=m->receptance_w;P->key_w=m->key_w;P->value_w=m->value_w;P->output_w=m->output_w; P->w0=m->w0;P->w1=m->w1;P->w2=m->w2;P->a0=m->a0;P->a1=m->a1;P->a2=m->a2; P->g1=m->g1;P->g2=m->g2;P->v0=m->v0;P->v1=m->v1;P->v2=m->v2; P->k_k=m->k_k;P->k_a=m->k_a;P->r_k=m->r_k;P->ln_x_w=m->ln_x_w;P->ln_x_b=m->ln_x_b; } /* ---------- fp64 reference forward + scalar loss ---------- */ static double dsig(double z){ return 1.0/(1.0+exp(-z)); } static double loss_fp64(const TMP *m, const cfg *cf, const float *x, const float *x_prev_init, const float *v_first, const float *cc) { int B=cf->B,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; double L=0.0; double *S=calloc((size_t)H*N*N,sizeof(double)); double *sh=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)); double *lo=malloc((size_t)(Dw>Da?(Dw>Dg?Dw:Dg):(Da>Dg?Da:Dg))*sizeof(double)+64); double *lo2=malloc((size_t)(Dv>0?Dv:1)*sizeof(double)+64); double *y=malloc(C*sizeof(double)),*ygn=malloc(C*sizeof(double)),*tg=malloc(C*sizeof(double)); 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;} double shv[4096]; SHIFT(shv,m->x_v); for(int j=0;jvalue_w[i*C+j]; v[j]=s;} /* decay */ 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);} /* gate a */ 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);} /* gate g */ 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;} /* key norm (per head) */ for(int i=0;ik_k[i]; for(int h=0;hk_a[i]); /* value residual */ 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;} } /* WKV7 recurrence (double) */ for(int h=0;hln_x_w[off+i]+(double)m->ln_x_b[off+i];} /* bonus */ for(int h=0;hr_k[off+j]; for(int j=0;joutput_w[i*C+j]; L+=(double)cc[base+j]*s;} #undef SHIFT } } free(S);free(sh);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); return L; } /* FD-check a parameter/input array (nv elements) vs analytic grad g. */ static float fd_array(float *arr, const float *g, int nv, const TMP *m, const cfg *cf, const float *x, const float *xpi, const float *vfst, const float *cc, const char *nm, int verbose) { /* Per-tensor relative error (standard gradcheck metric): the max absolute * analytic-vs-FD mismatch, normalised by the tensor's gradient scale. A * per-ELEMENT relative error is meaningless for the many near-zero entries * of the C×C weight matrices (a ~1e-7 absolute mismatch over a ~1e-5 entry * reads as ~1e-2 while g and fd still agree to 3-4 sig figs). */ 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); int C=cf.C, B=cf.B, T=cf.T; cf.C = C = cf.H*cf.N; size_t tot=(size_t)B*T*C; TMP m; tmp_alloc(&m,&cf); rx_tm_params P; tmp_to_params(&m,&cf,&P); float *x=rbuf((int)tot,1.0f); float *xpi=rbuf(B*C,1.0f); float *vfst=rbuf((int)tot,0.6f); /* external v_first (layer>0) */ float *cc=rbuf((int)tot,1.0f); /* dL/dout weights */ float *dy=malloc(tot*sizeof(float)); for (size_t i=0;i dy = cc */ /* analytic grads */ TMP 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); #undef GA rx_tm_grads G; G.x_r=gm.x_r;G.x_k=gm.x_k;G.x_v=gm.x_v;G.x_w=gm.x_w;G.x_a=gm.x_a;G.x_g=gm.x_g; G.receptance_w=gm.receptance_w;G.key_w=gm.key_w;G.value_w=gm.value_w;G.output_w=gm.output_w; G.w0=gm.w0;G.w1=gm.w1;G.w2=gm.w2;G.a0=gm.a0;G.a1=gm.a1;G.a2=gm.a2; G.g1=gm.g1;G.g2=gm.g2;G.v0=gm.v0;G.v1=gm.v1;G.v2=gm.v2; G.k_k=gm.k_k;G.k_a=gm.k_a;G.r_k=gm.r_k;G.ln_x_w=gm.ln_x_w;G.ln_x_b=gm.ln_x_b; float *dx=calloc(tot,sizeof(float)); float *dvf=cf.layer_id>0?calloc(tot,sizeof(float)):NULL; rx_timemix_backward(&P,x,xpi,vfst,dy,&G,dx,NULL,dvf,B,T); printf(" [%s] B=%d T=%d H=%d N=%d C=%d layer=%d\n", name,B,T,cf.H,cf.N,C,cf.layer_id); float wmax=0.0f; #define CK(f,n) do{ float e=fd_array(m.f,gm.f,(n),&m,&cf,x,xpi,vfst,cc,#f,1); \ if(e>wmax)wmax=e; }while(0) 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 /* block input */ { float e=fd_array(x,dx,(int)tot,&m,&cf,x,xpi,vfst,cc,"dx(input)",1); if(e>wmax)wmax=e; } printf(" -> worst rel-err = %.2e (%s)\n", wmax, wmax<1e-3f?"PASS":"FAIL"); /* cleanup */ tmp_free(&m); tmp_free(&gm); free(x);free(xpi);free(vfst);free(cc);free(dy);free(dx); free(dvf); return wmax<1e-3f; } static void test_timemix_backward(void){ printf(" Time-mix block backward — fp64 finite differences:\n"); int ok=1; /* layer 0, small multi-head, non-trivial T */ 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}, 11); /* layer>0: exercises value-residual gating path */ 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}, 23); /* batch>1, more heads, bigger head-dim */ 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}, 37); ASSERT_TRUE(ok); } int main(void){ printf("\nRWKV-7 time-mix block backward gradient check\n"); RUN_TEST(timemix_backward); TEST_REPORT(); return __ft_failed>0 ? 1 : 0; }