Merge #298: per-group scales in CUDA dense/attention kernels (fmt=4 correctness)

Fixes a live correctness bug on dev: with CUDA_DENSE=1 on a g64 (fmt=4) container, the dense matmul and attention-absorb kernels applied scales per-row (scales[o] / wscale[row]) while the uploaded scale array is per-group [O × ceil(I/gs)] — wrong scale for nearly every row, garbage output ('odesk odesk…'). quant_matmul now applies group scales inline (matching CPU matmul_i4_grouped exactly), absorb_scale handles fmt=4 in the attention kernels (the kv_b crash), w4a16/TC fast paths stay correctly gated to fmt=2, and a fused CPU gate+up pair (matmul_i4_grouped_pair, AVX2) lands as a bonus. Also adds the fmt=4→CPU fallback log requested in review.

Credits: @woolcoxm (author, rebase onto post-#391 dev), @mohamedmastouri2000-boop (root-cause isolation + hardware verification on RTX 5080/sm_120: garbage→coherent, 952 dense tensors + 109 experts fully VRAM-resident, zero fallbacks). Verified locally: clean build, token-exact tiny models unchanged; CI 8/8 green.
This commit is contained in:
Vincenzo Fornaro
2026-07-20 20:40:25 +02:00
committed by GitHub
9 changed files with 964 additions and 75 deletions
+76 -40
View File
@@ -21,6 +21,7 @@ struct ColiCudaTensor {
size_t weight_bytes;
int fmt, I, O, device;
int gs; /* quant group size; 0 = per-row scales (#334) */
int ng; /* number of scale groups per row = ceil(I/gs) for fmt=4 */
size_t scale_count; /* floats in `scales`: O per-row, O*ng grouped */
int tracked;
RaggedKVEntry ragged[512];
@@ -85,7 +86,7 @@ static int select_ctx(DeviceContext *ctx) {
__host__ __device__ static size_t row_bytes(int fmt, int I) {
if (fmt == 0) return (size_t)I * sizeof(float);
if (fmt == 1) return (size_t)I;
if (fmt == 2) return (size_t)(I + 1) / 2;
if (fmt == 2 || fmt == 4) return (size_t)(I + 1) / 2; /* fmt=4: same packed int4 */
if (fmt == 3) return (size_t)(I + 3) / 4;
if (fmt == 4) return (size_t)(I + 1) / 2; /* grouped int4: nibbles like fmt 2 */
return 0;
@@ -96,7 +97,7 @@ __device__ static float weight_at(const void *weights, int fmt, size_t row, int
if (fmt == 0) return reinterpret_cast<const float *>(base)[i];
if (fmt == 1) return static_cast<float>(reinterpret_cast<const int8_t *>(base)[i]);
const uint8_t *q = base;
if (fmt == 2) {
if (fmt == 2 || fmt == 4) { /* fmt=4: same nibble layout */
uint8_t v = q[i >> 1];
int n=(i&1)?(v>>4):(v&15); return static_cast<float>(n&8?n-16:n);
}
@@ -104,20 +105,45 @@ __device__ static float weight_at(const void *weights, int fmt, size_t row, int
return static_cast<float>(((v >> ((i & 3) * 2)) & 3) - 2);
}
/* Scale for output `row`, input element `k`. fmt=4 (grouped int4) stores ng
* scales per row at scales[row*ng + k/gs]; every other quantized format has
* one scale per row at scales[row]. Mirrors quant_matmul's fmt==4 branch so the
* attention absorb kernels apply per-group scales instead of the per-row
* (fmt=2) semantic that crashed #298's g64 kv_b. */
__device__ static float absorb_scale(const float *wscale, int fmt, int gs, int ng, int row, int k) {
if (!fmt) return 1.f;
if (fmt != 4) return wscale[row];
int g = k / gs; if (g >= ng) g = ng - 1; /* tail of the last (partial) group */
return wscale[(size_t)row * ng + g];
}
__global__ static void offset_to_signed_s4(uint8_t *q,size_t n){
size_t i=(size_t)blockIdx.x*blockDim.x+threadIdx.x;if(i<n)q[i]^=0x88;
}
__global__ static void quant_matmul(float *y, const float *x, const void *weights,
const float *scales, int fmt, int S, int I, int O,
size_t rb) {
size_t rb, int gs, int ng) {
int o = blockIdx.x;
int s = blockIdx.y;
float sum = 0.0f;
size_t row = (size_t)o * rb;
const float *xs = x + (size_t)s * I;
for (int i = threadIdx.x; i < I; i += blockDim.x)
sum += xs[i] * weight_at(weights, fmt, row, i);
if (fmt == 4) {
/* Grouped int4: one f32 scale per gs elements along I (ng groups per row).
* Scale layout: scales[o*ng + g]. Each thread strides through I, applying
* the appropriate group scale as it crosses group boundaries. This matches
* the CPU matmul_i4_grouped accumulation exactly. */
const float *scl = scales + (size_t)o * ng;
for (int i = threadIdx.x; i < I; i += blockDim.x) {
int g = i / gs;
if (g >= ng) g = ng - 1; /* tail elements in the last (partial) group */
sum += xs[i] * weight_at(weights, fmt, row, i) * scl[g];
}
} else {
for (int i = threadIdx.x; i < I; i += blockDim.x)
sum += xs[i] * weight_at(weights, fmt, row, i);
}
__shared__ float partial[256];
partial[threadIdx.x] = sum;
@@ -127,7 +153,7 @@ __global__ static void quant_matmul(float *y, const float *x, const void *weight
__syncthreads();
}
if (!threadIdx.x)
y[(size_t)s * O + o] = partial[0] * (fmt ? scales[o] : 1.0f);
y[(size_t)s * O + o] = (fmt && fmt != 4) ? partial[0] * scales[o] : partial[0];
}
__global__ static void silu_mul(float *gate, const float *up, size_t n) {
@@ -341,11 +367,12 @@ __global__ static void grouped_down_g4(float *y,const float *x,const GroupDesc *
__global__ static void attention_absorb_kernel(float *ctx,const float *q,const float *latent,
const float *rope,const void *weights,const float *wscale,
int fmt,int H,int Q,int R,int V,int K,int T,float scale){
int fmt,int H,int Q,int R,int V,int K,int T,float scale,
int gs,int ng){
int h=blockIdx.x,tid=threadIdx.x,rbase=h*(Q+V);extern __shared__ float sm[];
float *qa=sm,*cl=qa+K,*scores=cl+K;
for(int k=tid;k<K;k+=blockDim.x){float a=0;for(int d=0;d<Q;d++)
a+=q[(size_t)h*(Q+R)+d]*weight_at(weights,fmt,(size_t)(rbase+d)*row_bytes(fmt,K),k)*(fmt?wscale[rbase+d]:1.f);qa[k]=a;}
a+=q[(size_t)h*(Q+R)+d]*weight_at(weights,fmt,(size_t)(rbase+d)*row_bytes(fmt,K),k)*absorb_scale(wscale,fmt,gs,ng,rbase+d,k);qa[k]=a;}
__syncthreads();
for(int t=tid;t<T;t+=blockDim.x){float a=0;const float *lt=latent+(size_t)t*K,*rt=rope+(size_t)t*R;
for(int k=0;k<K;k++)a+=qa[k]*lt[k];for(int d=0;d<R;d++)a+=q[(size_t)h*(Q+R)+Q+d]*rt[d];scores[t]=a*scale;}
@@ -356,19 +383,20 @@ __global__ static void attention_absorb_kernel(float *ctx,const float *q,const f
for(int k=tid;k<K;k+=blockDim.x){float a=0;for(int t=0;t<T;t++)a+=scores[t]*latent[(size_t)t*K+k];cl[k]=a;}
__syncthreads();
for(int v=tid;v<V;v+=blockDim.x){int row=rbase+Q+v;float a=0;size_t rb=row_bytes(fmt,K);
for(int k=0;k<K;k++)a+=cl[k]*weight_at(weights,fmt,(size_t)row*rb,k);ctx[(size_t)h*V+v]=a*(fmt?wscale[row]:1.f);}
for(int k=0;k<K;k++)a+=cl[k]*weight_at(weights,fmt,(size_t)row*rb,k)*absorb_scale(wscale,fmt,gs,ng,row,k);ctx[(size_t)h*V+v]=a;}
}
__global__ static void attention_absorb_batch_kernel(float *ctx,const float *q,
const float *latent,const float *rope,const void *weights,const float *wscale,
int fmt,int S,int H,int Q,int R,int V,int K,int T,float scale){
int fmt,int S,int H,int Q,int R,int V,int K,int T,float scale,
int gs,int ng){
int s=blockIdx.y,h=blockIdx.x,tid=threadIdx.x,nt=T-S+s+1,rbase=h*(Q+V);
if(s>=S||nt<1)return;
extern __shared__ float sm[];float *qa=sm,*cl=qa+K,*scores=cl+K,*red=scores+T;
const float *qs=q+((size_t)s*H+h)*(Q+R);
for(int k=tid;k<K;k+=blockDim.x){float a=0;for(int d=0;d<Q;d++)
a+=qs[d]*weight_at(weights,fmt,(size_t)(rbase+d)*row_bytes(fmt,K),k)*
(fmt?wscale[rbase+d]:1.f);qa[k]=a;}
absorb_scale(wscale,fmt,gs,ng,rbase+d,k);qa[k]=a;}
__syncthreads();
for(int t=tid;t<nt;t+=blockDim.x){float a=0;const float *lt=latent+(size_t)t*K;
const float *rt=rope+(size_t)t*R;for(int k=0;k<K;k++)a+=qa[k]*lt[k];
@@ -386,8 +414,8 @@ __global__ static void attention_absorb_batch_kernel(float *ctx,const float *q,
a+=scores[t]*latent[(size_t)t*K+k];cl[k]=a;}
__syncthreads();
for(int v=tid;v<V;v+=blockDim.x){int row=rbase+Q+v;float a=0;size_t rb=row_bytes(fmt,K);
for(int k=0;k<K;k++)a+=cl[k]*weight_at(weights,fmt,(size_t)row*rb,k);
ctx[((size_t)s*H+h)*V+v]=a*(fmt?wscale[row]:1.f);}
for(int k=0;k<K;k++)a+=cl[k]*weight_at(weights,fmt,(size_t)row*rb,k)*absorb_scale(wscale,fmt,gs,ng,row,k);
ctx[((size_t)s*H+h)*V+v]=a;}
}
/* Independent device-resident KV sequence per row. lengths selects the valid
@@ -564,7 +592,8 @@ extern "C" int coli_cuda_tensor_upload(ColiCudaTensor **tensor,
* on such a slot failed here — the GPU tier silently never computed
* for host-released slab experts. */
ColiCudaTensor *t = *tensor;
return t->fmt == fmt && t->I == I && t->O == O && t->device == device;
int want_gs = (fmt==4 && g_upload_gs>0) ? g_upload_gs : 0;
return t->fmt == fmt && t->I == I && t->O == O && t->device == device && t->gs == want_gs;
}
DeviceContext *ctx = find_ctx(device);
if (!weights || I < 1 || O < 1 || !select_ctx(ctx)) return 0;
@@ -574,7 +603,8 @@ extern "C" int coli_cuda_tensor_upload(ColiCudaTensor **tensor,
if (!t) return 0;
t->fmt = fmt; t->I = I; t->O = O; t->device = device; t->weight_bytes = rb * (size_t)O;
t->gs = (fmt==4 && g_upload_gs>0) ? g_upload_gs : 0;
t->scale_count = t->gs ? (size_t)O * (size_t)((I + t->gs - 1) / t->gs) : (size_t)O;
t->ng = t->gs ? (I + t->gs - 1) / t->gs : 1;
t->scale_count = t->gs ? (size_t)O * (size_t)t->ng : (size_t)O;
if (!cuda_ok(cudaMalloc(&t->weights, t->weight_bytes), "tensor allocation") ||
!cuda_ok(cudaMemcpy(t->weights, weights, t->weight_bytes, cudaMemcpyHostToDevice), "tensor upload")) {
coli_cuda_tensor_free(t);
@@ -618,6 +648,7 @@ extern "C" int coli_cuda_tensor_update(ColiCudaTensor *tensor,
(uint8_t*)tensor->weights,tensor->weight_bytes);
if(!cuda_ok(cudaGetLastError(),"int4 weight refresh")) return 0;
}
int ng = tensor->ng > 0 ? tensor->ng : 1;
return !tensor->fmt || cuda_ok(cudaMemcpy(tensor->scales,scales,
(tensor->scale_count?tensor->scale_count:(size_t)tensor->O)*sizeof(float),
cudaMemcpyHostToDevice),"scale refresh");
@@ -636,9 +667,11 @@ static int fault_injected(void) {
extern "C" int coli_cuda_matmul(ColiCudaTensor **tensor,
float *y, const float *x,
const void *weights, const float *scales,
int fmt, int S, int I, int O, int device) {
int fmt, int S, int I, int O, int device, int gs) {
if (fault_injected()) return 0;
if (S < 1 || !coli_cuda_tensor_upload(tensor, weights, scales, fmt, I, O, device)) return 0;
if (S < 1) return 0;
if (gs > 0) { if (!coli_cuda_tensor_upload_g(tensor, weights, scales, fmt, I, O, device, gs)) return 0; }
else { if (!coli_cuda_tensor_upload(tensor, weights, scales, fmt, I, O, device)) return 0; }
ColiCudaTensor *t = *tensor;
DeviceContext *ctx = find_ctx(t->device);
if (!select_ctx(ctx)) return 0;
@@ -647,7 +680,7 @@ extern "C" int coli_cuda_matmul(ColiCudaTensor **tensor,
if (!reserve(&ctx->x, &ctx->x_cap, xb) || !reserve(&ctx->y, &ctx->y_cap, yb)) return 0;
if (!cuda_ok(cudaMemcpy(ctx->x, x, xb, cudaMemcpyHostToDevice), "input upload")) return 0;
dim3 grid((unsigned)O, (unsigned)S);
quant_matmul<<<grid, 256>>>(ctx->y, ctx->x, t->weights, t->scales, fmt, S, I, O, rb);
quant_matmul<<<grid, 256>>>(ctx->y, ctx->x, t->weights, t->scales, fmt, S, I, O, rb, t->gs, t->ng);
if (!cuda_ok(cudaGetLastError(), "matmul launch") ||
!cuda_ok(cudaMemcpy(y, ctx->y, yb, cudaMemcpyDeviceToHost), "output download")) return 0;
return 1;
@@ -671,13 +704,13 @@ extern "C" int coli_cuda_expert_mlp(ColiCudaTensor *gate, ColiCudaTensor *up,
if (!cuda_ok(cudaMemcpy(ctx->x,x,xb,cudaMemcpyHostToDevice),"expert input upload")) return 0;
dim3 hidden_grid((unsigned)I,(unsigned)S), output_grid((unsigned)D,(unsigned)S);
quant_matmul<<<hidden_grid,256>>>(ctx->gate,ctx->x,gate->weights,gate->scales,
gate->fmt,S,D,I,row_bytes(gate->fmt,D));
gate->fmt,S,D,I,row_bytes(gate->fmt,D),gate->gs,gate->ng);
quant_matmul<<<hidden_grid,256>>>(ctx->up,ctx->x,up->weights,up->scales,
up->fmt,S,D,I,row_bytes(up->fmt,D));
up->fmt,S,D,I,row_bytes(up->fmt,D),up->gs,up->ng);
size_t n=(size_t)S*I;
silu_mul<<<(unsigned)((n+255)/256),256>>>(ctx->gate,ctx->up,n);
quant_matmul<<<output_grid,256>>>(ctx->y,ctx->gate,down->weights,down->scales,
down->fmt,S,I,D,row_bytes(down->fmt,I));
down->fmt,S,I,D,row_bytes(down->fmt,I),down->gs,down->ng);
if (!cuda_ok(cudaGetLastError(),"expert MLP launch") ||
!cuda_ok(cudaMemcpy(y,ctx->y,yb,cudaMemcpyDeviceToHost),"expert output download")) return 0;
return 1;
@@ -805,12 +838,12 @@ extern "C" int coli_cuda_expert_group(ColiCudaTensor *const *gates,
/* piccoli batch: tile TC quasi vuoti + overhead di lancio — il
* kernel naive per-elemento resta piu' veloce (misurato in decode) */
quant_matmul<<<dim3((unsigned)I,(unsigned)r),256,0,ctx->stream>>>(g16,x16,
host[c].g,host[c].gs,host[c].gf,r,D,I,row_bytes(host[c].gf,D));
host[c].g,host[c].gs,host[c].gf,r,D,I,row_bytes(host[c].gf,D),0,1);
quant_matmul<<<dim3((unsigned)I,(unsigned)r),256,0,ctx->stream>>>(u16,x16,
host[c].u,host[c].us,host[c].uf,r,D,I,row_bytes(host[c].uf,D));
host[c].u,host[c].us,host[c].uf,r,D,I,row_bytes(host[c].uf,D),0,1);
silu_mul<<<(unsigned)(((size_t)r*I+255)/256),256,0,ctx->stream>>>(g16,u16,(size_t)r*I);
quant_matmul<<<dim3((unsigned)D,(unsigned)r),256,0,ctx->stream>>>(y16,g16,
host[c].d,host[c].ds,host[c].df,r,I,D,row_bytes(host[c].df,I));
host[c].d,host[c].ds,host[c].df,r,I,D,row_bytes(host[c].df,I),0,1);
}
off16+=r;
}
@@ -905,12 +938,12 @@ extern "C" int coli_cuda_expert_group_issue(ColiCudaTensor *const *gates,
float *g16=ctx->gate+(size_t)host[c].offset*I,*u16=ctx->up+(size_t)host[c].offset*I;
float *x16=ctx->x+(size_t)host[c].offset*D,*y16=ctx->y+(size_t)host[c].offset*D;
quant_matmul<<<dim3((unsigned)I,(unsigned)r),256,0,ctx->stream>>>(g16,x16,
host[c].g,host[c].gs,host[c].gf,r,D,I,row_bytes(host[c].gf,D));
host[c].g,host[c].gs,host[c].gf,r,D,I,row_bytes(host[c].gf,D),0,1);
quant_matmul<<<dim3((unsigned)I,(unsigned)r),256,0,ctx->stream>>>(u16,x16,
host[c].u,host[c].us,host[c].uf,r,D,I,row_bytes(host[c].uf,D));
host[c].u,host[c].us,host[c].uf,r,D,I,row_bytes(host[c].uf,D),0,1);
silu_mul<<<(unsigned)(((size_t)r*I+255)/256),256,0,ctx->stream>>>(g16,u16,(size_t)r*I);
quant_matmul<<<dim3((unsigned)D,(unsigned)r),256,0,ctx->stream>>>(y16,g16,
host[c].d,host[c].ds,host[c].df,r,I,D,row_bytes(host[c].df,I));
host[c].d,host[c].ds,host[c].df,r,I,D,row_bytes(host[c].df,I),0,1);
}
if(!cuda_ok(cudaGetLastError(),"expert group issue launch")||
!cuda_ok(cudaMemcpyAsync(ctx->host_y,ctx->y,xb,cudaMemcpyDeviceToHost,ctx->stream),
@@ -947,7 +980,7 @@ extern "C" int coli_cuda_attention_absorb(ColiCudaTensor *w,float *ctx,const flo
!cuda_ok(cudaMemcpyAsync(dc->ar,rope,rb,cudaMemcpyHostToDevice,dc->stream),"attention rope upload"))return 0;
size_t shared=(size_t)(2*K+T)*sizeof(float);
attention_absorb_kernel<<<H,256,shared,dc->stream>>>(dc->ac,dc->aq,dc->al,dc->ar,w->weights,w->scales,
w->fmt,H,Q,R,V,K,T,scale);
w->fmt,H,Q,R,V,K,T,scale,w->gs,w->ng);
if(!cuda_ok(cudaGetLastError(),"attention absorb launch")||
!cuda_ok(cudaMemcpyAsync(ctx,dc->ac,cb,cudaMemcpyDeviceToHost,dc->stream),"attention context download")||
!cuda_ok(cudaStreamSynchronize(dc->stream),"attention synchronize"))return 0;
@@ -970,13 +1003,13 @@ static int attention_absorb_batch_run(ColiCudaTensor *w,ColiCudaTensor *proj,flo
!cuda_ok(cudaMemcpyAsync(dc->ar,rope,rb,cudaMemcpyHostToDevice,dc->stream),"attention batch rope upload"))return 0;
size_t shared=(size_t)(2*K+T+256)*sizeof(float);
attention_absorb_batch_kernel<<<dim3(H,S),256,shared,dc->stream>>>(dc->ac,dc->aq,dc->al,
dc->ar,w->weights,w->scales,w->fmt,S,H,Q,R,V,K,T,scale);
dc->ar,w->weights,w->scales,w->fmt,S,H,Q,R,V,K,T,scale,w->gs,w->ng);
if(!cuda_ok(cudaGetLastError(),"attention batch launch"))return 0;
const float *src=dc->ac;size_t ob=cb;
if(proj){
ob=(size_t)S*proj->O*sizeof(float);if(!reserve(&dc->y,&dc->y_cap,ob))return 0;
quant_matmul<<<dim3(proj->O,S),256,0,dc->stream>>>(dc->y,dc->ac,proj->weights,
proj->scales,proj->fmt,S,proj->I,proj->O,row_bytes(proj->fmt,proj->I));
proj->scales,proj->fmt,S,proj->I,proj->O,row_bytes(proj->fmt,proj->I),proj->gs,proj->ng);
if(!cuda_ok(cudaGetLastError(),"attention o_proj launch"))return 0;src=dc->y;
}
if(!cuda_ok(cudaMemcpyAsync(out,src,ob,cudaMemcpyDeviceToHost,dc->stream),
@@ -1076,7 +1109,7 @@ extern "C" int coli_cuda_attention_project_ragged(ColiCudaTensor *w,ColiCudaTens
attention_absorb_ragged_kernel<<<dim3(H,S),256,shared,dc->stream>>>(dc->ac,dc->aq,ddl,ddr,
dn,w->weights,w->scales,w->fmt,S,H,Q,R,V,K,T,scale);
quant_matmul<<<dim3(proj->O,S),256,0,dc->stream>>>(dc->y,dc->ac,proj->weights,
proj->scales,proj->fmt,S,proj->I,proj->O,row_bytes(proj->fmt,proj->I));
proj->scales,proj->fmt,S,proj->I,proj->O,row_bytes(proj->fmt,proj->I),proj->gs,proj->ng);
return cuda_ok(cudaGetLastError(),"ragged attention launch")&&
cuda_ok(cudaMemcpyAsync(out,dc->y,ob,cudaMemcpyDeviceToHost,dc->stream),"ragged output download")&&
cuda_ok(cudaStreamSynchronize(dc->stream),"ragged attention synchronize");
@@ -1087,7 +1120,8 @@ extern "C" void coli_cuda_tensor_free(ColiCudaTensor *tensor) {
DeviceContext *ctx = find_ctx(tensor->device);
if (ctx) select_ctx(ctx);
if (tensor->tracked && ctx) {
size_t bytes = tensor->weight_bytes + (tensor->fmt ? (size_t)tensor->O * sizeof(float) : 0);
int ng = tensor->ng > 0 ? tensor->ng : 1;
size_t bytes = tensor->weight_bytes + (tensor->fmt ? (size_t)tensor->O * ng * sizeof(float) : 0);
if (ctx->tensor_count) ctx->tensor_count--;
if (ctx->tensor_bytes >= bytes) ctx->tensor_bytes -= bytes;
}
@@ -1101,7 +1135,9 @@ extern "C" void coli_cuda_tensor_free(ColiCudaTensor *tensor) {
}
extern "C" size_t coli_cuda_tensor_bytes(const ColiCudaTensor *tensor) {
return tensor ? tensor->weight_bytes + (tensor->fmt ? (size_t)tensor->O * sizeof(float) : 0) : 0;
if (!tensor) return 0;
int ng = tensor->ng > 0 ? tensor->ng : 1;
return tensor->weight_bytes + (tensor->fmt ? (size_t)tensor->O * ng * sizeof(float) : 0);
}
extern "C" int coli_cuda_tensor_device(const ColiCudaTensor *tensor) {
@@ -1315,12 +1351,12 @@ extern "C" int coli_cuda_attention_project_batch_dev(ColiCudaTensor *w,ColiCudaT
if(!reserve(&dc->ac,&dc->ac_cap,cb))return 0;
size_t shared=(size_t)(2*K+T+256)*sizeof(float);
attention_absorb_batch_kernel<<<dim3(H,S),256,shared,dc->stream>>>(dc->ac,q_dev,latent_dev,
rope_dev,w->weights,w->scales,w->fmt,S,H,Q,R,V,K,T,scale);
rope_dev,w->weights,w->scales,w->fmt,S,H,Q,R,V,K,T,scale,w->gs,w->ng);
if(!cuda_ok(cudaGetLastError(),"pipe attention launch"))return 0;
size_t ob=(size_t)S*proj->O*sizeof(float);
if(!reserve(&dc->y,&dc->y_cap,ob))return 0;
quant_matmul<<<dim3(proj->O,S),256,0,dc->stream>>>(dc->y,dc->ac,proj->weights,
proj->scales,proj->fmt,S,proj->I,proj->O,row_bytes(proj->fmt,proj->I));
proj->scales,proj->fmt,S,proj->I,proj->O,row_bytes(proj->fmt,proj->I),proj->gs,proj->ng);
if(!cuda_ok(cudaGetLastError(),"pipe o_proj launch"))return 0;
if(!cuda_ok(cudaMemcpyAsync(out,dc->y,ob,cudaMemcpyDeviceToHost,dc->stream),"pipe attention download")||
!cuda_ok(cudaStreamSynchronize(dc->stream),"pipe attention sync"))return 0;
@@ -1355,7 +1391,7 @@ extern "C" int coli_cuda_pipe_gemm(ColiCudaTensor *t,float *y_dev,const float *x
DeviceContext *ctx=find_ctx(t->device); if(!select_ctx(ctx)) return 0;
dim3 grid((unsigned)t->O,(unsigned)S);
quant_matmul<<<grid,256>>>(y_dev,x_dev,t->weights,t->scales,t->fmt,S,t->I,t->O,
row_bytes(t->fmt,t->I));
row_bytes(t->fmt,t->I),t->gs,t->ng);
return cuda_ok(cudaGetLastError(),"pipe gemm");
}
/* copia diretta scheda->scheda (P2P se disponibile, altrimenti staging driver) */
@@ -1379,10 +1415,10 @@ extern "C" int coli_cuda_attention_project_batch_dev_out(ColiCudaTensor *w,ColiC
if(!reserve(&dc->ac,&dc->ac_cap,cb))return 0;
size_t shared=(size_t)(2*K+T+256)*sizeof(float);
attention_absorb_batch_kernel<<<dim3(H,S),256,shared,dc->stream>>>(dc->ac,q_dev,latent_dev,
rope_dev,w->weights,w->scales,w->fmt,S,H,Q,R,V,K,T,scale);
rope_dev,w->weights,w->scales,w->fmt,S,H,Q,R,V,K,T,scale,w->gs,w->ng);
if(!cuda_ok(cudaGetLastError(),"pipe attention launch (dev out)"))return 0;
quant_matmul<<<dim3(proj->O,S),256,0,dc->stream>>>(out_dev,dc->ac,proj->weights,
proj->scales,proj->fmt,S,proj->I,proj->O,row_bytes(proj->fmt,proj->I));
proj->scales,proj->fmt,S,proj->I,proj->O,row_bytes(proj->fmt,proj->I),proj->gs,proj->ng);
if(!cuda_ok(cudaGetLastError(),"pipe o_proj launch (dev out)"))return 0;
return cuda_ok(cudaStreamSynchronize(dc->stream),"pipe attention sync (dev out)");
}
@@ -1398,7 +1434,7 @@ extern "C" int coli_cuda_attention_absorb_batch_dev(ColiCudaTensor *w,float *ctx
DeviceContext *dc=find_ctx(w->device);if(!select_ctx(dc))return 0;
size_t shared=(size_t)(2*K+T+256)*sizeof(float);
attention_absorb_batch_kernel<<<dim3(H,S),256,shared,dc->stream>>>(ctx_dev,q_dev,latent_dev,
rope_dev,w->weights,w->scales,w->fmt,S,H,Q,R,V,K,T,scale);
rope_dev,w->weights,w->scales,w->fmt,S,H,Q,R,V,K,T,scale,w->gs,w->ng);
if(!cuda_ok(cudaGetLastError(),"pipe shard attention launch"))return 0;
return cuda_ok(cudaStreamSynchronize(dc->stream),"pipe shard attention sync");
}
@@ -1416,7 +1452,7 @@ extern "C" int coli_cuda_attention_absorb_kvdev(ColiCudaTensor *w,float *ctx,con
if(!cuda_ok(cudaMemcpyAsync(dc->aq,q,qb,cudaMemcpyHostToDevice,dc->stream),"kvdev q upload"))return 0;
size_t shared=(size_t)(2*K+T+256)*sizeof(float);
attention_absorb_batch_kernel<<<dim3(H,1),256,shared,dc->stream>>>(dc->ac,dc->aq,latent_dev,
rope_dev,w->weights,w->scales,w->fmt,1,H,Q,R,V,K,T,scale);
rope_dev,w->weights,w->scales,w->fmt,1,H,Q,R,V,K,T,scale,w->gs,w->ng);
if(!cuda_ok(cudaGetLastError(),"kvdev absorb launch")||
!cuda_ok(cudaMemcpyAsync(ctx,dc->ac,cb,cudaMemcpyDeviceToHost,dc->stream),"kvdev ctx download")||
!cuda_ok(cudaStreamSynchronize(dc->stream),"kvdev absorb sync"))return 0;
+4 -3
View File
@@ -45,14 +45,15 @@ COLI_CUDA_DLLEXPORT int coli_cuda_tensor_upload(ColiCudaTensor **tensor,
/*
* y[S,O] = x[S,I] @ W[O,I]^T.
* fmt matches QT in glm.c: 0=f32, 1=int8, 2=int4, 3=int2.
* The first successful call uploads W and its row scales; later calls reuse it.
* fmt matches QT in glm.c: 0=f32, 1=int8, 2=int4, 3=int2, 4=grouped int4.
* gs is the group size for fmt=4 (0 for all other formats).
* The first successful call uploads W and its scales; later calls reuse it.
* Returns 1 on success and 0 when CUDA is not initialized or the format is invalid.
*/
COLI_CUDA_DLLEXPORT int coli_cuda_matmul(ColiCudaTensor **tensor,
float *y, const float *x,
const void *weights, const float *scales,
int fmt, int S, int I, int O, int device);
int fmt, int S, int I, int O, int device, int gs);
/* Fused expert pipeline: y = down(silu(gate(x)) * up(x)). All three tensors
* must already be resident on one device. Activations cross PCIe once in
+3 -3
View File
@@ -54,7 +54,7 @@ typedef int (*fn_tensor_upload)(ColiCudaTensor **tensor, const void *
typedef int (*fn_tensor_upload_g)(ColiCudaTensor **tensor, const void *weights, const float *scales, int fmt, int I, int O, int device, int gs);
typedef int (*fn_matmul)(ColiCudaTensor **tensor, float *y, const float *x,
const void *weights, const float *scales,
int fmt, int S, int I, int O, int device);
int fmt, int S, int I, int O, int device, int gs);
typedef void (*fn_tensor_free)(ColiCudaTensor *tensor);
typedef size_t (*fn_tensor_bytes)(const ColiCudaTensor *tensor);
typedef int (*fn_tensor_device)(const ColiCudaTensor *tensor);
@@ -337,9 +337,9 @@ int coli_cuda_tensor_upload_g(ColiCudaTensor **tensor, const void *weights, cons
int coli_cuda_matmul(ColiCudaTensor **tensor, float *y, const float *x,
const void *weights, const float *scales,
int fmt, int S, int I, int O, int device){
int fmt, int S, int I, int O, int device, int gs){
if(!g_cuda.available) return 0;
return g_cuda.matmul(tensor, y, x, weights, scales, fmt, S, I, O, device);
return g_cuda.matmul(tensor, y, x, weights, scales, fmt, S, I, O, device, gs);
}
void coli_cuda_tensor_free(ColiCudaTensor *tensor){
+105 -5
View File
@@ -256,9 +256,17 @@ static inline int spec_pinned(void){ return g_spec_pin && g_spec_live; }
static void matmul_qt_ex(float *y, const float *x, QT *w, int S, int allow_idot);
static void matmul_qt(float *y, const float *x, QT *w, int S){ matmul_qt_ex(y,x,w,S,1); }
/* fmt=4 fused gate+up (defined later, after the quant kernels) */
static void matmul_i4_grouped_pair(float *yg, float *yu, const float *x,
const uint8_t *qg, const float *sg,
const uint8_t *qu, const float *su,
int S, int I, int O, int gs);
static void expert_gate_up(float *g,float *u,const float *x,QT *wg,QT *wu,int S){
if(!g_no_fused_pair&&!spec_pinned()&&S==1&&wg->fmt==2&&wu->fmt==2&&wg->I==wu->I&&wg->O==wu->O)
matmul_i4_pair(g,u,x,wg->q4,wg->s,wu->q4,wu->s,wg->I,wg->O);
else if(!g_no_fused_pair&&S==1&&wg->fmt==4&&wu->fmt==4&&wg->I==wu->I&&wg->O==wu->O&&wg->gs==wu->gs)
matmul_i4_grouped_pair(g,u,x,wg->q4,wg->s,wu->q4,wu->s,S,wg->I,wg->O,wg->gs);
else { matmul_qt(g,x,wg,S); matmul_qt(u,x,wu,S); }
}
@@ -448,6 +456,70 @@ static float *falloc(int64_t n){
float *p=malloc((size_t)n*sizeof(float)); if(!p){fprintf(stderr,"OOM\n");exit(1);} return p; }
/* Fused gate+up for grouped int4 (fmt=4): computes both yg[S,O] and yu[S,O] from
* the same x[S,I], reading x once instead of twice saves ~33% of expert-matmul time at decode.
* The per-group scale logic matches matmul_i4_grouped exactly. */
static void matmul_i4_grouped_pair(float *yg, float *yu, const float *x,
const uint8_t *qg, const float *sg,
const uint8_t *qu, const float *su,
int S, int I, int O, int gs){
int rb=(I+1)/2; int ng=(I+gs-1)/gs;
#pragma omp parallel for schedule(static)
for(int o=0;o<O;o++){
const uint8_t *wg=qg+(int64_t)o*rb; const uint8_t *wu2=qu+(int64_t)o*rb;
const float *sgl=sg+(int64_t)o*ng; const float *sul=su+(int64_t)o*ng;
for(int s=0;s<S;s++){
const float *xs=x+(int64_t)s*I;
float ag=0, au=0;
for(int g=0; g*gs<I; g++){
int base=g*gs; int glen=gs; if(base+glen>I) glen=I-base;
float scg=sgl[g], scu=sul[g];
int i=base;
#ifdef __AVX2__
const __m128i m4=_mm_set1_epi8(0x0F); const __m256i b8=_mm256_set1_epi32(8);
__m256 accg=_mm256_setzero_ps(), accu=_mm256_setzero_ps();
for(; i+16<=base+glen; i+=16){
__m128i byg=_mm_loadl_epi64((const __m128i*)(wg+(i>>1)));
__m128i log=_mm_and_si128(byg,m4),hig=_mm_and_si128(_mm_srli_epi16(byg,4),m4);
__m128i nibg=_mm_unpacklo_epi8(log,hig);
__m256 w0g=_mm256_cvtepi32_ps(_mm256_sub_epi32(_mm256_cvtepu8_epi32(nibg),b8));
__m256 w1g=_mm256_cvtepi32_ps(_mm256_sub_epi32(_mm256_cvtepu8_epi32(_mm_srli_si128(nibg,8)),b8));
accg=_mm256_fmadd_ps(_mm256_loadu_ps(xs+i), w0g, accg);
accg=_mm256_fmadd_ps(_mm256_loadu_ps(xs+i+8), w1g, accg);
__m128i byu=_mm_loadl_epi64((const __m128i*)(wu2+(i>>1)));
__m128i lou=_mm_and_si128(byu,m4),hiu=_mm_and_si128(_mm_srli_epi16(byu,4),m4);
__m128i nibu=_mm_unpacklo_epi8(lou,hiu);
__m256 w0u=_mm256_cvtepi32_ps(_mm256_sub_epi32(_mm256_cvtepu8_epi32(nibu),b8));
__m256 w1u=_mm256_cvtepi32_ps(_mm256_sub_epi32(_mm256_cvtepu8_epi32(_mm_srli_si128(nibu,8)),b8));
accu=_mm256_fmadd_ps(_mm256_loadu_ps(xs+i), w0u, accu);
accu=_mm256_fmadd_ps(_mm256_loadu_ps(xs+i+8), w1u, accu);
}
ag+=hsum256(accg)*scg; au+=hsum256(accu)*scu;
#endif
for(; i+1<base+glen; i+=2){
uint8_t bg=wg[i>>1], bu=wu2[i>>1];
ag+=(xs[i]*(float)((int)(bg&0xF)-8)+xs[i+1]*(float)((int)(bg>>4)-8))*scg;
au+=(xs[i]*(float)((int)(bu&0xF)-8)+xs[i+1]*(float)((int)(bu>>4)-8))*scu;
}
if(i<base+glen){
uint8_t bg=wg[i>>1], bu=wu2[i>>1];
ag+=xs[i]*(float)((int)(bg&0xF)-8)*scg;
au+=xs[i]*(float)((int)(bu&0xF)-8)*scu;
}
}
yg[(int64_t)s*O+o]=ag; yu[(int64_t)s*O+o]=au;
}
}
}
/* allow_idot=0: forza il kernel int4/int8 ESATTO (attivazioni f32). Serve alle proiezioni di
* attenzione: sono sensibili alla quantizzazione int8 delle attivazioni dell'IDOT. Misurato su
* GLM-5.2 int4, 1023 token, log-lik -5040.33 (esatto) -> -5160.47 (IDOT) = +0.117 nat/token,
* ~+12% perplexity. Gli altri matmul del prefill (o_proj, kv_b, expert) tengono l'IDOT.
* EN: allow_idot=0 forces the EXACT int4/int8 kernel (f32 activations). The attention
* projections need it: IDOT's int8 activation quantization costs +0.117 nats/token there
* (~+12% perplexity), measured. Every other prefill matmul keeps IDOT as before. */
static void matmul_qt_ex(float *y, const float *x, QT *w, int S, int allow_idot){
#ifdef COLI_METAL
if(g_metal_enabled && S>=g_metal_gemm_min && !spec_pinned() && (w->fmt==1||w->fmt==2) && !omp_in_parallel()){
@@ -459,7 +531,7 @@ static void matmul_qt_ex(float *y, const float *x, QT *w, int S, int allow_idot)
if(g_cuda_enabled && w->cuda_eligible && !w->cuda_failed && w->fmt!=5 && !omp_in_parallel()){
const void *weights = w->fmt==0 ? (const void*)w->qf
: w->fmt==1 ? (const void*)w->q8 : (const void*)w->q4;
if(coli_cuda_matmul(&w->cuda,y,x,weights,w->s,w->fmt,S,w->I,w->O,w->cuda_device)) return;
if(coli_cuda_matmul(&w->cuda,y,x,weights,w->s,w->fmt,S,w->I,w->O,w->cuda_device,w->gs)) return;
w->cuda_failed=1;
fprintf(stderr,"[CUDA] tensor [%d,%d] on device %d disabled after an error; falling back to CPU\n",
w->O,w->I,w->cuda_device);
@@ -934,13 +1006,14 @@ static void qt_cuda_colocate(QT *dst,const QT *src){
}
static void layer_cuda_shard_kvb(Layer *l,int H,int Q,int V){
if(!g_cuda_enabled||!g_cuda_dense||g_cuda_ndev<2||l->kv_b.fmt==0)return;
int rb=l->kv_b.fmt==1?l->kv_b.I:(l->kv_b.fmt==2?(l->kv_b.I+1)/2:(l->kv_b.I+3)/4);
int rb=l->kv_b.fmt==1?l->kv_b.I:
(l->kv_b.fmt==2||l->kv_b.fmt==4)?(l->kv_b.I+1)/2:(l->kv_b.I+3)/4;
const uint8_t *weights=l->kv_b.fmt==1?(const uint8_t*)l->kv_b.q8:l->kv_b.q4;
for(int d=0,h0=0;d<g_cuda_ndev;d++){
int hn=H/g_cuda_ndev+(d<H%g_cuda_ndev),rows=hn*(Q+V);
const void *part=weights+(int64_t)h0*(Q+V)*rb;
const float *scale=l->kv_b.s+(int64_t)h0*(Q+V);
if(!coli_cuda_tensor_upload(&l->kv_b_shard[d],part,scale,l->kv_b.fmt,l->kv_b.I,rows,g_cuda_devices[d]))return;
const float *scale=l->kv_b.s+(int64_t)h0*(Q+V)*(l->kv_b.gs>0?(l->kv_b.I+l->kv_b.gs-1)/l->kv_b.gs:1);
if(!coli_cuda_tensor_upload_g(&l->kv_b_shard[d],part,scale,l->kv_b.fmt,l->kv_b.I,rows,g_cuda_devices[d],l->kv_b.gs))return;
l->shard_h0[d]=h0;l->shard_hn[d]=hn;l->n_kv_b_shard++;h0+=hn;
}
int old=-1;for(int i=0;i<g_cuda_ndev;i++)if(g_cuda_devices[i]==l->kv_b.cuda_device)old=i;
@@ -1122,6 +1195,14 @@ static void model_init(Model *m, const char *snap, int cap, int ebits, int dbits
static void embed_row(Model *m, int tok, float *x){
int D=m->c.hidden; QT *e=&m->embed;
if(e->fmt==0){ memcpy(x, e->qf+(int64_t)tok*D, D*sizeof(float)); return; }
if(e->fmt==4){ /* grouped int4: per-group scale (embed/lm_head at io_bits, usually fmt 0/1) */
const uint8_t *q=e->q4+(int64_t)tok*((D+1)/2); int gs=e->gs,ng=(D+gs-1)/gs;
const float *scl=e->s+(int64_t)tok*ng;
for(int g=0;g*gs<D;g++){ int base=g*gs,glen=gs; if(base+glen>D)glen=D-base; float s=scl[g];
for(int i=base;i+1<base+glen;i+=2){ uint8_t byte=q[i>>1]; x[i]=(float)((int)(byte&0xF)-8)*s;
x[i+1]=(float)((int)(byte>>4)-8)*s; }
if(glen&1){ uint8_t byte=q[(base+glen-1)>>1]; x[base+glen-1]=(float)((int)(byte&0xF)-8)*s; } }
return; }
if(e->fmt==1){ const int8_t *q=e->q8+(int64_t)tok*D; float s=e->s[tok];
for(int i=0;i<D;i++) x[i]=(float)q[i]*s; return; }
if(e->fmt==2){ const uint8_t *q=e->q4+(int64_t)tok*((D+1)/2); float s=e->s[tok]; /* int4 */
@@ -1885,6 +1966,13 @@ static void qt_matvec_rows(const QT *t, int r0, int n, const float *x, float *y)
int I=t->I;
for(int j=0;j<n;j++){ int row=r0+j; double a=0;
if(t->fmt==0){ const float *w=t->qf+(int64_t)row*I; for(int i=0;i<I;i++) a+=(double)w[i]*x[i]; }
else if(t->fmt==4){ /* grouped int4: per-group scale */
const uint8_t *w=t->q4+(int64_t)row*((I+1)/2); int gs=t->gs,ng=(I+gs-1)/gs;
const float *scl=t->s+(int64_t)row*ng;
for(int g=0;g*gs<I;g++){ int base=g*gs,glen=gs; if(base+glen>I)glen=I-base; float sc=scl[g]; float acc=0;
for(int i=base;i+1<base+glen;i+=2){ uint8_t b=w[i>>1]; acc+=((int)(b&0xF)-8)*x[i]+((int)(b>>4)-8)*x[i+1]; }
if(glen&1){ uint8_t b=w[(base+glen-1)>>1]; acc+=((int)(b&0xF)-8)*x[base+glen-1]; }
a+=acc*sc; } }
else if(t->fmt==1){ const int8_t *w=t->q8+(int64_t)row*I; float s=t->s[row];
float acc=0; for(int i=0;i<I;i++) acc+=(float)w[i]*x[i]; a=acc*s; }
else if(t->fmt==2){ const uint8_t *w=t->q4+(int64_t)row*((I+1)/2); float s=t->s[row]; float acc=0;
@@ -4479,9 +4567,21 @@ static void run_text(Model *m, const char *snap, const char *prompt, int ngen){
grammar_setup(&T); /* metodo F: GRAMMAR=file.gbnf (#48) */
if(g_temp<0) g_temp=0.7f; /* auto: 0.7, NON l'1.0 ufficiale — la coda della
* distribuzione int4 e' rumore di quantizzazione */
int cap=(int)strlen(prompt)+16; int *pids=malloc(cap*sizeof(int));
int cap=(int)strlen(prompt)+16; int *pids=malloc((cap+2)*sizeof(int));
int np=tok_encode(&T,prompt,(int)strlen(prompt),pids,cap);
if(np<1){ fprintf(stderr,"prompt is empty after tokenization\n"); return; }
/* GLM prefix (#108): every GLM training sequence starts [gMASK]<sop>; without it the
* model is out-of-distribution and generates garbage. Serve/SCORE modes prepend it;
* run_text must too. CHAT_TEMPLATE=0 disables (raw prompt, for debugging/non-GLM).
* A prompt that already starts with the prefix passes through intact. */
int templ=getenv("CHAT_TEMPLATE")?atoi(getenv("CHAT_TEMPLATE")):1;
if(templ){
int gmask=tok_id_of(&T,"[gMASK]"), sop=tok_id_of(&T,"<sop>");
if(gmask>=0 && sop>=0 && (np<2 || pids[0]!=gmask || pids[1]!=sop)){
memmove(pids+2,pids,np*sizeof(int));
pids[0]=gmask; pids[1]=sop; np+=2;
}
}
printf("prompt: %d tokens | generating up to %d (EOS stop=%d) | n-gram draft=%d\n", np, ngen, eos, g_draft);
fputs(prompt,stdout); fflush(stdout);
kv_alloc(m, np+ngen+g_draft+2);
+3 -3
View File
@@ -30,9 +30,9 @@ int main(){
for(int i=0;i<D;i++)ds[i]=0.006f+(i%7)*0.0002f;
for(size_t i=0;i<x.size();i++)x[i]=std::sin((float)(i+1)*0.013f)*2.f;
ColiCudaTensor *g=nullptr,*u=nullptr,*d=nullptr;
if(!coli_cuda_tensor_upload(&g,hidden.data(),hs.data(),2,D,I,device)||
!coli_cuda_tensor_upload(&u,hidden.data(),hs.data(),2,D,I,device)||
!coli_cuda_tensor_upload(&d,down.data(),ds.data(),2,I,D,device))return 2;
if(!coli_cuda_tensor_upload(&g,hidden.data(),hs.data(),2,D,I,device,0)||
!coli_cuda_tensor_upload(&u,hidden.data(),hs.data(),2,D,I,device,0)||
!coli_cuda_tensor_upload(&d,down.data(),ds.data(),2,I,D,device,0))return 2;
for(int rows: {1,2,4,8}){
double scalar=run(g,u,d,x.data(),a.data(),rows,3,0);
double packed=run(g,u,d,x.data(),b.data(),rows,3,1);
+11 -11
View File
@@ -99,7 +99,7 @@ int main(int argc, char **argv) {
const int8_t q8b[8]={-1,-2,-3,-4, 1,-2,3,-4};
const float s8b[2]={1.f,.5f},want8b[4]={10.f,15.f,-3.f,-2.5f};
if(!coli_cuda_tensor_update(t8,q8b,s8b)||
!coli_cuda_matmul(&t8,got,x,q8b,s8b,1,2,4,2,d0)||
!coli_cuda_matmul(&t8,got,x,q8b,s8b,1,2,4,2,d0,0)||
!close_enough(got,want8b,4))return 1;
/* Rows [-8,-1,0,7] and [1,2,3,4], packed low nibble first. */
@@ -107,26 +107,26 @@ int main(int argc, char **argv) {
const float s4[2] = {1.0f, 0.25f};
const float want4[2] = {-34.0f, -2.5f};
ColiCudaTensor *t4 = nullptr;
if (!coli_cuda_matmul(&t4, got, x, q4, s4, 2, 1, 4, 2, d1) || !close_enough(got, want4, 2)) return 1;
if (!coli_cuda_matmul(&t4, got, x, q4, s4, 2, 1, 4, 2, d1, 0) || !close_enough(got, want4, 2)) return 1;
const uint8_t q2[2] = {0xe4, 0x1b};
const float s2[2] = {0.5f, 2.0f};
const float want2[2] = {-2.0f, 12.0f};
ColiCudaTensor *t2 = nullptr;
if (!coli_cuda_matmul(&t2, got, x, q2, s2, 3, 1, 4, 2, d1) || !close_enough(got, want2, 2)) return 1;
if (!coli_cuda_matmul(&t2, got, x, q2, s2, 3, 1, 4, 2, d1, 0) || !close_enough(got, want2, 2)) return 1;
const float wf[8] = {1, 0, -1, 2, 0.5f, 0.5f, 0.5f, 0.5f};
const float wantf[2] = {-10.0f, -1.0f};
ColiCudaTensor *tf = nullptr;
if (!coli_cuda_matmul(&tf, got, x, wf, nullptr, 0, 1, 4, 2, d0) || !close_enough(got, wantf, 2)) return 1;
if (!coli_cuda_matmul(&tf, got, x, wf, nullptr, 0, 1, 4, 2, d0, 0) || !close_enough(got, wantf, 2)) return 1;
const float eg[8] = {1,0,0,0, 0,1,0,0};
const float eu[8] = {1,0,0,0, 0,1,0,0};
const float ed[8] = {1,0, 0,1, 1,1, 1,-1};
ColiCudaTensor *tg=nullptr,*tu=nullptr,*td=nullptr;
if (!coli_cuda_tensor_upload(&tg,eg,nullptr,0,4,2,d0) ||
!coli_cuda_tensor_upload(&tu,eu,nullptr,0,4,2,d0) ||
!coli_cuda_tensor_upload(&td,ed,nullptr,0,2,4,d0)) return 1;
if (!coli_cuda_tensor_upload(&tg,eg,nullptr,0,4,2,d0,0) ||
!coli_cuda_tensor_upload(&tu,eu,nullptr,0,4,2,d0,0) ||
!coli_cuda_tensor_upload(&td,ed,nullptr,0,2,4,d0,0)) return 1;
float expert[8], want_expert[8];
for(int s=0;s<2;s++){
float a=x[s*4], b=x[s*4+1];
@@ -144,7 +144,7 @@ int main(int argc, char **argv) {
const float aw[16]={1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};
const float aq[4]={1,2,.5f,-.5f},al[12]={1,0,0,0, 0,1,0,0, 0,0,1,0};
const float ar[6]={1,0, 0,1, 1,1};float actx[2],aref[2];
ColiCudaTensor *at=nullptr;if(!coli_cuda_tensor_upload(&at,aw,nullptr,0,4,4,d0))return 1;
ColiCudaTensor *at=nullptr;if(!coli_cuda_tensor_upload(&at,aw,nullptr,0,4,4,d0,0))return 1;
float score[3];for(int t=0;t<3;t++)score[t]=aq[0]*al[t*4]+aq[1]*al[t*4+1]+aq[2]*ar[t*2]+aq[3]*ar[t*2+1];
float mx=score[0],z=0;for(int t=1;t<3;t++)mx=score[t]>mx?score[t]:mx;
for(int t=0;t<3;t++){score[t]=std::exp(score[t]-mx);z+=score[t];}for(int t=0;t<3;t++)score[t]/=z;
@@ -163,9 +163,9 @@ int main(int argc, char **argv) {
for(int i=0;i<32;i++)ws4[i]=0.01f+(i%5)*0.002f;
for(int i=0;i<64;i++)gx4[i]=std::sin((float)(i+1)*0.17f)*2.f;
ColiCudaTensor *g4=nullptr,*u4=nullptr,*d4=nullptr;
if(!coli_cuda_tensor_upload(&g4,w4,ws4,2,32,32,d0)||
!coli_cuda_tensor_upload(&u4,w4,ws4,2,32,32,d0)||
!coli_cuda_tensor_upload(&d4,w4,ws4,2,32,32,d0))return 1;
if(!coli_cuda_tensor_upload(&g4,w4,ws4,2,32,32,d0,0)||
!coli_cuda_tensor_upload(&u4,w4,ws4,2,32,32,d0,0)||
!coli_cuda_tensor_upload(&d4,w4,ws4,2,32,32,d0,0))return 1;
ColiCudaTensor *gg4[2]={g4,g4},*ug4[2]={u4,u4},*dg4[2]={d4,d4};
if(!coli_cuda_expert_group(gg4,ug4,dg4,group_rows,2,scalar4,gx4))return 1;
setenv("COLI_CUDA_TC_INT4","1",1);
+1 -1
View File
@@ -119,7 +119,7 @@ int main(void){
for(int i=0;i<O;i++) sc[i]=0.01f+0.001f*(i%7);
for(size_t i=0;i<(size_t)S*K;i++) x[i]=rndf();
ColiCudaTensor *t=NULL;
ok&=coli_cuda_matmul(&t,ref,x,w4,sc,2,S,K,O,0); /* host path = reference */
ok&=coli_cuda_matmul(&t,ref,x,w4,sc,2,S,K,O,0,0); /* host path = reference */
float *xd=(float*)coli_cuda_pipe_alloc(0,(size_t)S*K*4);
float *yd=(float*)coli_cuda_pipe_alloc(0,(size_t)S*O*4);
ok&=coli_cuda_pipe_upload(0,xd,x,(size_t)S*K*4);
+704
View File
@@ -0,0 +1,704 @@
#!/usr/bin/env python3
"""
diag_harness.py — Comprehensive model diagnostic harness for the colibri GLM-5.2 engine.
Runs a full campaign of tests against any model snapshot:
Phase 0 (system): startup telemetry — GPU, RAM, cache cap, load time, MTP, idot kernel
Phase 1 (smoke): correctness — curated prompts, coherence checks, corruption detection
Phase 2 (diagnostic): deep x-ray — full PROFILE breakdown, routing, MTP, disk-split, CUDA tier
Phase 3 (quality): benchmark accuracy — hellaswag/arc_challenge/mmlu via eval_glm.py SCORE
Phase 4 (throughput): tok/s with and without MTP speculation
Phase 5 (report): structured JSON + human-readable Markdown summary
Usage:
python tools/diag_harness.py --snap /path/to/model --phase all
python tools/diag_harness.py --snap /path/to/model --phase smoke --ngen 64
python tools/diag_harness.py --snap /path/to/model --phase quality --quality-limit 200
Output goes to --out (default ./diag_results/<timestamp>/). Each phase writes a raw log
(<phase>_<run>.log) and all metrics are collected into report.json + report.md.
Design notes:
- stdout and stderr are captured separately via subprocess.PIPE. The engine streams
generated text to stdout (interleaved with prompt + PROFILE stats), but TOKENS=1 dumps
clean token-id lists to stderr — that is the primary text-capture path.
- Every regex is anchored to the exact printf format strings in glm.c (verified against
profile_print line 3853, run_text line 3948, the banner line 5299, etc.).
- Subprocess calls have a hard timeout (default 600s) and are killed cleanly on expiry.
- A single phase can be run standalone; results accumulate in the output dir.
"""
import os, sys, re, json, time, argparse, subprocess, signal, traceback
from datetime import datetime
from pathlib import Path
# ---------------------------------------------------------------------------
# PROMPT SUITE — curated across categories. "expect" is a case-insensitive
# substring checked against the generated text (None = coherence-only check).
# ---------------------------------------------------------------------------
PROMPTS = [
{"id":"fact_capital", "cat":"factual", "prompt":"The capital of France is",
"expect":"Paris", "note":"basic world knowledge"},
{"id":"fact_boiling", "cat":"factual", "prompt":"What is the boiling point of water in Celsius?",
"expect":"100", "note":"basic science"},
{"id":"fact_planet", "cat":"factual", "prompt":"What planet is closest to the Sun?",
"expect":"Mercury","note":"astronomy fact"},
{"id":"math_mult", "cat":"math", "prompt":"What is 15 times 12?",
"expect":"180", "note":"2-digit multiplication"},
{"id":"math_add", "cat":"math", "prompt":"What is 847 plus 153?",
"expect":"1000", "note":"3-digit addition"},
{"id":"reason_train", "cat":"reasoning", "prompt":"If a train travels 60 mph for 2.5 hours, how far does it go?",
"expect":"150", "note":"rate-time-distance"},
{"id":"code_factorial","cat":"code", "prompt":"Write a Python function that computes the factorial of a number.",
"expect":"def", "note":"code generation"},
{"id":"explain_nn", "cat":"explanation","prompt":"Explain what a neural network is in one sentence.",
"expect":None, "note":"coherence check"},
{"id":"creative_story","cat":"creative", "prompt":"Write a one-sentence story about a lighthouse.",
"expect":None, "note":"creative coherence"},
{"id":"edge_hello", "cat":"edge", "prompt":"Hello, how are you today?",
"expect":None, "note":"conversational opener"},
{"id":"edge_the", "cat":"edge", "prompt":"The weather today is",
"expect":None, "note":"simple continuation"},
{"id":"edge_repeat", "cat":"edge", "prompt":"The quick brown fox jumps over the lazy dog. The quick brown fox",
"expect":None, "note":"repetition-bait"},
]
# ---------------------------------------------------------------------------
# METRIC EXTRACTION — each parser is (name, compiled_regex, group_index, cast).
# Regexes match the EXACT printf format strings in glm.c.
# ---------------------------------------------------------------------------
def _f1(g): return float(g)
def _i1(g): return int(g)
# Build parsers as (regex, lambda(match)->value) for clarity
RX = {
# stdout: run_text summary line (line 3948)
"decode_toks": (re.compile(r"decode (\d+) tokens in"), lambda m: int(m.group(1))),
"decode_secs": (re.compile(r"decode \d+ tokens in ([\d.]+)s"), lambda m: float(m.group(1))),
"decode_tps": (re.compile(r"decode \d+ tokens in [\d.]+s \(([\d.]+) tok/s\)"), lambda m: float(m.group(1))),
"prefill_toks": (re.compile(r"prefill (\d+) tokens in"), lambda m: int(m.group(1))),
"prefill_secs": (re.compile(r"prefill \d+ tokens in ([\d.]+)s"), lambda m: float(m.group(1))),
"hit_rate": (re.compile(r"expert hit rate ([\d.]+)%"), lambda m: float(m.group(1))),
"rss_gb": (re.compile(r"RSS ([\d.]+) GB"), lambda m: float(m.group(1))),
"experts_per_tok":(re.compile(r"experts loaded/token: ([\d.]+)"), lambda m: float(m.group(1))),
"mtp_accept": (re.compile(r"MTP acceptance ([\d.]+)%"), lambda m: float(m.group(1))),
"mtp_acc_cnt": (re.compile(r"MTP acceptance \d+% \((\d+)/(\d+)\)"), lambda m: (int(m.group(1)), int(m.group(2)))),
"spec_tok_per_fw":(re.compile(r"speculation: ([\d.]+) tokens/forward"),lambda m: float(m.group(1))),
# stdout: PROFILE lines (profile_print, line 3853-3864)
"prof_expert_disk": (re.compile(r"expert-disk ([\d.]+)s service"), lambda m: float(m.group(1))),
"prof_expert_wait": (re.compile(r"service / ([\d.]+)s wait"), lambda m: float(m.group(1))),
"prof_expert_mm": (re.compile(r"expert-matmul ([\d.]+)s"), lambda m: float(m.group(1))),
"prof_attention": (re.compile(r"\| attention ([\d.]+)s"), lambda m: float(m.group(1))),
"prof_kvb": (re.compile(r"including kvb ([\d.]+)s"), lambda m: float(m.group(1))),
"prof_lm_head": (re.compile(r"lm_head ([\d.]+)s"), lambda m: float(m.group(1))),
"prof_other": (re.compile(r"\| other ([\d.]+)s"), lambda m: float(m.group(1))),
# stdout: banner (line 5299)
"load_secs": (re.compile(r"loaded in ([\d.]+)s"), lambda m: float(m.group(1))),
"resident_mb": (re.compile(r"resident dense: ([\d.]+) MB"), lambda m: float(m.group(1))),
"mtp_status": (re.compile(r"MTP (ACTIVE|absent)"), lambda m: m.group(1)),
"n_layers": (re.compile(r"layers=(\d+) experts=(\d+)"), lambda m: int(m.group(1))),
"n_experts": (re.compile(r"layers=(\d+) experts=(\d+)"), lambda m: int(m.group(2))),
# stdout: banner idot kernel
"idot_kernel": (re.compile(r"idot: (\S+) =="), lambda m: m.group(1)),
"cache_cap": (re.compile(r"cache=(\d+) experts/layer"), lambda m: int(m.group(1))),
# stderr: RAM_GB (line 5086-5112)
"ram_budget": (re.compile(r"\[RAM_GB=([\d.]+)"), lambda m: float(m.group(1))),
"cap_lowered": (re.compile(r"cap lowered (\d+)->(\d+)"), lambda m: (int(m.group(1)), int(m.group(2)))),
"cap_raised": (re.compile(r"cap raised (\d+)->(\d+)"), lambda m: (int(m.group(1)), int(m.group(2)))),
"cap_ok": (re.compile(r"cap=(\d+) ok"), lambda m: int(m.group(1))),
# stderr: CUDA (backend_cuda.cu:389)
"cuda_device": (re.compile(r"\[CUDA\] device (\d+): (.*?), ([\d.]+) GB VRAM, sm_(\d)(\d)"),
lambda m: {"id":int(m.group(1)),"name":m.group(2).strip(),"vram_gb":float(m.group(3)),
"sm":f"{m.group(4)}.{m.group(5)}"}),
"cuda_mode": (re.compile(r"\[CUDA\] mode: (.+)"), lambda m: m.group(1)),
"cuda_tier": (re.compile(r"CUDA expert tier: (\d+) resident experts \(([\d.]+) GB\)"),
lambda m: {"resident":int(m.group(1)),"vram_gb":float(m.group(2))}),
# stderr: TOKENS dump (line 4010-4012)
"tokens_dump": (re.compile(r"^\[TOKENS\] (\d+) generated:(.*)$", re.MULTILINE),
lambda m: [int(x) for x in m.group(2).split()]),
# stderr: per-16-token progress (emit_stream line 3742)
"progress_tps": (re.compile(r"t=(\d+)\s+RSS ([\d.]+) GB\s+hit ([\d.]+)%\s+([\d.]+) tok/s\s+([\d.]+) tok/fw"),
lambda m: {"tok":int(m.group(1)),"rss":float(m.group(2)),"hit":float(m.group(3)),
"tps":float(m.group(4)),"tpf":float(m.group(5))}),
# stderr: DSA, USAGE, KV startup lines
"usage_loaded": (re.compile(r"\[USAGE\].*?(\d+) selections"), lambda m: int(m.group(1))),
"kv_slots": (re.compile(r"\[KV\].*?(\d+) context slots"), lambda m: int(m.group(1))),
}
def extract_metrics(stdout: str, stderr: str) -> dict:
"""Parse all metrics from the engine's stdout+stderr output."""
text_out = stdout or ""
text_err = stderr or ""
metrics = {}
# For most parsers we search BOTH streams (engine is inconsistent about which
# channel a given line lands on). Some are stream-specific (noted below).
combined = text_out + "\n" + text_err
for name, (rx, fn) in RX.items():
m = rx.search(combined)
if m:
try: metrics[name] = fn(m)
except (ValueError, IndexError): pass
# TOKENS dump is stderr-only and may appear once; grab it explicitly
m = RX["tokens_dump"][0].search(text_err)
if m:
try: metrics["tokens_dump"] = RX["tokens_dump"][1](m)
except: pass
# Multiple CUDA devices — collect all
cuda_devs = []
for m in re.finditer(r"\[CUDA\] device (\d+): (.*?), ([\d.]+) GB VRAM, sm_(\d)(\d)", text_err):
cuda_devs.append({"id":int(m.group(1)),"name":m.group(2).strip(),
"vram_gb":float(m.group(3)),"sm":f"{m.group(4)}.{m.group(5)}"})
if cuda_devs: metrics["cuda_devices"] = cuda_devs
# Multiple progress checkpoints — collect the full curve
progress = []
for m in RX["progress_tps"][0].finditer(text_err):
try:
progress.append(RX["progress_tps"][1](m))
except: pass
if progress: metrics["progress_curve"] = progress
# Multiple PROFILE lines — there are two (prefill + decode). Keep both.
prof_lines = [(i, line) for i, line in enumerate(text_out.splitlines()) if line.startswith("PROFILE:")]
for idx, (line_no, line) in enumerate(prof_lines):
label = "prefill_profile" if idx == 0 else "decode_profile"
p = {}
for pname, (rx, fn) in RX.items():
if not pname.startswith("prof_"): continue
mm = rx.search(line)
if mm:
try: p[pname] = fn(mm)
except: pass
if p: metrics[label] = p
return metrics
# ---------------------------------------------------------------------------
# ENGINE RUNNER — subprocess wrapper with timeout, signal handling, logging.
# ---------------------------------------------------------------------------
class EngineRunner:
def __init__(self, glm_path, snap, out_dir, default_env=None, timeout=600):
self.glm = str(glm_path)
self.snap = str(snap)
self.out_dir = Path(out_dir)
self.out_dir.mkdir(parents=True, exist_ok=True)
self.default_env = default_env or {}
self.timeout = timeout
self.default_cap = 75 # production default (matches bench_full.sh)
def run_prompt(self, prompt, ngen=64, env_extra=None, log_name=None, cap=None):
"""Run the engine in PROMPT mode and return (stdout, stderr, returncode, elapsed)."""
env = dict(os.environ, SNAP=self.snap, PROMPT=prompt, NGEN=str(ngen))
env.update(self.default_env)
if env_extra: env.update(env_extra)
env["TOKENS"] = "1" # always capture token ids for reliable text extraction
cmd = [self.glm, str(cap if cap is not None else self.default_cap)]
return self._exec(cmd, env, log_name)
def run_score(self, score_file, cap=None, env_extra=None, log_name=None):
"""Run the engine in SCORE (log-likelihood) mode."""
env = dict(os.environ, SNAP=self.snap, SCORE=score_file)
env.update(self.default_env)
if env_extra: env.update(env_extra)
cmd = [self.glm, str(cap if cap is not None else self.default_cap)]
return self._exec(cmd, env, log_name)
def _exec(self, cmd, env, log_name):
t0 = time.time()
log_path = self.out_dir / (log_name or f"run_{int(t0)}.log")
try:
proc = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, encoding="utf-8", errors="replace")
try:
stdout, stderr = proc.communicate(timeout=self.timeout)
rc = proc.returncode
except subprocess.TimeoutExpired:
proc.kill()
stdout, stderr = proc.communicate()
rc = -1
stderr = (stderr or "") + f"\n[TIMEOUT after {self.timeout}s]\n"
except Exception as e:
stdout, stderr, rc = "", f"[EXCEPTION] {e}\n{traceback.format_exc()}", -2
elapsed = time.time() - t0
# Write raw log (both streams, clearly delimited)
with open(log_path, "w", encoding="utf-8") as f:
f.write(f"=== CMD: {' '.join(cmd)}\n=== ELAPSED: {elapsed:.1f}s\n=== RC: {rc}\n\n")
f.write("--- STDOUT ---\n"); f.write(stdout or ""); f.write("\n")
f.write("--- STDERR ---\n"); f.write(stderr or ""); f.write("\n")
return stdout or "", stderr or "", rc, elapsed
# ---------------------------------------------------------------------------
# TEXT RECOVERY — decode the [TOKENS] ids back to text using the tokenizer.
# Falls back to stdout extraction if tokenizer/tokens unavailable.
# ---------------------------------------------------------------------------
def recover_text(stdout, stderr, prompt, tokenizer=None):
"""Extract generated text. Primary: TOKENS dump decoded. Fallback: stdout parse."""
# Method 1: decode TOKENS ids via tokenizer
if tokenizer:
m = re.search(r"^\[TOKENS\] \d+ generated:(.*)$", stderr, re.MULTILINE)
if m:
ids = [int(x) for x in m.group(1).split()]
if ids:
try:
return tokenizer.decode(ids), "tokens"
except Exception: pass
# Method 2: stdout — text is between the prompt string and "PROFILO" or "\n---"
text = stdout or ""
# Find the prompt in stdout, take everything after it up to PROFILO
idx = text.find(prompt)
if idx >= 0:
after = text[idx + len(prompt):]
cut = after.find("PROFILO")
if cut >= 0:
return after[:cut].strip(), "stdout"
cut = after.find("\n---")
if cut >= 0:
return after[:cut].strip(), "stdout"
return "", "none"
# ---------------------------------------------------------------------------
# CORRUPTION / COHERENCE CHECKS
# ---------------------------------------------------------------------------
def check_repetition(token_ids):
"""Detect degenerate repetition: same token 3+ consecutive times."""
if not token_ids or len(token_ids) < 6:
return False, 0
max_run = 1; cur = 1
for i in range(1, len(token_ids)):
if token_ids[i] == token_ids[i-1]: cur += 1; max_run = max(max_run, cur)
else: cur = 1
return max_run >= 3, max_run
def check_expected(text, expect):
"""Check if expected substring appears case-insensitively in the first 200 chars."""
if not expect or not text: return None
return expect.lower() in text[:200].lower()
# ---------------------------------------------------------------------------
# PHASES
# ---------------------------------------------------------------------------
class DiagnosticHarness:
def __init__(self, args):
self.args = args
self.snap = args.snap
self.glm = args.glm
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
self.out_dir = Path(args.out or f"./diag_results/{ts}")
self.out_dir.mkdir(parents=True, exist_ok=True)
# Base env for all runs
base_env = {"TEMP": "0", "PIPE": "1", "PIPE_WORKERS": "8", "DIRECT": "1"}
if args.ram: base_env["RAM_GB"] = str(args.ram)
if args.cuda:
base_env.update({
"COLI_CUDA": "1",
"COLI_GPU": str(args.gpu) if args.gpu is not None else "0",
"CUDA_DENSE": "1",
"COLI_CUDA_ATTN": "1",
"COLI_CUDA_PIPE": "2",
"COLI_CUDA_PIPE_S_MIN": "1",
})
self.runner = EngineRunner(self.glm, self.snap, self.out_dir, base_env, timeout=args.timeout)
self.runner.default_cap = args.cap
# Load tokenizer for text recovery
self.tokenizer = None
tok_path = os.path.join(self.snap, "tokenizer.json")
if os.path.exists(tok_path):
try:
from tokenizers import Tokenizer
self.tokenizer = Tokenizer.from_file(tok_path)
except Exception as e:
print(f"[warn] could not load tokenizer: {e}", file=sys.stderr)
self.results = {"meta": {"snap": self.snap, "glm": self.glm,
"timestamp": ts, "args": vars(args)},
"phases": {}}
def phase_system(self):
"""Phase 0: minimal run to capture startup telemetry."""
print("\n" + "="*60 + "\nPHASE 0: SYSTEM PROBE\n" + "="*60)
stdout, stderr, rc, elapsed = self.runner.run_prompt(
"Hello", ngen=1, log_name="system_probe.log")
metrics = extract_metrics(stdout, stderr)
result = {
"rc": rc, "elapsed": elapsed,
"load_secs": metrics.get("load_secs"),
"resident_mb": metrics.get("resident_mb"),
"n_layers": metrics.get("n_layers"),
"n_experts": metrics.get("n_experts"),
"mtp_status": metrics.get("mtp_status"),
"idot_kernel": metrics.get("idot_kernel"),
"cache_cap_requested": metrics.get("cache_cap"),
"ram_budget_gb": metrics.get("ram_budget"),
"cap_lowered": metrics.get("cap_lowered"),
"cap_raised": metrics.get("cap_raised"),
"cap_final": metrics.get("cap_ok"),
"cuda_devices": metrics.get("cuda_devices", []),
"cuda_mode": metrics.get("cuda_mode"),
}
# Print summary
print(f" load time: {result['load_secs']:.2f}s" if result['load_secs'] else " load time: FAILED")
print(f" resident: {result['resident_mb']:.0f} MB" if result['resident_mb'] else "")
print(f" layers/exp: {result['n_layers']}/{result['n_experts']}" if result['n_layers'] else "")
print(f" MTP: {result['mtp_status']}")
print(f" idot kernel: {result['idot_kernel']}")
print(f" RAM budget: {result['ram_budget_gb']} GB" if result['ram_budget_gb'] else " RAM budget: auto")
if result['cap_lowered']:
print(f" cache cap: {result['cap_lowered'][0]} -> {result['cap_lowered'][1]} (RAM-lowered)")
elif result['cap_final']:
print(f" cache cap: {result['cap_final']} (ok)")
else:
print(f" cache cap: {result['cache_cap_requested']}")
for d in result['cuda_devices']:
print(f" GPU {d['id']}: {d['name']}, {d['vram_gb']:.1f} GB, sm_{d['sm']}")
if rc != 0 and rc != -1:
print(f" WARNING: engine returned rc={rc}")
self.results["phases"]["system"] = result
return result
def phase_smoke(self):
"""Phase 1: correctness smoke test across curated prompts."""
print("\n" + "="*60 + "\nPHASE 1: CORRECTNESS SMOKE TEST\n" + "="*60)
ngen = self.args.ngen
prompt_results = []
pass_count = 0; total = len(PROMPTS)
for p in PROMPTS:
stdout, stderr, rc, elapsed = self.runner.run_prompt(
p["prompt"], ngen=ngen, log_name=f"smoke_{p['id']}.log")
metrics = extract_metrics(stdout, stderr)
token_ids = metrics.get("tokens_dump", [])
text, method = recover_text(stdout, stderr, p["prompt"], self.tokenizer)
is_rep, max_run = check_repetition(token_ids)
expect_ok = check_expected(text, p.get("expect"))
# Verdict: pass if text is non-empty, no severe repetition, and (if factual) expected found
has_text = bool(text.strip())
ok = has_text and not is_rep
if p.get("expect") and expect_ok is False: ok = False
if ok: pass_count += 1
# Diagnose failure mode for reporting
if not has_text:
fail_reason = "no tokens generated (immediate EOS)"
elif is_rep:
fail_reason = f"repetition loop (max run={max_run})"
elif p.get("expect") and expect_ok is False:
fail_reason = f"expected '{p['expect']}' not found"
else:
fail_reason = ""
prompt_results.append({
"id": p["id"], "cat": p["cat"], "prompt": p["prompt"],
"expect": p.get("expect"), "generated": text[:300],
"expect_match": expect_ok, "repetition": is_rep, "max_run": max_run,
"toks_generated": len(token_ids), "decode_tps": metrics.get("decode_tps"),
"hit_rate": metrics.get("hit_rate"), "rc": rc, "elapsed": elapsed,
"text_method": method, "pass": ok, "fail_reason": fail_reason,
})
status = "PASS" if ok else "FAIL"
extra = f" expect={'Y' if expect_ok else ('N' if expect_ok is False else '-')}" if p.get("expect") else ""
tps = f" {metrics.get('decode_tps',0):.2f}t/s" if metrics.get("decode_tps") else ""
print(f" [{status}] {p['id']:<22} ({p['cat']:<11}) rep={'Y' if is_rep else 'N'}{extra}{tps}")
if text:
preview = text.replace("\n", " ")[:80]
print(f" -> {preview}")
elif rc != 0:
print(f" -> [engine rc={rc}]")
else:
print(f" -> [{fail_reason}]")
result = {"prompts": prompt_results, "pass": pass_count, "total": total,
"pass_rate": 100.0 * pass_count / total if total else 0}
print(f"\n SMOKE SUMMARY: {pass_count}/{total} passed ({result['pass_rate']:.0f}%)")
self.results["phases"]["smoke"] = result
return result
def phase_diagnostic(self):
"""Phase 2: single deep-instrumented run — full PROFILE + routing + MTP."""
print("\n" + "="*60 + "\nPHASE 2: FULL SYSTEM DIAGNOSTIC\n" + "="*60)
diag_env = {
"LOOKA": "1", "DISK_SPLIT": "1", "ROUTE_AGREE": "1",
}
if self.args.cuda:
diag_env["COLI_CUDA_PROFILE"] = "1"
prompt = ("Write a short paragraph explaining how photosynthesis works. "
"Include the roles of sunlight, water, and carbon dioxide.")
stdout, stderr, rc, elapsed = self.runner.run_prompt(
prompt, ngen=self.args.ngen, env_extra=diag_env, log_name="diagnostic.log")
metrics = extract_metrics(stdout, stderr)
text, _ = recover_text(stdout, stderr, prompt, self.tokenizer)
result = {
"rc": rc, "elapsed": elapsed,
"generated_text": text[:500],
"prefill_profile": metrics.get("prefill_profile", {}),
"decode_profile": metrics.get("decode_profile", {}),
"decode_tps": metrics.get("decode_tps"),
"prefill_secs": metrics.get("prefill_secs"),
"hit_rate": metrics.get("hit_rate"),
"rss_gb": metrics.get("rss_gb"),
"experts_per_tok": metrics.get("experts_per_tok"),
"mtp_accept": metrics.get("mtp_accept"),
"mtp_counts": metrics.get("mtp_acc_cnt"),
"spec_tok_per_fw": metrics.get("spec_tok_per_fw"),
"cuda_tier": metrics.get("cuda_tier"),
"progress_curve": metrics.get("progress_curve", []),
}
# Print the PROFILE breakdown
dp = result["decode_profile"]
print(f"\n DECODE TIMING BREAKDOWN (per-bucket, seconds):")
print(f" expert-disk: {dp.get('prof_expert_disk', '?'):>8}")
print(f" expert-matmul: {dp.get('prof_expert_mm', '?'):>8}")
print(f" attention: {dp.get('prof_attention', '?'):>8}")
print(f" lm_head: {dp.get('prof_lm_head', '?'):>8}")
print(f" other: {dp.get('prof_other', '?'):>8}")
print(f"\n PERFORMANCE:")
print(f" prefill: {result['prefill_secs']:.2f}s" if result['prefill_secs'] else " prefill: ?")
print(f" decode: {result['decode_tps']:.3f} tok/s" if result['decode_tps'] else " decode: ?")
print(f" hit rate: {result['hit_rate']:.1f}%" if result.get('hit_rate') is not None else " hit rate: ?")
print(f" RSS: {result['rss_gb']:.1f} GB" if result.get('rss_gb') else " RSS: ?")
print(f" exp/tok: {result['experts_per_tok']:.1f}" if result.get('experts_per_tok') else " exp/tok: ?")
print(f" MTP: {result['mtp_accept']:.0f}% accept" if result.get('mtp_accept') is not None else " MTP: ?")
if result.get('cuda_tier'):
ct = result['cuda_tier']
print(f" CUDA tier: {ct['resident']} experts, {ct['vram_gb']:.1f} GB VRAM")
# Show generated text preview
if text:
print(f"\n GENERATED TEXT (first 200 chars):")
print(f" {text[:200].replace(chr(10), ' ')}")
else:
print(f"\n GENERATED TEXT: [none recovered]")
self.results["phases"]["diagnostic"] = result
return result
def phase_quality(self):
"""Phase 3: benchmark accuracy via eval_glm.py SCORE mode."""
print("\n" + "="*60 + "\nPHASE 3: QUALITY BENCHMARKS\n" + "="*60)
eval_script = os.path.join(os.path.dirname(__file__), "eval_glm.py")
bench_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "bench")
if not os.path.exists(eval_script):
print(f" [SKIP] eval_glm.py not found at {eval_script}")
self.results["phases"]["quality"] = {"error": "eval_glm.py not found"}
return None
tasks = ["hellaswag", "arc_challenge", "mmlu"]
missing = [t for t in tasks if not os.path.exists(os.path.join(bench_dir, f"{t}.jsonl"))]
if missing:
print(f" [SKIP] benchmark data missing: {missing}")
print(f" run: python tools/fetch_benchmarks.py --out {bench_dir}")
self.results["phases"]["quality"] = {"error": f"missing benchmark data: {missing}"}
return None
limit = self.args.quality_limit
py = sys.executable
cmd = [py, eval_script, "--snap", self.snap, "--glm", self.glm,
"--data", bench_dir, "--tasks", ",".join(tasks), "--limit", str(limit)]
env = dict(os.environ)
if self.args.ram: env["RAM_GB"] = str(self.args.ram)
print(f" Running eval_glm.py (tasks={tasks}, n={limit})...")
print(f" This takes ~{limit*3*4/0.05:.0f}s at 0.05 tok/s (worst case)...")
t0 = time.time()
log_path = self.out_dir / "quality_eval.log"
try:
with open(log_path, "w", encoding="utf-8") as logf:
proc = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=self.args.timeout*3,
encoding="utf-8", errors="replace")
logf.write(proc.stdout); logf.write("\n--- STDERR ---\n"); logf.write(proc.stderr)
elapsed = time.time() - t0
# Parse the acc/acc_norm table from eval_glm.py output
scores = {}
for line in proc.stdout.splitlines():
# lines like: "hellaswag 40 45.0% 50.0%"
m = re.match(r"(\w+)\s+(\d+)\s+([\d.]+)%\s+([\d.]+)%", line.strip())
if m:
scores[m.group(1)] = {"n": int(m.group(2)), "acc": float(m.group(3)),
"acc_norm": float(m.group(4))}
mean_m = re.search(r"MEAN acc_norm:\s*([\d.]+)%", proc.stdout)
result = {"scores": scores, "mean_acc_norm": float(mean_m.group(1)) if mean_m else None,
"elapsed": elapsed, "rc": proc.returncode}
print(f" Completed in {elapsed:.0f}s\n")
print(f" {'task':<18} {'n':>4} {'acc':>7} {'acc_norm':>9}")
for t, s in scores.items():
print(f" {t:<18} {s['n']:>4} {s['acc']:>6.1f}% {s['acc_norm']:>8.1f}%")
if result["mean_acc_norm"] is not None:
print(f"\n MEAN acc_norm: {result['mean_acc_norm']:.1f}%")
except subprocess.TimeoutExpired:
result = {"error": f"eval timed out after {self.args.timeout*3}s"}
print(f" [TIMEOUT] eval_glm.py exceeded {self.args.timeout*3}s")
except Exception as e:
result = {"error": str(e)}
print(f" [ERROR] {e}")
self.results["phases"]["quality"] = result
return result
def phase_throughput(self):
"""Phase 4: tok/s comparison — MTP on vs off."""
print("\n" + "="*60 + "\nPHASE 4: THROUGHPUT BENCHMARK\n" + "="*60)
prompt = "Summarize the plot of Romeo and Juliet in three sentences."
ngen = self.args.ngen
results = {}
# Run 1: with MTP (default)
print(f" [1/2] MTP ON (draft=3)...")
stdout, stderr, rc, t = self.runner.run_prompt(
prompt, ngen=ngen, log_name="throughput_mtp_on.log")
m_on = extract_metrics(stdout, stderr)
results["mtp_on"] = {"tps": m_on.get("decode_tps"), "hit": m_on.get("hit_rate"),
"mtp_accept": m_on.get("mtp_accept"), "elapsed": t}
print(f" {m_on.get('decode_tps',0):.3f} tok/s | hit {m_on.get('hit_rate',0):.1f}% | "
f"MTP {m_on.get('mtp_accept',0):.0f}%")
# Run 2: MTP off
print(f" [2/2] MTP OFF (MTP=0)...")
stdout, stderr, rc, t = self.runner.run_prompt(
prompt, ngen=ngen, env_extra={"MTP": "0"}, log_name="throughput_mtp_off.log")
m_off = extract_metrics(stdout, stderr)
results["mtp_off"] = {"tps": m_off.get("decode_tps"), "hit": m_off.get("hit_rate"),
"elapsed": t}
print(f" {m_off.get('decode_tps',0):.3f} tok/s | hit {m_off.get('hit_rate',0):.1f}%")
# Compute speedup
if results["mtp_on"]["tps"] and results["mtp_off"]["tps"] and results["mtp_off"]["tps"] > 0:
sp = results["mtp_on"]["tps"] / results["mtp_off"]["tps"]
results["mtp_speedup"] = sp
print(f"\n MTP speedup: {sp:.2f}x ({'MTP helps' if sp > 1.05 else 'MTP hurts' if sp < 0.95 else 'no effect'})")
else:
print(f"\n MTP speedup: (insufficient data)")
self.results["phases"]["throughput"] = results
return results
def write_report(self):
"""Write report.json and report.md."""
ts = self.results["meta"]["timestamp"]
json_path = self.out_dir / "report.json"
md_path = self.out_dir / "report.md"
# JSON
with open(json_path, "w", encoding="utf-8") as f:
json.dump(self.results, f, indent=2, default=str)
# Markdown
lines = [
f"# Diagnostic Report — {ts}",
f"",
f"**Model:** `{self.snap}`",
f"**Engine:** `{self.glm}`",
f"**Date:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
f"",
]
phases = self.results["phases"]
# System
if "system" in phases:
s = phases["system"]
lines += ["## Phase 0: System Probe", ""]
lines += [f"- Load time: **{s.get('load_secs','?')}s**"]
lines += [f"- Layers/experts: {s.get('n_layers','?')}/{s.get('n_experts','?')}"]
lines += [f"- MTP: {s.get('mtp_status','?')}"]
lines += [f"- idot kernel: `{s.get('idot_kernel','?')}`"]
lines += [f"- RAM budget: {s.get('ram_budget_gb','auto')} GB"]
if s.get("cap_lowered"):
lines += [f"- Cache cap: {s['cap_lowered'][0]}{s['cap_lowered'][1]} (RAM-lowered)"]
elif s.get("cap_final"):
lines += [f"- Cache cap: {s['cap_final']}"]
for d in s.get("cuda_devices", []):
lines += [f"- GPU {d['id']}: {d['name']}, {d['vram_gb']:.1f} GB, sm_{d['sm']}"]
lines.append("")
# Smoke
if "smoke" in phases:
sm = phases["smoke"]
lines += [f"## Phase 1: Correctness Smoke", "",
f"**{sm['pass']}/{sm['total']} prompts passed ({sm['pass_rate']:.0f}%)**", "",
"| ID | Category | Pass | Expect | Repetition | tok/s | Generated (first 80 chars) |",
"|---|---|---|---|---|---|---|"]
for p in sm["prompts"]:
gen = p["generated"][:80].replace("|", "\\|").replace("\n", " ") if p["generated"] else ""
exp = "Y" if p.get("expect_match") else ("N" if p.get("expect_match") is False else "-")
tps = f"{p.get('decode_tps',0):.2f}" if p.get("decode_tps") else "-"
lines.append(f"| {p['id']} | {p['cat']} | {'' if p['pass'] else ''} | {exp} | "
f"{'⚠️' if p['repetition'] else '-'} (run={p.get('max_run',0)}) | {tps} | {gen} |")
lines.append("")
# Diagnostic
if "diagnostic" in phases:
d = phases["diagnostic"]
lines += ["## Phase 2: Full System Diagnostic", ""]
dp = d.get("decode_profile", {})
lines += ["### Decode Timing Breakdown", "",
"| Bucket | Seconds |", "|---|---|"]
for k, label in [("prof_expert_disk","expert-disk"),("prof_expert_mm","expert-matmul"),
("prof_attention","attention"),("prof_lm_head","lm_head"),
("prof_other","other")]:
v = dp.get(k, "?")
lines.append(f"| {label} | {v} |")
lines += [f"", f"### Performance", f"- Decode: **{d.get('decode_tps','?')} tok/s**",
f"- Prefill: {d.get('prefill_secs','?')}s",
f"- Expert hit rate: {d.get('hit_rate','?')}%",
f"- RSS: {d.get('rss_gb','?')} GB",
f"- Experts/token: {d.get('experts_per_tok','?')}",
f"- MTP acceptance: {d.get('mtp_accept','?')}%", ""]
if d.get("generated_text"):
lines += [f"### Generated Text", f"```\n{d['generated_text'][:300]}\n```", ""]
# Quality
if "quality" in phases:
q = phases["quality"]
lines += ["## Phase 3: Quality Benchmarks", ""]
if "error" in q:
lines += [f"⚠️ {q['error']}", ""]
else:
lines += [f"**Mean acc_norm: {q.get('mean_acc_norm','?')}%**", "",
"| Task | n | acc | acc_norm |", "|---|---|---|---|"]
for t, s in q.get("scores", {}).items():
lines.append(f"| {t} | {s['n']} | {s['acc']:.1f}% | {s['acc_norm']:.1f}% |")
lines.append("")
# Throughput
if "throughput" in phases:
th = phases["throughput"]
lines += ["## Phase 4: Throughput", "",
"| Mode | tok/s | hit% | MTP accept |", "|---|---|---|---|"]
on = th.get("mtp_on", {}); off = th.get("mtp_off", {})
lines.append(f"| MTP ON | {on.get('tps','?')} | {on.get('hit','?')}% | {on.get('mtp_accept','?')}% |")
lines.append(f"| MTP OFF | {off.get('tps','?')} | {off.get('hit','?')}% | — |")
if th.get("mtp_speedup"):
lines.append(f"\n**MTP speedup: {th['mtp_speedup']:.2f}x**")
lines.append("")
with open(md_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
print(f"\n{'='*60}")
print(f"Report written:")
print(f" JSON: {json_path}")
print(f" MD: {md_path}")
print(f" Logs: {self.out_dir}/*.log")
print(f"{'='*60}")
def run(self):
phase = self.args.phase
if phase == "all":
self.phase_system()
self.phase_smoke()
self.phase_diagnostic()
self.phase_quality()
self.phase_throughput()
elif phase == "system": self.phase_system()
elif phase == "smoke": self.phase_smoke()
elif phase == "diagnostic": self.phase_diagnostic()
elif phase == "quality": self.phase_quality()
elif phase == "throughput": self.phase_throughput()
else:
print(f"Unknown phase: {phase}", file=sys.stderr); sys.exit(1)
self.write_report()
def main():
ap = argparse.ArgumentParser(description="Comprehensive model diagnostic harness for colibri GLM-5.2")
ap.add_argument("--snap", required=True, help="model snapshot directory")
ap.add_argument("--glm", default=None, help="engine binary path (default: ./glm.exe or ./glm)")
ap.add_argument("--phase", default="all",
choices=["all","system","smoke","diagnostic","quality","throughput"],
help="which test phase to run")
ap.add_argument("--out", default=None, help="output directory (default: ./diag_results/<timestamp>)")
ap.add_argument("--ngen", type=int, default=64, help="generation length for smoke/throughput (default 64)")
ap.add_argument("--quality-limit", type=int, default=40, help="questions per benchmark task (default 40)")
ap.add_argument("--ram", type=float, default=0, help="RAM_GB override (0=auto)")
ap.add_argument("--cuda", action="store_true", help="enable COLI_CUDA GPU tier")
ap.add_argument("--gpu", type=int, default=None, help="GPU device ordinal (with --cuda)")
ap.add_argument("--cap", type=int, default=75, help="experts-per-layer cache cap (default 75)")
ap.add_argument("--timeout", type=int, default=600, help="per-run timeout in seconds (default 600)")
a = ap.parse_args()
# Auto-detect engine binary
if a.glm is None:
for cand in ["./glm.exe", "./glm", "../glm.exe", os.path.join(os.path.dirname(__file__), "..", "glm.exe")]:
if os.path.exists(cand): a.glm = os.path.abspath(cand); break
if a.glm is None:
print("ERROR: could not find glm/glm.exe. Specify with --glm", file=sys.stderr); sys.exit(1)
if not os.path.isdir(a.snap):
print(f"ERROR: snapshot dir does not exist: {a.snap}", file=sys.stderr); sys.exit(1)
harness = DiagnosticHarness(a)
harness.run()
if __name__ == "__main__":
main()
+57 -9
View File
@@ -22,7 +22,7 @@ USO:
# leve di ricerca: passate al motore via env
TOPP=0.9 python3 tools/eval_glm.py --snap /home/vincenzo/glm52_i4 --data ./bench --tasks mmlu --ram 15
"""
import os, sys, subprocess, argparse, random, json, tempfile, time
import os, sys, subprocess, argparse, random, json, tempfile, time, threading
# mini-set OFFLINE per testare la meccanica (NON misura qualita': domande banali)
SMOKE = [
@@ -114,6 +114,7 @@ def main():
ap.add_argument("--seed", type=int, default=1234)
ap.add_argument("--dry", action="store_true", help="build requests and stop without running the engine")
ap.add_argument("--selftest", action="store_true", help="verify the scoring calculations")
ap.add_argument("--out", default="", help="write incremental results CSV here (one row per request, flushed as it lands)")
a = ap.parse_args()
if a.selftest: # acc/acc_norm con logprob sintetici
@@ -143,15 +144,62 @@ def main():
if a.ram: env["RAM_GB"] = str(a.ram)
cmd = [a.glm, str(a.cap)] + a.bits.split()
print("running:", " ".join(cmd), file=sys.stderr)
# Stream results line-by-line so a crash at request N keeps 1..N-1 and shows
# exactly where it stopped. The engine prints "<lp> <contlen> <greedy>" per
# request to stdout and "[score N req | ...]" progress to stderr; buffering
# both until exit (the old subprocess.run) wastes the whole run on a crash.
out_f = open(a.out, "a") if a.out else None
if out_f:
out_f.write(f"# eval_glm snap={a.snap} tasks={a.tasks} limit={a.limit} seed={a.seed} started={time.strftime('%Y-%m-%dT%H:%M:%S')}\n")
out_f.write("req_idx,task,qi,oi,contlen,contchars,gold,logprob,greedy\n")
out_f.flush()
t0 = time.time()
proc = subprocess.run(cmd, env=env, capture_output=True, text=True)
if proc.returncode != 0:
print("ENGINE ERROR:\n", proc.stderr[-2000:], file=sys.stderr); sys.exit(1)
lines = [l for l in proc.stdout.strip().splitlines() if l and l[0] in "-0123456789"]
if len(lines) != len(reqs):
print(f"WARNING: {len(lines)} outputs for {len(reqs)} requests", file=sys.stderr)
lp = [float(l.split()[0]) for l in lines]
print(f"(engine: {time.time()-t0:.0f}s){proc.stderr.strip().splitlines()[-1] if proc.stderr.strip() else ''}", file=sys.stderr)
proc = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, bufsize=1) # line-buffered
lp = [None] * len(reqs)
n_done = 0
# Drain stderr (engine progress lines) to console live on a background thread
# so the [score N req] heartbeat is visible while stdout is consumed below.
def _drain_stderr():
for line in proc.stderr:
print(f" [engine] {line.rstrip()}", file=sys.stderr)
threading.Thread(target=_drain_stderr, daemon=True).start()
for line in proc.stdout:
line = line.strip()
if not line or line[0] not in "-0123456789": continue
parts = line.split()
if n_done >= len(reqs): break
try: logprob = float(parts[0])
except (ValueError, IndexError): continue
lp[n_done] = logprob
greedy = parts[2] if len(parts) > 2 else "?"
t, qi, oi, clen, cchars, gold = meta[n_done]
if out_f:
out_f.write(f"{n_done},{t},{qi},{oi},{clen},{cchars},{gold},{logprob:.6f},{greedy}\n")
out_f.flush()
n_done += 1
if n_done % 5 == 0 or n_done == len(reqs):
elapsed = time.time() - t0
rate = n_done / elapsed if elapsed > 0 else 0
eta = (len(reqs) - n_done) / rate if rate > 0 else 0
print(f"[progress] {n_done}/{len(reqs)} requests scored | {elapsed:.0f}s elapsed | "
f"{rate:.2f} req/s | ETA {eta:.0f}s | last: {t} q{qi} opt{oi} lp={logprob:.3f}",
file=sys.stderr)
proc.wait()
elapsed = time.time() - t0
if out_f:
out_f.write(f"# finished: {n_done}/{len(reqs)} in {elapsed:.0f}s, exit={proc.returncode}\n")
out_f.close()
if proc.returncode != 0 and n_done == 0:
print(f"ENGINE ERROR (exit {proc.returncode})", file=sys.stderr); sys.exit(1)
if n_done != len(reqs):
print(f"WARNING: only {n_done}/{len(reqs)} requests scored (engine exited {proc.returncode}); "
f"scoring partial results.", file=sys.stderr)
# Fill any unscored slots with -inf so argmax never picks them
for i in range(len(lp)):
if lp[i] is None: lp[i] = float("-inf")
print(f"(engine: {elapsed:.0f}s, {n_done}/{len(reqs)} scored, exit {proc.returncode})", file=sys.stderr)
score_accuracy(tasks, meta, perq, lp)
print("\nNOTE: compare acc_norm with GLM-5.2's PUBLISHED model-card score. A close result"
"\n indicates that int4 quantization preserved quality. (Fill REFERENCE in tools/eval_glm.py.)")