418 lines
21 KiB
C
418 lines
21 KiB
C
/* 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 <math.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
|
|
#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;i<C;i++) mean+=x[i]; mean/=C;
|
|
double var=0.0; for(int i=0;i<C;i++){double d=x[i]-mean;var+=d*d;} var/=C;
|
|
double rstd=1.0/sqrt(var+1e-5);
|
|
for(int i=0;i<C;i++) out[i]=(x[i]-mean)*rstd*(double)w[i]+(double)b[i];
|
|
}
|
|
|
|
/* RWKV-7 block fp64 forward: out[T*C]. vfirst[T*C] WRITTEN at layer 0 (=v),
|
|
* READ at layer>0. (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;t<T;t++){ size_t base=(size_t)t*C;
|
|
for(int i=0;i<C;i++) xd[i]=x[base+i];
|
|
ln_fp64(xd,P->ln1_w,P->ln1_b,&h1[base],C);
|
|
}
|
|
for (int e=0;e<H*N*N;e++) S[e]=0.0;
|
|
for (int t=0;t<T;t++){
|
|
size_t base=(size_t)t*C;
|
|
const double *xx=&h1[base];
|
|
const double *prev=(t==0)?NULL:&h1[base-C];
|
|
#define SHIFT(dst,lerp) for(int i=0;i<C;i++){ \
|
|
double pv=prev?prev[i]:0.0; dst[i]=xx[i]+(pv-xx[i])*(double)lerp[i]; }
|
|
SHIFT(sh,tm->x_r); for(int j=0;j<C;j++){double s=0;for(int i=0;i<C;i++)s+=sh[i]*tm->receptance_w[i*C+j]; r[j]=s;}
|
|
SHIFT(sh,tm->x_k); for(int j=0;j<C;j++){double s=0;for(int i=0;i<C;i++)s+=sh[i]*tm->key_w[i*C+j]; k[j]=s;}
|
|
SHIFT(shv,tm->x_v);
|
|
for(int j=0;j<C;j++){double s=0;for(int i=0;i<C;i++)s+=shv[i]*tm->value_w[i*C+j]; v[j]=s;}
|
|
SHIFT(sh,tm->x_w);
|
|
for(int d=0;d<Dw;d++){double s=0;for(int i=0;i<C;i++)s+=sh[i]*tm->w1[i*Dw+d]; lo[d]=tanh(s);}
|
|
for(int j=0;j<C;j++){double s=0;for(int d=0;d<Dw;d++)s+=lo[d]*tm->w2[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;d<Da;d++){double s=0;for(int i=0;i<C;i++)s+=sh[i]*tm->a1[i*Da+d]; lo[d]=s;}
|
|
for(int j=0;j<C;j++){double s=0;for(int d=0;d<Da;d++)s+=lo[d]*tm->a2[d*C+j];
|
|
a[j]=dsig((double)tm->a0[j]+s);}
|
|
SHIFT(sh,tm->x_g);
|
|
for(int d=0;d<Dg;d++){double s=0;for(int i=0;i<C;i++)s+=sh[i]*tm->g1[i*Dg+d]; lo[d]=dsig(s);}
|
|
for(int j=0;j<C;j++){double s=0;for(int d=0;d<Dg;d++)s+=lo[d]*tm->g2[d*C+j]; g[j]=s;}
|
|
for(int i=0;i<C;i++) kk[i]=k[i]*(double)tm->k_k[i];
|
|
for(int h=0;h<H;h++){int off=h*N;double ss=0;for(int i=0;i<N;i++)ss+=kk[off+i]*kk[off+i];
|
|
double inv=1.0/(sqrt(ss)+1e-12); for(int i=0;i<N;i++) kk[off+i]*=inv;}
|
|
for(int i=0;i<C;i++) kmod[i]=k[i]*(1.0+(a[i]-1.0)*(double)tm->k_a[i]);
|
|
if (L0){ for(int i=0;i<C;i++){ vf[i]=v[i]; vfirst[base+i]=v[i]; } }
|
|
else {
|
|
for(int d=0;d<Dv;d++){double s=0;for(int i=0;i<C;i++)s+=shv[i]*tm->v1[i*Dv+d]; lo2[d]=s;}
|
|
for(int j=0;j<C;j++){double s=0;for(int d=0;d<Dv;d++)s+=lo2[d]*tm->v2[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;h<H;h++){int off=h*N;
|
|
for(int i=0;i<N;i++){
|
|
double *Si=&S[(size_t)(h*N+i)*N];
|
|
double sa=0; for(int j=0;j<N;j++) sa+=(-kk[off+j])*Si[j];
|
|
double vi=vf[off+i], yy=0;
|
|
for(int j=0;j<N;j++){ Si[j]=Si[j]*w[off+j]+kmod[off+j]*vi+sa*(kk[off+j]*a[off+j]);
|
|
yy+=Si[j]*r[off+j]; }
|
|
y[off+i]=yy;
|
|
}
|
|
}
|
|
for(int h=0;h<H;h++){int off=h*N;double mean=0;for(int i=0;i<N;i++)mean+=y[off+i];mean/=N;
|
|
double var=0;for(int i=0;i<N;i++){double d=y[off+i]-mean;var+=d*d;}var/=N;
|
|
double rstd=1.0/sqrt(var+64e-5);
|
|
for(int i=0;i<N;i++) ygn[off+i]=(y[off+i]-mean)*rstd*(double)tm->ln_x_w[off+i]+(double)tm->ln_x_b[off+i];}
|
|
for(int h=0;h<H;h++){int off=h*N;double s=0;
|
|
for(int j=0;j<N;j++)s+=r[off+j]*kmod[off+j]*(double)tm->r_k[off+j];
|
|
for(int j=0;j<N;j++)ygn[off+j]+=s*vf[off+j];}
|
|
for(int i=0;i<C;i++) ygn[i]*=g[i]; /* g gate */
|
|
for(int j=0;j<C;j++){double s=0;for(int i=0;i<C;i++)s+=ygn[i]*tm->output_w[i*C+j]; tmo[base+j]=s;}
|
|
#undef SHIFT
|
|
}
|
|
for (int t=0;t<T;t++){ size_t base=(size_t)t*C;
|
|
for(int i=0;i<C;i++) x1[base+i]=x[base+i]+tmo[base+i];
|
|
ln_fp64(&x1[base],P->ln2_w,P->ln2_b,&h2[base],C);
|
|
}
|
|
for (int t=0;t<T;t++){ size_t base=(size_t)t*C;
|
|
const double *xx=&h2[base]; const double *prev=(t==0)?NULL:&h2[base-C];
|
|
for(int i=0;i<C;i++){ double pv=prev?prev[i]:0.0;
|
|
sh[i]=xx[i]+(pv-xx[i])*(double)P->cm.ffn_x_k[i]; }
|
|
for(int mm=0;mm<F;mm++){double s=0;for(int i=0;i<C;i++)s+=sh[i]*P->cm.ffn_key_w[i*F+mm];
|
|
double rr=s>0.0?s:0.0; fk[mm]=rr*rr;}
|
|
for(int j=0;j<C;j++){double s=0;for(int mm=0;mm<F;mm++)s+=fk[mm]*P->cm.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;t<T;t++){ size_t base=(size_t)t*C;
|
|
for(int i=0;i<C;i++) xd[i]=x[base+i];
|
|
ln_fp64(xd,P->ln1_w,P->ln1_b,&h1[base],C);
|
|
for(int j=0;j<C;j++){ double sq=0,sk=0,sv=0;
|
|
for(int i=0;i<C;i++){ double hv=h1[base+i];
|
|
sq+=hv*P->receptance_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;h<H;h++){ int off=h*N;
|
|
for (int tq=0;tq<T;tq++){
|
|
size_t qb=(size_t)tq*C+off;
|
|
int local_chunk=tq/cs, ncand=local_chunk;
|
|
unsigned char *csel=calloc((size_t)(T>0?T:1),1);
|
|
int keepall=(P->moba_top_k<=0 || P->moba_top_k>=ncand);
|
|
if (keepall){ for(int cc=0;cc<ncand;cc++) csel[cc]=1; }
|
|
else {
|
|
double *gate=malloc((size_t)(T>0?T:1)*sizeof(double));
|
|
for(int c2=0;c2<ncand;c2++){ double gg=0;
|
|
for(int j=0;j<N;j++){ double mkj=0;
|
|
for(int s=c2*cs;s<(c2+1)*cs;s++) mkj+=k[(size_t)s*C+off+j];
|
|
mkj/=(double)cs; gg+=q[qb+j]*mkj; }
|
|
gate[c2]=gg; }
|
|
for(int it=0;it<P->moba_top_k;it++){ int arg=-1; double best=-1e300;
|
|
for(int c2=0;c2<ncand;c2++) if(!csel[c2]&&gate[c2]>best){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)||(c2<ncand&&csel[c2]);
|
|
if(!inc){ sc[s]=-1e300; continue; }
|
|
double d=0; for(int j=0;j<N;j++) d+=q[qb+j]*k[(size_t)s*C+off+j];
|
|
d*=scale; sc[s]=d; if(d>maxs)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;j<N;j++){ double acc=0;
|
|
for(int s=0;s<=tq;s++){ if(sc[s]<=-1e299)continue;
|
|
acc+=(sc[s]/sum)*v[(size_t)s*C+off+j]; }
|
|
y[qb+j]=acc; }
|
|
free(sc); free(csel);
|
|
}
|
|
}
|
|
for (int t=0;t<T;t++){ size_t base=(size_t)t*C;
|
|
for(int j=0;j<C;j++){ double s=0; for(int i=0;i<C;i++) s+=y[base+i]*P->output_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;t<T;t++){ size_t base=(size_t)t*C;
|
|
const double *xx=&h2[base]; const double *prev=(t==0)?NULL:&h2[base-C];
|
|
for(int i=0;i<C;i++){ double pv=prev?prev[i]:0.0;
|
|
sh[i]=xx[i]+(pv-xx[i])*(double)P->cm.ffn_x_k[i]; }
|
|
for(int mm=0;mm<F;mm++){ double s=0; for(int i=0;i<C;i++) s+=sh[i]*P->cm.ffn_key_w[i*F+mm];
|
|
double rr=s>0.0?s:0.0; fk[mm]=rr*rr; }
|
|
for(int j=0;j<C;j++){ double s=0; for(int mm=0;mm<F;mm++) s+=fk[mm]*P->cm.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<n_blocks;i++){btype[i]=0;bidx[i]=i;} }
|
|
else { int interval=pc.n_rwkv_layer/pc.n_attn_layer, pos=0;
|
|
for(int a=0;a<pc.n_attn_layer;a++){ int st=a*interval,en=(a+1)*interval;
|
|
for(int j=st;j<en;j++){btype[pos]=0;bidx[pos]=j;pos++;}
|
|
btype[pos]=1;bidx[pos]=a;pos++; }
|
|
for(int j=pc.n_attn_layer*interval;j<pc.n_rwkv_layer;j++){btype[pos]=0;bidx[pos]=j;pos++;} }
|
|
|
|
size_t TC=(size_t)T*C;
|
|
double *x=malloc(TC*sizeof(double));
|
|
double *tmp=malloc(TC*sizeof(double));
|
|
double *vfirst=calloc(TC,sizeof(double));
|
|
{ /* emb gather — read emb from the registry (name "emb") */
|
|
float *emb=NULL;
|
|
for (int i=0;i<rx_ptmodel_num_params(m);i++){ const char*nm; float*d;int nn;
|
|
if (rx_ptmodel_param(m,i,&nm,&d,NULL,&nn)==0 && strcmp(nm,"emb")==0){emb=d;break;} }
|
|
for (int t=0;t<T;t++) for (int i=0;i<C;i++) x[(size_t)t*C+i]=(double)emb[(size_t)tokens[t]*C+i];
|
|
}
|
|
for (int pos=0;pos<n_blocks;pos++){
|
|
if (btype[pos]==0) block_fwd_fp64(rx_ptmodel_rwkv(m,bidx[pos]), x, vfirst, tmp, T);
|
|
else attn_fwd_fp64(rx_ptmodel_attn(m,bidx[pos]), x, tmp, T);
|
|
memcpy(x,tmp,TC*sizeof(double));
|
|
}
|
|
/* ln_out + head + CE */
|
|
float *lnw=NULL,*lnb=NULL,*head=NULL;
|
|
for (int i=0;i<rx_ptmodel_num_params(m);i++){ const char*nm; float*d;int nn;
|
|
if (rx_ptmodel_param(m,i,&nm,&d,NULL,&nn)!=0) continue;
|
|
if (!strcmp(nm,"ln_out_w")) lnw=d; else if(!strcmp(nm,"ln_out_b")) lnb=d;
|
|
else if(!strcmp(nm,"head")) head=d; }
|
|
double *h=malloc(C*sizeof(double));
|
|
double loss=0.0; int nv=0;
|
|
for (int t=0;t<T;t++){
|
|
ln_fp64(&x[(size_t)t*C],lnw,lnb,h,C);
|
|
double *lg = logits_out ? &logits_out[(size_t)t*V] : malloc(V*sizeof(double));
|
|
for (int j=0;j<V;j++){ double s=0; for(int i=0;i<C;i++) s+=h[i]*(double)head[(size_t)i*V+j]; lg[j]=s; }
|
|
int tg=targets[t];
|
|
if (!(ignore>=0 && tg==ignore)){
|
|
double mx=-1e300; for(int j=0;j<V;j++) if(lg[j]>mx)mx=lg[j];
|
|
double se=0; for(int j=0;j<V;j++) se+=exp(lg[j]-mx);
|
|
loss += (mx+log(se)) - lg[tg]; nv++;
|
|
}
|
|
if (!logits_out) free(lg);
|
|
}
|
|
free(x);free(tmp);free(vfirst);free(h);free(btype);free(bidx);
|
|
return nv>0?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<FD_CAP ? n : FD_CAP;
|
|
int step = n/cap; if (step<1) step=1;
|
|
float maxdiff=0.0f, maxfd=0.0f, maxg=0.0f;
|
|
for (int c=0;c<cap;c++){
|
|
int e=c*step; if (e>=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;t<T;t++){ tok[t]=rand()%pc.vocab_size; tgt[t]=rand()%pc.vocab_size; }
|
|
|
|
rx_pt_acts *acts=rx_pt_acts_new(m,T);
|
|
if (rx_pretrain_forward(m,tok,T,acts)!=0){ printf(" forward alloc FAIL\n"); return 0; }
|
|
|
|
/* fp32 forward vs fp64 mirror logits (assembly self-consistency) */
|
|
double *lg64=malloc((size_t)T*pc.vocab_size*sizeof(double));
|
|
double loss64=model_loss_fp64(m,tok,tgt,T,-1,lg64);
|
|
float maxlog=0.0f, reflog=0.0f;
|
|
for (int t=0;t<T;t++) for (int j=0;j<pc.vocab_size;j++){
|
|
float a=acts->logits[(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;t<T;t++){
|
|
tm_tensor *lg = rx_forward(rm, (uint32_t)tok[t], st);
|
|
const float *ld = tm_data_f32(lg);
|
|
for (int j=0;j<pc.vocab_size;j++){
|
|
float a=acts->logits[(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;i<np;i++){
|
|
const char *nm; float *data,*grad; int n;
|
|
rx_ptmodel_param(m,i,&nm,&data,&grad,&n);
|
|
/* verbose print only for representative tensors to keep output readable */
|
|
int rep = (strstr(nm,"emb")||strstr(nm,"head")||strstr(nm,"ln_out")||
|
|
!strncmp(nm,"rwkv0.",6)||!strncmp(nm,"attn0.",6));
|
|
float e=fd_tensor(m,tok,tgt,T,-1,data,grad,n,nm,rep);
|
|
if (e>wmax){ 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;
|
|
}
|