Merge remote-tracking branch 'upstream/main' into feat/gpu-backend-hardening
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
name: linux-x86_64
|
||||
ext: ""
|
||||
make_args: "ARCH=x86-64-v3"
|
||||
- os: macos-latest
|
||||
name: macos-arm64
|
||||
ext: ""
|
||||
make_args: ""
|
||||
- os: windows-latest
|
||||
name: windows-x86_64
|
||||
ext: ".exe"
|
||||
make_args: ""
|
||||
# GitHub shells are format strings: 'msys2 {0}', never bare 'msys2'
|
||||
# (the v1.0.0 tag build failed on exactly this). Same as ci.yml.
|
||||
shell: "msys2 {0}"
|
||||
runs-on: ${{ matrix.os }}
|
||||
defaults:
|
||||
run:
|
||||
shell: ${{ matrix.shell || 'bash' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- if: matrix.os == 'windows-latest'
|
||||
uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: UCRT64
|
||||
update: false
|
||||
# inherit: the Package step calls the runner's 7z.exe, which lives on
|
||||
# the Windows PATH; the default minimal path would not see it.
|
||||
path-type: inherit
|
||||
install: >-
|
||||
make
|
||||
mingw-w64-ucrt-x86_64-gcc
|
||||
|
||||
- if: matrix.os == 'macos-latest'
|
||||
run: brew install libomp
|
||||
|
||||
- name: Build engine
|
||||
run: |
|
||||
cd c
|
||||
make glm ${{ matrix.make_args }}
|
||||
ls -lh glm${{ matrix.ext }}
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
TAG=${GITHUB_REF#refs/tags/}
|
||||
mkdir -p dist
|
||||
cp c/glm${{ matrix.ext }} dist/colibri-${TAG}-${{ matrix.name }}${{ matrix.ext }}
|
||||
cp c/coli dist/
|
||||
cp c/version.py dist/
|
||||
cp c/openai_server.py dist/
|
||||
cp c/resource_plan.py dist/
|
||||
cp c/doctor.py dist/
|
||||
cp LICENSE dist/
|
||||
cd dist
|
||||
if [ "${{ matrix.ext }}" = ".exe" ]; then
|
||||
7z a colibri-${TAG}-${{ matrix.name }}.zip *
|
||||
else
|
||||
tar czf colibri-${TAG}-${{ matrix.name }}.tar.gz *
|
||||
fi
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: colibri-${{ matrix.name }}
|
||||
path: dist/colibri-*.*
|
||||
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
TAG=${GITHUB_REF#refs/tags/}
|
||||
# Extract the latest version section from CHANGELOG
|
||||
NOTES=$(awk "/^## \\[${TAG#v}\\]/{found=1;next} /^## \\[/{if(found)exit} found{print}" CHANGELOG.md)
|
||||
if [ -z "$NOTES" ]; then
|
||||
NOTES="Release ${TAG}"
|
||||
fi
|
||||
gh release create "$TAG" artifacts/* \
|
||||
--title "colibrì ${TAG}" \
|
||||
--notes "$NOTES"
|
||||
@@ -0,0 +1,53 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to colibrì are documented here.
|
||||
Format follows [Keep a Changelog](https://keepachangelog.com/).
|
||||
|
||||
## [1.0.0] — 2026-07-19
|
||||
|
||||
First tagged release. The engine has been running in production since late June
|
||||
2026; this tag marks the baseline for semantic versioning going forward.
|
||||
|
||||
### Highlights
|
||||
|
||||
- **GLM-5.2 (744B MoE)** runs on ~25 GB RAM in pure C, streaming experts from disk
|
||||
- **Three-tier placement**: VRAM (hot) / RAM (warm) / NVMe (cold), with a learning
|
||||
cache that pins your workload's hottest experts automatically
|
||||
- **CUDA backend**: multi-GPU expert tier, dense tensor distribution, batched
|
||||
ragged attention, resident pipeline (`COLI_CUDA_PIPE=2`)
|
||||
- **Metal backend** (Apple Silicon): batched expert SwiGLU + fused decode attention
|
||||
on unified memory GPU
|
||||
- **MTP speculation**: native GLM-5.2 draft heads, grammar-forced drafts, kernel-
|
||||
pinned verification (`SPEC_PIN=1`)
|
||||
- **OpenAI-compatible API**: `coli serve` with SSE streaming, KV slots, bounded
|
||||
queue, web dashboard (`coli web`)
|
||||
- **Web UI**: chat with live metrics, expert cortex brain page, profiling breakdown,
|
||||
expert atlas 3-D galaxy
|
||||
- **Cross-platform**: Linux, macOS, Windows 11 (native MinGW), PowerPC; CI on all three
|
||||
- **Auto-tune**: `coli plan --auto-tier` classifies the bottleneck and derives
|
||||
MTP/PIPE/NUMA/PIN settings with explanations
|
||||
|
||||
### Engine
|
||||
|
||||
- Token-exact validation against `transformers` oracle (teacher-forcing 32/32)
|
||||
- Compressed MLA KV cache (576 floats/token, 57× smaller), persisted across
|
||||
restarts (`.coli_kv`, zero re-prefill)
|
||||
- DSA sparse attention (lightning indexer), faithfully implemented
|
||||
- Router-lookahead prefetch (`PILOT=1`, 71.6% predictive)
|
||||
- Async expert I/O pool (`PIPE=1`), io_uring batching (`URING=1`)
|
||||
- NUMA-aware expert placement (`COLI_NUMA=1`, +13–40% on multi-socket)
|
||||
- AVX2 / AVX-512 / AVX-VNNI / ARM NEON / NEON-i8mm / POWER VSX kernels
|
||||
- int4 / int8 / int2 / grouped-int4 (fmt=4) quantization formats
|
||||
|
||||
### Tools
|
||||
|
||||
- `coli convert` — FP8→int4 one-shard-at-a-time converter
|
||||
- `coli doctor` — read-only setup diagnostics
|
||||
- `coli plan` — resource planner with auto-tune prescription
|
||||
- `coli bench` — MMLU / HellaSwag / ARC quality benchmarks
|
||||
- Expert atlas (`tools/analyze.py --web`) — measured topic affinity for 19,456 experts
|
||||
|
||||
### Community
|
||||
|
||||
- 30+ hardware datapoints in the benchmark tracker
|
||||
- Contributions from 20+ authors across engine, docs, tooling, and ports
|
||||
+7
-1
@@ -166,7 +166,7 @@ else
|
||||
PYTHON ?= python3
|
||||
endif
|
||||
CUDA_OBJ =
|
||||
TEST_BINS = tests/test_json$(EXE) tests/test_st$(EXE) tests/test_st_pread$(EXE) tests/test_tier$(EXE) tests/test_grammar$(EXE) tests/test_schema_gbnf$(EXE) tests/test_decode_batch$(EXE) tests/test_idot$(EXE) tests/test_i4_grouped$(EXE) tests/test_stops$(EXE) tests/test_topp$(EXE) tests/test_kv_alloc$(EXE) tests/test_i4_acc512$(EXE) tests/test_compat_direct$(EXE) tests/test_dsa_select$(EXE)
|
||||
TEST_BINS = tests/test_json$(EXE) tests/test_st$(EXE) tests/test_st_pread$(EXE) tests/test_tier$(EXE) tests/test_grammar$(EXE) tests/test_schema_gbnf$(EXE) tests/test_decode_batch$(EXE) tests/test_idot$(EXE) tests/test_i4_grouped$(EXE) tests/test_stops$(EXE) tests/test_topp$(EXE) tests/test_sample_nan$(EXE) tests/test_kv_alloc$(EXE) tests/test_i4_acc512$(EXE) tests/test_compat_direct$(EXE) tests/test_dsa_select$(EXE) tests/test_logit_nan$(EXE)
|
||||
ifneq (,$(LINUX))
|
||||
TEST_BINS += tests/test_uring$(EXE)
|
||||
endif
|
||||
@@ -328,9 +328,15 @@ tests/test_topp$(EXE): tests/test_topp.c glm.c st.h uring.h json.h tok.h tok_uni
|
||||
tests/bench_topp$(EXE): tests/bench_topp.c glm.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
|
||||
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
|
||||
|
||||
tests/test_sample_nan$(EXE): tests/test_sample_nan.c glm.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
|
||||
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
|
||||
|
||||
tests/test_kv_alloc$(EXE): tests/test_kv_alloc.c glm.c st.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
|
||||
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
|
||||
|
||||
tests/test_logit_nan$(EXE): tests/test_logit_nan.c glm.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
|
||||
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
|
||||
|
||||
tests/test_i4_acc512$(EXE): tests/test_i4_acc512.c
|
||||
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
|
||||
|
||||
|
||||
+143
-3
@@ -8,12 +8,21 @@
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
|
||||
struct RaggedKVEntry {
|
||||
const void *key;
|
||||
const float *host_l,*host_r;
|
||||
float *latent,*rope;
|
||||
int length,capacity,K,R;
|
||||
};
|
||||
|
||||
struct ColiCudaTensor {
|
||||
void *weights;
|
||||
float *scales;
|
||||
size_t weight_bytes;
|
||||
int fmt, I, O, device;
|
||||
int tracked;
|
||||
RaggedKVEntry ragged[512];
|
||||
int ragged_count;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
@@ -23,7 +32,7 @@ typedef struct {
|
||||
size_t x_cap, y_cap, gate_cap, up_cap;
|
||||
uint8_t *qx; float *qscale;
|
||||
size_t qx_cap, qscale_cap;
|
||||
float *host_x,*host_y; size_t host_x_cap,host_y_cap;
|
||||
float *host_x,*host_y,*host_kv; size_t host_x_cap,host_y_cap,host_kv_cap;
|
||||
float *aq,*al,*ar,*ac; size_t aq_cap,al_cap,ar_cap,ac_cap;
|
||||
float *pipe_buf[24]; size_t pipe_cap[24]; /* scratch persistenti del resident pipeline */
|
||||
cudaStream_t stream;
|
||||
@@ -340,6 +349,49 @@ __global__ static void attention_absorb_batch_kernel(float *ctx,const float *q,
|
||||
ctx[((size_t)s*H+h)*V+v]=a*(fmt?wscale[row]:1.f);}
|
||||
}
|
||||
|
||||
/* Independent device-resident KV sequence per row. lengths selects the valid
|
||||
* prefix; latent/rope point at paged caches updated by the host wrapper. */
|
||||
__global__ static void attention_absorb_ragged_kernel(float *ctx,const float *q,
|
||||
const float *const *latent,const float *const *rope,const int *lengths,
|
||||
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 s=blockIdx.y,h=blockIdx.x,tid=threadIdx.x,nt=lengths[s],rbase=h*(Q+V);
|
||||
if(s>=S||nt<1||nt>T)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);
|
||||
const float *ls=latent[s],*rs=rope[s];
|
||||
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;}
|
||||
__syncthreads();
|
||||
for(int t=tid;t<nt;t+=blockDim.x){float a=0;const float *lt=ls+(size_t)t*K;
|
||||
const float *rt=rs+(size_t)t*R;for(int k=0;k<K;k++)a+=qa[k]*lt[k];
|
||||
for(int d=0;d<R;d++)a+=qs[Q+d]*rt[d];scores[t]=a*scale;}
|
||||
__syncthreads();
|
||||
float local=-3.402823466e+38F;for(int t=tid;t<nt;t+=blockDim.x)local=fmaxf(local,scores[t]);
|
||||
red[tid]=local;__syncthreads();
|
||||
for(int n=blockDim.x>>1;n;n>>=1){if(tid<n)red[tid]=fmaxf(red[tid],red[tid+n]);__syncthreads();}
|
||||
float mx=red[0];local=0;for(int t=tid;t<nt;t+=blockDim.x){float e=expf(scores[t]-mx);scores[t]=e;local+=e;}
|
||||
red[tid]=local;__syncthreads();
|
||||
for(int n=blockDim.x>>1;n;n>>=1){if(tid<n)red[tid]+=red[tid+n];__syncthreads();}
|
||||
float inv=1.f/red[0];for(int t=tid;t<nt;t+=blockDim.x)scores[t]*=inv;
|
||||
__syncthreads();
|
||||
for(int k=tid;k<K;k+=blockDim.x){float a=0;for(int t=0;t<nt;t++)a+=scores[t]*ls[(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);}
|
||||
}
|
||||
|
||||
__global__ static void ragged_kv_append(float *const *latent,float *const *rope,
|
||||
const float *packed,const int *old_len,const int *add,const int *offset,int K,int R){
|
||||
int s=blockIdx.x,n=add[s],base=offset[s];
|
||||
for(int i=threadIdx.x;i<n*(K+R);i+=blockDim.x){
|
||||
if(i<n*K)latent[s][(size_t)old_len[s]*K+i]=packed[base+i];
|
||||
else rope[s][(size_t)old_len[s]*R+i-n*K]=packed[base+i];
|
||||
}
|
||||
}
|
||||
|
||||
static int reserve(float **ptr, size_t *cap, size_t bytes) {
|
||||
if (*cap >= bytes) return 1;
|
||||
if (*ptr) cudaFree(*ptr);
|
||||
@@ -408,16 +460,17 @@ extern "C" void coli_cuda_shutdown(void) {
|
||||
for(int b=0;b<24;b++) if(ctx->pipe_buf[b]) cudaFree(ctx->pipe_buf[b]);
|
||||
if (ctx->host_x) cudaFreeHost(ctx->host_x);
|
||||
if (ctx->host_y) cudaFreeHost(ctx->host_y);
|
||||
if (ctx->host_kv) cudaFreeHost(ctx->host_kv);
|
||||
if (ctx->stream) cudaStreamDestroy(ctx->stream);
|
||||
if (ctx->group_desc) cudaFree(ctx->group_desc);
|
||||
ctx->x = ctx->y = ctx->gate = ctx->up = nullptr;
|
||||
ctx->qx=nullptr; ctx->qscale=nullptr;
|
||||
ctx->aq=ctx->al=ctx->ar=ctx->ac=nullptr;
|
||||
ctx->host_x=ctx->host_y=nullptr;ctx->stream=nullptr;
|
||||
ctx->host_x=ctx->host_y=ctx->host_kv=nullptr;ctx->stream=nullptr;
|
||||
ctx->x_cap = ctx->y_cap = ctx->gate_cap = ctx->up_cap = 0;
|
||||
ctx->qx_cap=ctx->qscale_cap=0;
|
||||
ctx->aq_cap=ctx->al_cap=ctx->ar_cap=ctx->ac_cap=0;
|
||||
ctx->host_x_cap=ctx->host_y_cap=0;
|
||||
ctx->host_x_cap=ctx->host_y_cap=ctx->host_kv_cap=0;
|
||||
ctx->group_desc=nullptr; ctx->group_desc_cap=0;
|
||||
}
|
||||
g_nctx = 0;
|
||||
@@ -795,6 +848,89 @@ extern "C" int coli_cuda_attention_project_batch(ColiCudaTensor *w,ColiCudaTenso
|
||||
return attention_absorb_batch_run(w,proj,out,q,latent,rope,S,H,Q,R,V,K,T,scale);
|
||||
}
|
||||
|
||||
extern "C" int coli_cuda_attention_project_ragged(ColiCudaTensor *w,ColiCudaTensor *proj,
|
||||
float *out,const float *q,const void *const *keys,
|
||||
const float *const *latent,const float *const *rope,
|
||||
const int *lengths,int S,int H,int Q,int R,int V,int K,int T,float scale){
|
||||
if(!w||!proj||!out||!q||!keys||!latent||!rope||!lengths||S<1||S>512||T<1||T>8192||
|
||||
H<1||Q<1||R<1||V<1||K<1||K>512||w->I!=K||w->O!=H*(Q+V)||
|
||||
proj->device!=w->device||proj->I!=H*V)return 0;
|
||||
DeviceContext *dc=find_ctx(w->device);
|
||||
if(!select_ctx(dc))return 0;
|
||||
float **dl=(float**)std::malloc((size_t)S*sizeof(*dl));
|
||||
float **dr=(float**)std::malloc((size_t)S*sizeof(*dr));
|
||||
int *old=(int*)std::malloc((size_t)S*sizeof(*old));
|
||||
int *add=(int*)std::malloc((size_t)S*sizeof(*add));
|
||||
int *off=(int*)std::malloc((size_t)S*sizeof(*off));int packed_n=0;
|
||||
if(!dl||!dr||!old||!add||!off){std::free(dl);std::free(dr);std::free(old);std::free(add);std::free(off);return 0;}
|
||||
for(int s=0;s<S;s++){
|
||||
if(!keys[s]||lengths[s]<1||lengths[s]>T){std::free(dl);std::free(dr);std::free(old);std::free(add);std::free(off);return 0;}
|
||||
RaggedKVEntry *e=nullptr;
|
||||
for(int i=0;i<w->ragged_count;i++)if(w->ragged[i].key==keys[s]){e=&w->ragged[i];break;}
|
||||
if(!e){
|
||||
if(w->ragged_count>=512){std::free(dl);std::free(dr);std::free(old);std::free(add);std::free(off);return 0;}
|
||||
e=&w->ragged[w->ragged_count++];std::memset(e,0,sizeof(*e));e->key=keys[s];
|
||||
}
|
||||
if(e->K!=K||e->R!=R||e->host_l!=latent[s]||e->host_r!=rope[s]||lengths[s]<e->length){
|
||||
if(e->latent)cudaFree(e->latent);if(e->rope)cudaFree(e->rope);
|
||||
e->latent=e->rope=nullptr;e->length=e->capacity=0;
|
||||
e->K=K;e->R=R;e->host_l=latent[s];e->host_r=rope[s];
|
||||
}
|
||||
if(lengths[s]>e->capacity){
|
||||
int cap=(lengths[s]+63)&~63;float *nl=nullptr,*nr=nullptr;
|
||||
if(!cuda_ok(cudaMalloc(&nl,(size_t)cap*K*sizeof(float)),"ragged KV latent page")||
|
||||
!cuda_ok(cudaMalloc(&nr,(size_t)cap*R*sizeof(float)),"ragged KV rope page")){
|
||||
if(nl)cudaFree(nl);if(nr)cudaFree(nr);std::free(dl);std::free(dr);std::free(old);std::free(add);std::free(off);return 0;
|
||||
}
|
||||
if(e->length){
|
||||
cudaMemcpyAsync(nl,e->latent,(size_t)e->length*K*sizeof(float),cudaMemcpyDeviceToDevice,dc->stream);
|
||||
cudaMemcpyAsync(nr,e->rope,(size_t)e->length*R*sizeof(float),cudaMemcpyDeviceToDevice,dc->stream);
|
||||
}
|
||||
if(e->latent)cudaFree(e->latent);if(e->rope)cudaFree(e->rope);
|
||||
e->latent=nl;e->rope=nr;e->capacity=cap;
|
||||
}
|
||||
dl[s]=e->latent;dr[s]=e->rope;old[s]=e->length;add[s]=lengths[s]-e->length;
|
||||
off[s]=packed_n;packed_n+=add[s]*(K+R);
|
||||
}
|
||||
size_t qb=(size_t)S*H*(Q+R)*sizeof(float);
|
||||
size_t cb=(size_t)S*H*V*sizeof(float),ob=(size_t)S*proj->O*sizeof(float);
|
||||
size_t pb=(size_t)packed_n*sizeof(float);
|
||||
size_t desc=(size_t)S*(2*sizeof(float*)+4*sizeof(int));
|
||||
int ok=reserve(&dc->aq,&dc->aq_cap,qb)&&reserve(&dc->ac,&dc->ac_cap,cb)&&
|
||||
reserve(&dc->y,&dc->y_cap,ob)&&reserve_bytes(&dc->group_desc,&dc->group_desc_cap,desc)&&
|
||||
(!pb||(reserve(&dc->al,&dc->al_cap,pb)&&reserve_pinned(&dc->host_kv,&dc->host_kv_cap,pb)));
|
||||
char *db=(char*)dc->group_desc;float **ddl=(float**)db,**ddr=ddl+S;
|
||||
int *dn=(int*)(ddr+S),*dold=dn+S,*dadd=dold+S,*doff=dadd+S;
|
||||
if(ok&&pb){
|
||||
for(int s=0;s<S;s++)if(add[s]){
|
||||
float *p=dc->host_kv+off[s];
|
||||
std::memcpy(p,latent[s]+(size_t)old[s]*K,(size_t)add[s]*K*sizeof(float));
|
||||
std::memcpy(p+(size_t)add[s]*K,rope[s]+(size_t)old[s]*R,(size_t)add[s]*R*sizeof(float));
|
||||
}
|
||||
ok=cuda_ok(cudaMemcpyAsync(dc->al,dc->host_kv,pb,cudaMemcpyHostToDevice,dc->stream),"ragged KV append upload");
|
||||
}
|
||||
if(ok)ok=cuda_ok(cudaMemcpyAsync(dc->aq,q,qb,cudaMemcpyHostToDevice,dc->stream),"ragged q upload")&&
|
||||
cuda_ok(cudaMemcpyAsync(ddl,dl,(size_t)S*sizeof(float*),cudaMemcpyHostToDevice,dc->stream),"ragged latent pointers")&&
|
||||
cuda_ok(cudaMemcpyAsync(ddr,dr,(size_t)S*sizeof(float*),cudaMemcpyHostToDevice,dc->stream),"ragged rope pointers")&&
|
||||
cuda_ok(cudaMemcpyAsync(dn,lengths,(size_t)S*sizeof(int),cudaMemcpyHostToDevice,dc->stream),"ragged lengths upload")&&
|
||||
cuda_ok(cudaMemcpyAsync(dold,old,(size_t)S*sizeof(int),cudaMemcpyHostToDevice,dc->stream),"ragged old lengths")&&
|
||||
cuda_ok(cudaMemcpyAsync(dadd,add,(size_t)S*sizeof(int),cudaMemcpyHostToDevice,dc->stream),"ragged append lengths")&&
|
||||
cuda_ok(cudaMemcpyAsync(doff,off,(size_t)S*sizeof(int),cudaMemcpyHostToDevice,dc->stream),"ragged append offsets");
|
||||
if(ok&&pb)ragged_kv_append<<<S,256,0,dc->stream>>>(ddl,ddr,dc->al,dold,dadd,doff,K,R);
|
||||
if(ok)for(int s=0;s<S;s++){
|
||||
for(int i=0;i<w->ragged_count;i++)if(w->ragged[i].key==keys[s]){w->ragged[i].length=lengths[s];break;}
|
||||
}
|
||||
std::free(dl);std::free(dr);std::free(old);std::free(add);std::free(off);if(!ok)return 0;
|
||||
size_t shared=(size_t)(2*K+T+256)*sizeof(float);
|
||||
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));
|
||||
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");
|
||||
}
|
||||
|
||||
extern "C" void coli_cuda_tensor_free(ColiCudaTensor *tensor) {
|
||||
if (!tensor) return;
|
||||
DeviceContext *ctx = find_ctx(tensor->device);
|
||||
@@ -806,6 +942,10 @@ extern "C" void coli_cuda_tensor_free(ColiCudaTensor *tensor) {
|
||||
}
|
||||
if (tensor->weights) cudaFree(tensor->weights);
|
||||
if (tensor->scales) cudaFree(tensor->scales);
|
||||
for(int i=0;i<tensor->ragged_count;i++){
|
||||
if(tensor->ragged[i].latent)cudaFree(tensor->ragged[i].latent);
|
||||
if(tensor->ragged[i].rope)cudaFree(tensor->ragged[i].rope);
|
||||
}
|
||||
std::free(tensor);
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -14,6 +14,7 @@
|
||||
#define COLI_CUDA_DLLEXPORT
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -92,6 +93,11 @@ COLI_CUDA_DLLEXPORT int coli_cuda_attention_project_batch(ColiCudaTensor *kv_b,C
|
||||
const float *rope,int S,int H,int Q,int R,
|
||||
int V,int K,int T,float attention_scale);
|
||||
|
||||
COLI_CUDA_DLLEXPORT int coli_cuda_attention_project_ragged(ColiCudaTensor *kv_b,ColiCudaTensor *o_proj,
|
||||
float *out,const float *q,const void *const *keys,
|
||||
const float *const *latent,const float *const *rope,
|
||||
const int *lengths,int S,int H,int Q,int R,int V,int K,int max_t,float attention_scale);
|
||||
|
||||
COLI_CUDA_DLLEXPORT void coli_cuda_tensor_free(ColiCudaTensor *tensor);
|
||||
COLI_CUDA_DLLEXPORT size_t coli_cuda_tensor_bytes(const ColiCudaTensor *tensor);
|
||||
COLI_CUDA_DLLEXPORT int coli_cuda_tensor_device(const ColiCudaTensor *tensor);
|
||||
@@ -143,4 +149,3 @@ COLI_CUDA_DLLEXPORT int coli_cuda_pipe_sync(int device);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -61,6 +61,10 @@ typedef int (*fn_attention_absorb_batch)(ColiCudaTensor *kv_b,float *ctx,const f
|
||||
typedef int (*fn_attention_absorb_batch_dev)(ColiCudaTensor *kv_b_shard,float *ctx_dev, const float *q_dev,const float *latent_dev,const float *rope_dev, int S,int H,int Q,int R,int V,int K,int T,float scale);
|
||||
typedef int (*fn_attention_absorb_kvdev)(ColiCudaTensor *kv_b,float *ctx,const float *q, const float *latent_dev,const float *rope_dev,int H,int Q,int R,int V,int K,int T, float scale);
|
||||
typedef int (*fn_attention_project_batch)(ColiCudaTensor *kv_b,ColiCudaTensor *o_proj, float *out,const float *q,const float *latent, const float *rope,int S,int H,int Q,int R, int V,int K,int T,float attention_scale);
|
||||
typedef int (*fn_attention_project_ragged)(ColiCudaTensor *kv_b,ColiCudaTensor *o_proj,
|
||||
float *out,const float *q,const void *const *keys,
|
||||
const float *const *latent,const float *const *rope,
|
||||
const int *lengths,int S,int H,int Q,int R,int V,int K,int max_t,float attention_scale);
|
||||
typedef int (*fn_attention_project_batch_dev)(ColiCudaTensor *kv_b,ColiCudaTensor *o_proj, float *out,const float *q_dev,const float *latent_dev,const float *rope_dev, int S,int H,int Q,int R,int V,int K,int T,float scale);
|
||||
typedef int (*fn_attention_project_batch_dev_out)(ColiCudaTensor *kv_b,ColiCudaTensor *o_proj, float *out_dev,const float *q_dev,const float *latent_dev,const float *rope_dev, int S,int H,int Q,int R,int V,int K,int T,float scale);
|
||||
typedef int (*fn_pipe_add)(int device,float *x_dev,const float *t_dev,size_t n);
|
||||
@@ -107,6 +111,7 @@ static struct {
|
||||
fn_attention_absorb_batch_dev attention_absorb_batch_dev;
|
||||
fn_attention_absorb_kvdev attention_absorb_kvdev;
|
||||
fn_attention_project_batch attention_project_batch;
|
||||
fn_attention_project_ragged attention_project_ragged;
|
||||
fn_attention_project_batch_dev attention_project_batch_dev;
|
||||
fn_attention_project_batch_dev_out attention_project_batch_dev_out;
|
||||
fn_pipe_add pipe_add;
|
||||
@@ -200,6 +205,7 @@ static int coli_cuda_load(void){
|
||||
RESOLVE(attention_absorb_batch_dev, fn_attention_absorb_batch_dev)
|
||||
RESOLVE(attention_absorb_kvdev, fn_attention_absorb_kvdev)
|
||||
RESOLVE(attention_project_batch, fn_attention_project_batch)
|
||||
RESOLVE(attention_project_ragged, fn_attention_project_ragged)
|
||||
RESOLVE(attention_project_batch_dev, fn_attention_project_batch_dev)
|
||||
RESOLVE(attention_project_batch_dev_out, fn_attention_project_batch_dev_out)
|
||||
RESOLVE(pipe_add, fn_pipe_add)
|
||||
@@ -342,6 +348,15 @@ int coli_cuda_attention_project_batch(ColiCudaTensor *kv_b,ColiCudaTensor *o_pro
|
||||
return g_cuda.attention_project_batch(kv_b, o_proj, out, q, latent, rope, S, H, Q, R, V, K, T, attention_scale);
|
||||
}
|
||||
|
||||
int coli_cuda_attention_project_ragged(ColiCudaTensor *kv_b,ColiCudaTensor *o_proj,
|
||||
float *out,const float *q,const void *const *keys,
|
||||
const float *const *latent,const float *const *rope,
|
||||
const int *lengths,int S,int H,int Q,int R,int V,int K,int max_t,float attention_scale){
|
||||
if(!coli_cuda_load()) return 0;
|
||||
return g_cuda.attention_project_ragged(kv_b,o_proj,out,q,keys,latent,rope,lengths,
|
||||
S,H,Q,R,V,K,max_t,attention_scale);
|
||||
}
|
||||
|
||||
int coli_cuda_attention_project_batch_dev(ColiCudaTensor *kv_b,ColiCudaTensor *o_proj, float *out,const float *q_dev,const float *latent_dev,const float *rope_dev, int S,int H,int Q,int R,int V,int K,int T,float scale){
|
||||
if(!g_cuda.available){ return 0; }
|
||||
return g_cuda.attention_project_batch_dev(kv_b, o_proj, out, q_dev, latent_dev, rope_dev, S, H, Q, R, V, K, T, scale);
|
||||
|
||||
@@ -40,6 +40,8 @@ if sys.platform == "win32":
|
||||
except (AttributeError, OSError): pass
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
from version import __version__ as _version
|
||||
|
||||
# Run-in-place (source checkout, "cd c && ./coli ..."): the engine, the
|
||||
# support modules (resource_plan.py, doctor.py, openai_server.py) and
|
||||
@@ -115,7 +117,7 @@ def sprite_lines():
|
||||
def banner(sub=""):
|
||||
sp=sprite_lines()
|
||||
txt=[
|
||||
f"{C.teal}{C.b}colibrì{C.r} {C.dim}v1.0{C.r}",
|
||||
f"{C.teal}{C.b}colibrì{C.r} {C.dim}v{_version}{C.r}",
|
||||
f"{C.dim}tiny engine, immense model{C.r}",
|
||||
f"{C.gray}GLM-5.2 · 744B MoE · int4 · streaming CPU{C.r}",
|
||||
f"{C.dgray}{sub}{C.r}" if sub else "",
|
||||
@@ -141,12 +143,24 @@ def need_model(model):
|
||||
sys.exit(f"{C.yel}engine is not built.{C.r} Run: coli build")
|
||||
|
||||
def cuda_binary():
|
||||
if not os.path.exists(GLM) or sys.platform != "linux": return False
|
||||
try:
|
||||
linked=subprocess.run(["ldd",GLM],capture_output=True,text=True,timeout=3)
|
||||
return any("libcudart" in line and "not found" not in line
|
||||
for line in linked.stdout.splitlines())
|
||||
except (OSError,subprocess.SubprocessError): return False
|
||||
if not os.path.exists(GLM): return False
|
||||
if sys.platform == "linux":
|
||||
try:
|
||||
linked=subprocess.run(["ldd",GLM],capture_output=True,text=True,timeout=3)
|
||||
return any("libcudart" in line and "not found" not in line
|
||||
for line in linked.stdout.splitlines())
|
||||
except (OSError,subprocess.SubprocessError): return False
|
||||
if sys.platform == "win32":
|
||||
# Windows CUDA_DLL=1 builds never link libcudart directly: glm.exe loads
|
||||
# coli_cuda.dll at runtime via LoadLibrary (backend_loader.c), so there's no
|
||||
# import-table entry for ldd/dumpbin to see. Detect the COLI_CUDA build via a
|
||||
# marker string baked into glm.c's #ifdef COLI_CUDA block instead, and require
|
||||
# coli_cuda.dll to actually sit next to glm.exe (else CUDA init fails at startup).
|
||||
try:
|
||||
with open(GLM,"rb") as f: built=b"[CUDA] mode: routed experts" in f.read()
|
||||
except OSError: return False
|
||||
return built and os.path.exists(os.path.join(os.path.dirname(GLM),"coli_cuda.dll"))
|
||||
return False
|
||||
|
||||
def resource_request(a, env):
|
||||
ctx=a.ctx or int(env.get("CTX",4096))
|
||||
@@ -480,8 +494,11 @@ def cmd_run(a):
|
||||
need_model(a.model)
|
||||
prompt=" ".join(a.prompt) if a.prompt else sys.exit('usage: coli run "your prompt"')
|
||||
banner("run")
|
||||
# template ufficiale GLM-5.2: niente \n dopo i ruoli; <think></think> = risposta diretta (nothink)
|
||||
e=env_for(a); e["PROMPT"]=f"[gMASK]<sop><|user|>{prompt}<|assistant|><think></think>"
|
||||
# template ufficiale GLM-5.2: niente \n dopo i ruoli; <think></think> = risposta diretta (nothink).
|
||||
# THINK=1 lascia <think> aperto, stessa convenzione del serve mode (glm.c). EN: THINK=1 leaves
|
||||
# <think> open so the engine emits its reasoning block; the default stays nothink.
|
||||
tk="<think>" if os.environ.get("THINK","0")=="1" else "<think></think>"
|
||||
e=env_for(a); e["PROMPT"]=f"[gMASK]<sop><|user|>{prompt}<|assistant|>{tk}"
|
||||
sys.exit(subprocess.call([GLM, str(a.cap)], env=e))
|
||||
|
||||
def server_probe(base, api_key=None, timeout=1.5):
|
||||
@@ -890,6 +907,7 @@ def main():
|
||||
common.add_argument("--topp", type=float, default=0); common.add_argument("--topk", type=int, default=0)
|
||||
common.add_argument("--temp", type=float, default=None) # temperatura token (0=greedy, default 1.0+nucleus .95)
|
||||
ap=argparse.ArgumentParser(prog="coli", parents=[common], description="colibrì — run GLM-5.2 locally")
|
||||
ap.add_argument("--version", action="version", version=f"colibrì {_version}")
|
||||
sub=ap.add_subparsers(dest="cmd")
|
||||
sub.add_parser("build", parents=[common]); sub.add_parser("info", parents=[common])
|
||||
pp=sub.add_parser("plan",parents=[common])
|
||||
|
||||
+45
@@ -80,6 +80,7 @@ static inline int compat_open_direct(const char *path){
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <io.h>
|
||||
#include <direct.h> /* _mkdir (for the mkdtemp shim below) */
|
||||
#include <process.h>
|
||||
#include <malloc.h>
|
||||
#include <fcntl.h>
|
||||
@@ -313,8 +314,52 @@ static inline int compat_setenv(const char *name, const char *value, int overwri
|
||||
}
|
||||
#define setenv(name,value,overwrite) compat_setenv(name,value,overwrite)
|
||||
|
||||
/* --- getenv_utf8: read an env var as UTF-8, not through the ANSI codepage ---
|
||||
* Plain getenv()/_environ are populated by the CRT from the ANSI-codepage view
|
||||
* of the process environment block, not UTF-8. A parent that hands the child a
|
||||
* Unicode value via CreateProcessW's wide env block (e.g. Python's subprocess
|
||||
* module, which coli uses to pass the chat prompt) round-trips correctly only
|
||||
* through GetEnvironmentVariableW; going through narrow getenv() re-encodes it
|
||||
* via CP_ACP first, so any non-ASCII prompt text (Cyrillic, CJK, ...) comes out
|
||||
* corrupted before the byte-level tokenizer ever sees it. Read the wide value
|
||||
* directly and convert straight to UTF-8, bypassing the ANSI codepage entirely.
|
||||
* Returned buffer is intentionally leaked: called a handful of times at
|
||||
* startup, lives for the process. */
|
||||
static inline const char *compat_getenv_utf8(const char *name){
|
||||
wchar_t wname[64];
|
||||
if(MultiByteToWideChar(CP_UTF8, 0, name, -1, wname, 64) <= 0) return getenv(name);
|
||||
DWORD need = GetEnvironmentVariableW(wname, NULL, 0);
|
||||
if(!need) return NULL;
|
||||
wchar_t *wval = (wchar_t*)malloc(need * sizeof(wchar_t));
|
||||
if(!wval) return NULL;
|
||||
GetEnvironmentVariableW(wname, wval, need);
|
||||
int blen = WideCharToMultiByte(CP_UTF8, 0, wval, -1, NULL, 0, NULL, NULL);
|
||||
char *val = blen>0 ? (char*)malloc((size_t)blen) : NULL;
|
||||
if(val) WideCharToMultiByte(CP_UTF8, 0, wval, -1, val, blen, NULL, NULL);
|
||||
free(wval);
|
||||
return val;
|
||||
}
|
||||
#define getenv_utf8(name) compat_getenv_utf8(name)
|
||||
|
||||
/* --- mkdtemp -> _mktemp + _mkdir (POSIX mkdtemp assente su Windows) ---
|
||||
* Test binaries (test_stops.c) create a scratch dir in the CWD via a
|
||||
* "name_XXXXXX" template; POSIX mkdtemp fills the X's and mkdirs 0700. The
|
||||
* Windows CRT has _mktemp (in-place, same XXXXXX contract) so we compose it.
|
||||
* Returns the template pointer on success, NULL on failure — matching POSIX. */
|
||||
static inline char *compat_mkdtemp(char *tmpl){
|
||||
if(!tmpl) return NULL;
|
||||
if(!_mktemp(tmpl)) return NULL; /* fills the trailing X's in place */
|
||||
if(_mkdir(tmpl) != 0) return NULL; /* EEXIST is impossible post-_mktemp */
|
||||
return tmpl;
|
||||
}
|
||||
#define mkdtemp(tmpl) compat_mkdtemp(tmpl)
|
||||
|
||||
#endif /* _WIN32 */
|
||||
|
||||
#ifndef getenv_utf8
|
||||
#define getenv_utf8(name) getenv(name)
|
||||
#endif
|
||||
|
||||
/* --- compat_aligned_free su piattaforme diverse da Windows ---
|
||||
* Su Linux/macOS, posix_memalign usa free() normale. */
|
||||
#ifndef compat_aligned_free
|
||||
|
||||
+26
-9
@@ -19,16 +19,33 @@ def _check(identifier, status, summary, **details):
|
||||
|
||||
def cuda_linkage(engine_path):
|
||||
"""Return CUDA linkage state without loading the executable or CUDA runtime."""
|
||||
if not Path(engine_path).is_file() or os.name != "posix":
|
||||
engine = Path(engine_path)
|
||||
if not engine.is_file():
|
||||
return {"linked": False, "missing": False}
|
||||
try:
|
||||
result = subprocess.run(["ldd", str(engine_path)], capture_output=True, text=True,
|
||||
timeout=3, check=False)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return {"linked": False, "missing": False}
|
||||
lines = [line for line in result.stdout.splitlines() if "libcudart" in line]
|
||||
return {"linked": any("not found" not in line for line in lines),
|
||||
"missing": any("not found" in line for line in lines)}
|
||||
if os.name == "posix":
|
||||
try:
|
||||
result = subprocess.run(["ldd", str(engine)], capture_output=True, text=True,
|
||||
timeout=3, check=False)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return {"linked": False, "missing": False}
|
||||
lines = [line for line in result.stdout.splitlines() if "libcudart" in line]
|
||||
return {"linked": any("not found" not in line for line in lines),
|
||||
"missing": any("not found" in line for line in lines)}
|
||||
if sys.platform == "win32":
|
||||
# Windows CUDA_DLL=1 builds never link libcudart directly: glm.exe loads
|
||||
# coli_cuda.dll at runtime via LoadLibrary (backend_loader.c), so there's no
|
||||
# import-table entry for ldd/dumpbin to see. Detect the COLI_CUDA build via a
|
||||
# marker string baked into glm.c's #ifdef COLI_CUDA block instead, and require
|
||||
# coli_cuda.dll to actually sit next to glm.exe (else CUDA init fails at startup).
|
||||
try:
|
||||
built = b"[CUDA] mode: routed experts" in engine.read_bytes()
|
||||
except OSError:
|
||||
return {"linked": False, "missing": False}
|
||||
if not built:
|
||||
return {"linked": False, "missing": False}
|
||||
dll_present = (engine.parent / "coli_cuda.dll").is_file()
|
||||
return {"linked": dll_present, "missing": not dll_present}
|
||||
return {"linked": False, "missing": False}
|
||||
|
||||
|
||||
def run_doctor(model, ram_gb=0, context=4096, gpu_indices=None, vram_gb=0, *,
|
||||
|
||||
@@ -196,12 +196,16 @@ typedef struct {
|
||||
uint64_t mtp_prop, mtp_acc; /* statistica acceptance */
|
||||
int **eroute; int *enr; /* metodo C: routing dell'ULTIMO token per layer */
|
||||
uint64_t eclock, hits, miss, ereq;
|
||||
uint64_t hit_pin, hit_ecache; /* split di hits per tier (#336): pin vs LRU ecache */
|
||||
uint64_t gpu_expert_calls; int gpu_expert_count; int64_t gpu_expert_bytes;
|
||||
uint64_t n_fw, n_emit; /* metodo E: forward di decode / token emessi */
|
||||
uint64_t route_slots, route_swaps; /* CACHE_ROUTE: slots chosen / substituted vs true top-K */
|
||||
uint64_t route_agree_hit, route_agree_tot; /* ROUTE_AGREE: |chosen ∩ true top-K| / K */
|
||||
double route_kl_sum; uint64_t route_kl_n; /* mean KL(true||chosen) on gate mass */
|
||||
double t_ewait, t_emm, t_attn, t_kvb, t_head;/* profiling: dove va il tempo (wall del
|
||||
double t_ewait, t_emm, t_ecpu, t_egpu, t_route, t_p2p, t_attn, t_kvb, t_head;
|
||||
uint64_t n_p2p; /* P0 execution profile: tier split + residual hops */
|
||||
uint64_t cpu_expert_rows; int64_t cpu_expert_bytes;
|
||||
/* profiling: dove va il tempo (wall del
|
||||
* thread di compute; il servizio disco
|
||||
* overlappato vive in g_edisk_ns) */
|
||||
double t_aproj,t_acore,t_aout; /* attention breakdown */
|
||||
@@ -307,15 +311,19 @@ static uint64_t g_prof_nlat; /* forwards recorded (monotonic
|
||||
static void prof_lat(double s){ g_prof_lat[g_prof_nlat++ % PROF_LAT_CAP]=s; }
|
||||
/* snapshot for windowed reports (serve mode: one report per turn) */
|
||||
typedef struct {
|
||||
double edisk,ewait,emm,attn,head;
|
||||
int64_t io; uint64_t hits,miss,ereq,n_fw,n_emit,nlat;
|
||||
double edisk,ewait,emm,ecpu,egpu,route,p2p,attn,head;
|
||||
int64_t io,cpu_bytes; uint64_t hits,miss,ereq,n_fw,n_emit,nlat,n_p2p,cpu_rows;
|
||||
uint64_t hit_pin,hit_ecache;
|
||||
} ProfBase;
|
||||
static void prof_base(Model *m, ProfBase *b){
|
||||
b->edisk=edisk_s(); b->ewait=m->t_ewait; b->emm=m->t_emm;
|
||||
b->ecpu=m->t_ecpu; b->egpu=m->t_egpu; b->route=m->t_route; b->p2p=m->t_p2p;
|
||||
b->attn=m->t_attn; b->head=m->t_head;
|
||||
b->io=atomic_load_explicit(&g_prof_io,memory_order_relaxed);
|
||||
b->hits=m->hits; b->miss=m->miss; b->ereq=m->ereq;
|
||||
b->n_fw=m->n_fw; b->n_emit=m->n_emit; b->nlat=g_prof_nlat;
|
||||
b->hit_pin=m->hit_pin; b->hit_ecache=m->hit_ecache;
|
||||
b->n_fw=m->n_fw; b->n_emit=m->n_emit; b->nlat=g_prof_nlat; b->n_p2p=m->n_p2p;
|
||||
b->cpu_bytes=m->cpu_expert_bytes;b->cpu_rows=m->cpu_expert_rows;
|
||||
}
|
||||
|
||||
static float *falloc(int64_t n){
|
||||
@@ -1649,7 +1657,7 @@ static void model_init(Model *m, const char *snap, int cap, int ebits, int dbits
|
||||
c->index_topk, c->index_topk);
|
||||
}
|
||||
}
|
||||
m->hlast=falloc(D); m->h_all=falloc((int64_t)64*D);
|
||||
m->hlast=falloc(D); m->h_all=falloc((int64_t)512*D);
|
||||
|
||||
/* byte della parte DENSA residente (embed+lm_head+attn+mlp densa+shared+norme) */
|
||||
int64_t rb=qt_bytes(&m->embed)+qt_bytes(&m->lm_head);
|
||||
@@ -2284,6 +2292,18 @@ static void expert_prefetch(Model *m, int layer, int eid){
|
||||
static void qt_addrow(const QT *t, int row, float coef, float *acc){
|
||||
int I=t->I;
|
||||
if(t->fmt==0){ const float *w=t->qf+(int64_t)row*I; for(int i=0;i<I;i++) acc[i]+=coef*w[i]; return; }
|
||||
/* fmt=4 PRIMA del calcolo di c: s[] e' [O,ng] per-gruppo, s[row] sarebbe la scala
|
||||
* sbagliata. Senza questo ramo il fall-through int2 decodificava i nibble int4 come
|
||||
* coppie di valori a 2 bit — lo stesso bug di #298 sui kernel absorb CUDA, lato CPU.
|
||||
* EN: fmt=4 BEFORE computing c: s[] is [O,ng] per-group, s[row] would be the wrong
|
||||
* scale. Without this branch the int2 fall-through decoded int4 nibbles as pairs of
|
||||
* 2-bit values — the same bug #298 fixed in the CUDA absorb kernels, CPU side. */
|
||||
if(t->fmt==4){ 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 i=0;i+1<I;i+=2){ uint8_t b=w[i>>1];
|
||||
acc[i] +=coef*scl[i/gs] *((int)(b&0xF)-8);
|
||||
acc[i+1]+=coef*scl[(i+1)/gs]*((int)(b>>4)-8); }
|
||||
if(I&1){ uint8_t b=w[I>>1]; acc[I-1]+=coef*scl[(I-1)/gs]*((int)(b&0xF)-8); } return; }
|
||||
float c=coef*t->s[row];
|
||||
if(t->fmt==1){ const int8_t *w=t->q8+(int64_t)row*I; for(int i=0;i<I;i++) acc[i]+=c*(float)w[i]; return; }
|
||||
if(t->fmt==2){ const uint8_t *w=t->q4+(int64_t)row*((I+1)/2);
|
||||
@@ -2302,6 +2322,13 @@ static void qt_matvec_rows(const QT *t, int r0, int n, const float *x, float *y)
|
||||
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;
|
||||
for(int i=0;i+1<I;i+=2){ uint8_t b=w[i>>1]; acc+=((int)(b&0xF)-8)*x[i]+((int)(b>>4)-8)*x[i+1]; }
|
||||
if(I&1){ uint8_t b=w[I>>1]; acc+=((int)(b&0xF)-8)*x[I-1]; } a=acc*s; }
|
||||
else if(t->fmt==4){ /* per-gruppo, come matmul_i4_grouped / per-group, as matmul_i4_grouped */
|
||||
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, end=base+gs>I?I:base+gs; float acc=0;
|
||||
for(int i=base;i<end;i++){ uint8_t b=w[i>>1];
|
||||
acc+=(float)((i&1)?((int)(b>>4)-8):((int)(b&0xF)-8))*x[i]; }
|
||||
a+=(double)acc*scl[g]; } }
|
||||
else { const uint8_t *w=t->q4+(int64_t)row*((I+3)/4); float s=t->s[row]; float acc=0;
|
||||
for(int i=0;i<I;i++){ uint8_t b=w[i>>2]; acc+=((int)((b>>((i&3)*2))&3)-2)*x[i]; } a=acc*s; }
|
||||
y[j]=(float)a;
|
||||
@@ -2686,7 +2713,25 @@ static void attention_rows(Model *m, Layer *l, int layer, float *x, int S, int p
|
||||
float *sc_all = falloc((int64_t)omp_get_max_threads()*sc_cap);
|
||||
int cuda_core=0,cuda_projected=0;
|
||||
#ifdef COLI_CUDA
|
||||
if(cuda_absorb&&l->n_kv_b_shard>1){
|
||||
if(kvs&&g_cuda_enabled&&getenv("COLI_CUDA_ATTN")&&atoi(getenv("COLI_CUDA_ATTN"))&&
|
||||
!dnsel&&l->kv_b.cuda_eligible&&l->o.cuda_eligible&&
|
||||
qt_cuda_upload(&l->kv_b)&&qt_cuda_upload(&l->o)){
|
||||
const float **rl=malloc((size_t)S*sizeof(*rl)),**rr=malloc((size_t)S*sizeof(*rr));
|
||||
const void **rk=malloc((size_t)S*sizeof(*rk));
|
||||
int *rn=malloc((size_t)S*sizeof(*rn)); int mt=0;
|
||||
if(rk&&rl&&rr&&rn){
|
||||
for(int s=0;s<S;s++){
|
||||
int pos=positions[s],st0=kvs[s]->kv_start[layer]; rn[s]=pos+1-st0;
|
||||
rk[s]=kvs[s];
|
||||
rl[s]=coli_kv_row(kvs[s]->Lc[layer],st0,kvl);
|
||||
rr[s]=coli_kv_row(kvs[s]->Rc[layer],st0,c->qk_rope);
|
||||
if(rn[s]>mt)mt=rn[s];
|
||||
}
|
||||
cuda_core=cuda_projected=coli_cuda_attention_project_ragged(l->kv_b.cuda,l->o.cuda,
|
||||
out,Q,rk,rl,rr,rn,S,H,c->qk_nope,c->qk_rope,vh,kvl,mt,c->attn_scale);
|
||||
}
|
||||
free(rk);free(rl);free(rr);free(rn);
|
||||
} else if(cuda_absorb&&l->n_kv_b_shard>1){
|
||||
int n=l->n_kv_b_shard,st0=m->kv_start[layer],nt=pos_base+S-st0,ok=1;
|
||||
float *qs=falloc((int64_t)S*H*qh),*cs=falloc((int64_t)S*H*vh);
|
||||
for(int d=0;d<n;d++)for(int s=0;s<S;s++)memcpy(
|
||||
@@ -2848,6 +2893,7 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
|
||||
if(!rank_buf||!rank_w){ free(rank_buf); free(rank_w); rank_buf=NULL; rank_w=NULL; do_cache_route=0; }
|
||||
}
|
||||
/* ---- FASE A: routing di tutte le S posizioni ---- */
|
||||
double route_t0=g_prof?now_s():0;
|
||||
int *idxs=malloc((size_t)S*K*sizeof(int)); float *ws=malloc((size_t)S*K*sizeof(float));
|
||||
int *keff=malloc(S*sizeof(int));
|
||||
/* router in UN matmul batch: stessa matematica, via le S chiamate S=1 */
|
||||
@@ -2997,6 +3043,7 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
|
||||
for(int d=0;d<D;d++) out[(int64_t)s*D+d]=0;
|
||||
}
|
||||
free(rank_buf); free(rank_w);
|
||||
if(g_prof)m->t_route+=now_s()-route_t0;
|
||||
if(g_route_fp) g_route_call++;
|
||||
if(g_couple && cp_pred && S<=8)
|
||||
for(int s2=0;s2<S;s2++) couple_prefetch(m,layer,idxs+(int64_t)s2*K,keff[s2]);
|
||||
@@ -3106,9 +3153,9 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
|
||||
ESlot *use[64]; int missk[64]; int qof[64]; int nmiss=0;
|
||||
for(int j=0;j<nb;j++){ int eid=uniq[base+j]; use[j]=NULL; qof[j]=-1;
|
||||
ESlot *P=m->pin[layer];
|
||||
for(int z=0;z<m->npin[layer];z++) if(P[z].eid==eid){ m->hits++; use[j]=&P[z]; break; }
|
||||
for(int z=0;z<m->npin[layer];z++) if(P[z].eid==eid){ m->hits++; m->hit_pin++; use[j]=&P[z]; break; }
|
||||
if(!use[j]){ ESlot *Sl=m->ecache[layer]; int nn=m->ecn[layer];
|
||||
for(int z=0;z<nn;z++) if(Sl[z].eid==eid){ m->hits++; Sl[z].used=(uint64_t)__atomic_add_fetch(&m->eclock,1,__ATOMIC_RELAXED); use[j]=&Sl[z]; break; } }
|
||||
for(int z=0;z<nn;z++) if(Sl[z].eid==eid){ m->hits++; m->hit_ecache++; Sl[z].used=(uint64_t)__atomic_add_fetch(&m->eclock,1,__ATOMIC_RELAXED); use[j]=&Sl[z]; break; } }
|
||||
if(!use[j]){ qof[j]=nmiss; use[j]=&m->ws[nmiss]; missk[nmiss++]=j; m->miss++;
|
||||
if(g_disk_split){ if(m->ld_ctx==1) m->miss_draft++; else if(m->ld_ctx==2) m->miss_absorb++; } }
|
||||
}
|
||||
@@ -3263,7 +3310,7 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
|
||||
coli_cuda_expert_mlp(e->g.cuda,e->u.cuda,e->d.cuda,hh,xg,nr)){
|
||||
for(int r=0;r<nr;r++){ float *os=out+(int64_t)rows[r]*D,wgt=rw[r],*hr=hh+(int64_t)r*D;
|
||||
for(int d=0;d<D;d++) os[d]+=wgt*hr[d]; }
|
||||
m->t_emm+=now_s()-t0; continue;
|
||||
double dt=now_s()-t0;m->t_emm+=dt;if(g_prof)m->t_egpu+=dt;continue;
|
||||
}
|
||||
if(!e->slab) expert_host_ensure(m,layer,e);
|
||||
#endif
|
||||
@@ -3272,7 +3319,9 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
|
||||
matmul_qt(hh, gg, &e->d, nr);
|
||||
for(int r=0;r<nr;r++){ float *os=out+(int64_t)rows[r]*D, wgt=rw[r], *hr=hh+(int64_t)r*D;
|
||||
for(int d=0;d<D;d++) os[d]+=wgt*hr[d]; }
|
||||
m->t_emm += now_s()-t0;
|
||||
double dt=now_s()-t0;m->t_emm+=dt;if(g_prof){m->t_ecpu+=dt;
|
||||
m->cpu_expert_bytes+=qt_bytes(&e->g)+qt_bytes(&e->u)+qt_bytes(&e->d);
|
||||
m->cpu_expert_rows+=(uint64_t)nr;}
|
||||
}
|
||||
#ifdef COLI_CUDA
|
||||
ColiCudaTensor *dev_g[COLI_CUDA_MAX_DEVICES][64],*dev_u[COLI_CUDA_MAX_DEVICES][64];
|
||||
@@ -3280,6 +3329,7 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
|
||||
int dev_rows[COLI_CUDA_MAX_DEVICES][64],dev_which[COLI_CUDA_MAX_DEVICES][64];
|
||||
int dev_nc[COLI_CUDA_MAX_DEVICES]={0},dev_total[COLI_CUDA_MAX_DEVICES]={0};
|
||||
int dev_off[COLI_CUDA_MAX_DEVICES]={0},dev_ok[COLI_CUDA_MAX_DEVICES]={0};
|
||||
double dev_time[COLI_CUDA_MAX_DEVICES]={0};
|
||||
for(int di=0;di<g_cuda_ndev;di++) for(int q=0;q<ngroup;q++)
|
||||
if(group_e[q]->g.cuda_device==g_cuda_devices[di]) dev_total[di]+=group_n[q];
|
||||
for(int di=1;di<g_cuda_ndev;di++) dev_off[di]=dev_off[di-1]+dev_total[di-1];
|
||||
@@ -3296,21 +3346,28 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
|
||||
}
|
||||
double tg=now_s();
|
||||
#pragma omp parallel for if(g_cuda_ndev>1) schedule(static)
|
||||
for(int di=0;di<g_cuda_ndev;di++) if(dev_nc[di])
|
||||
for(int di=0;di<g_cuda_ndev;di++) if(dev_nc[di]){
|
||||
double td=g_prof?now_s():0;
|
||||
dev_ok[di]=coli_cuda_expert_group(dev_g[di],dev_u[di],dev_d[di],dev_rows[di],dev_nc[di],
|
||||
group_y+(int64_t)dev_off[di]*D,group_x+(int64_t)dev_off[di]*D);
|
||||
if(g_prof)dev_time[di]=now_s()-td;
|
||||
}
|
||||
for(int di=0;di<g_cuda_ndev;di++){
|
||||
int off=dev_off[di];
|
||||
for(int q=0;q<dev_nc[di];q++){
|
||||
int gi=dev_which[di][q],nr=group_n[gi]; ESlot *e=group_e[gi];
|
||||
if(!dev_ok[di]){
|
||||
for(int r=0;r<nr;r++) memcpy(xg+(int64_t)r*D,x+(int64_t)group_row[(int64_t)gi*S+r]*D,D*sizeof(float));
|
||||
double tc=g_prof?now_s():0;
|
||||
if(!coli_cuda_expert_mlp(e->g.cuda,e->u.cuda,e->d.cuda,hh,xg,nr)){
|
||||
expert_host_ensure(m,layer,e);
|
||||
expert_gate_up(gg,uu,xg,&e->g,&e->u,nr);
|
||||
for(int64_t z=0;z<(int64_t)nr*I;z++) gg[z]=siluf(gg[z])*uu[z];
|
||||
matmul_qt(hh,gg,&e->d,nr);
|
||||
if(g_prof){m->cpu_expert_bytes+=qt_bytes(&e->g)+qt_bytes(&e->u)+qt_bytes(&e->d);
|
||||
m->cpu_expert_rows+=(uint64_t)nr;}
|
||||
}
|
||||
if(g_prof)m->t_ecpu+=now_s()-tc;
|
||||
}
|
||||
float *src=dev_ok[di]?group_y+(int64_t)off*D:hh;
|
||||
for(int r=0;r<nr;r++){ float *os=out+(int64_t)group_row[(int64_t)gi*S+r]*D,wgt=group_weight[(int64_t)gi*S+r];
|
||||
@@ -3318,6 +3375,7 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
|
||||
off+=nr;
|
||||
}
|
||||
}
|
||||
if(g_prof){double mx=0;for(int di=0;di<g_cuda_ndev;di++)if(dev_time[di]>mx)mx=dev_time[di];m->t_egpu+=mx;}
|
||||
m->t_emm+=now_s()-tg;
|
||||
#endif
|
||||
/* No drain barrier: the per-expert pipe_wait(qof[j]) above (issued for every
|
||||
@@ -3925,7 +3983,11 @@ static void layers_forward_rows(Model *m, float *x, int S, int pos_base,
|
||||
float *dst=coli_cuda_pipe_scratch(dev,15,xb);
|
||||
if(dst){
|
||||
if(x_dev_on<0) ok=coli_cuda_pipe_upload(dev,dst,x,xb);
|
||||
else if(x_dev_on!=dev){ ok=coli_cuda_pipe_peer_copy(dev,dst,x_dev_on,x_dev,xb); }
|
||||
else if(x_dev_on!=dev){
|
||||
double tp=g_prof?now_s():0;
|
||||
ok=coli_cuda_pipe_peer_copy(dev,dst,x_dev_on,x_dev,xb);
|
||||
if(g_prof){m->t_p2p+=now_s()-tp;m->n_p2p++;}
|
||||
}
|
||||
else dst=x_dev;
|
||||
if(ok){
|
||||
x_dev=dst; x_dev_on=dev;
|
||||
@@ -4021,7 +4083,7 @@ static float *step_all(Model *m, const int *ids, int S, int pos_base){
|
||||
float *x=falloc((int64_t)S*D);
|
||||
for(int s=0;s<S;s++) embed_row(m, ids[s], x+(int64_t)s*D);
|
||||
layers_forward(m,x,S,pos_base);
|
||||
if(m->h_all) memcpy(m->h_all, x, (int64_t)S*D*sizeof(float)); /* hidden di TUTTE le pos (S<=64) */
|
||||
if(m->h_all) memcpy(m->h_all, x, (int64_t)S*D*sizeof(float)); /* hidden di TUTTE le pos (S<=512) */
|
||||
if(m->hlast) memcpy(m->hlast, x+(int64_t)(S-1)*D, D*sizeof(float));
|
||||
float *lo=falloc((int64_t)S*c->vocab), *row=falloc(D);
|
||||
for(int s=0;s<S;s++){ rmsnorm(row, x+(int64_t)s*D, m->final_norm, D, c->eps);
|
||||
@@ -4034,8 +4096,8 @@ static float *step_all(Model *m, const int *ids, int S, int pos_base){
|
||||
static float *step_decode_batch(Model *m, const DecodeRow *rows, int S){
|
||||
Cfg *c=&m->c; int D=c->hidden;
|
||||
/* Ragged KV currently uses MLA absorption; the stack kernel is sized to 512. */
|
||||
if(!rows || S<1 || S>64 || c->kv_lora>512) return NULL;
|
||||
KVState *kvs[64]; int positions[64];
|
||||
if(!rows || S<1 || S>512 || c->kv_lora>512) return NULL;
|
||||
KVState *kvs[512]; int positions[512];
|
||||
float *x=falloc((int64_t)S*D);
|
||||
for(int s=0;s<S;s++){
|
||||
if(!rows[s].kv || !rows[s].kv->Lc || !rows[s].kv->Rc || !rows[s].kv->kv_start ||
|
||||
@@ -4151,7 +4213,11 @@ static void mtp_absorb(Model *m, const int *next_ids, const float *x, int S, int
|
||||
}
|
||||
|
||||
static inline int argmax_v(const float *lo, int V){
|
||||
int b=0; float bv=lo[0]; for(int i=1;i<V;i++) if(lo[i]>bv){bv=lo[i];b=i;} return b;
|
||||
/* skip NaN (x==x is false for NaN) so a poisoned logit can't pin the argmax
|
||||
* to index 0 — pick the max finite/+Inf entry instead. */
|
||||
int b=-1; float bv=-INFINITY;
|
||||
for(int i=0;i<V;i++){ float x=lo[i]; if(x==x && x>bv){ bv=x; b=i; } }
|
||||
return b<0?0:b;
|
||||
}
|
||||
|
||||
/* ---- METODO F: draft grammaticale (#48) ----
|
||||
@@ -4255,9 +4321,28 @@ static void topp_siftdown(int *h, int n, int i){
|
||||
* la coda troncata va AZZERATA in g_pbuf (dist_sample la legge direttamente per id). */
|
||||
static void dist_build(const float *lo, int V){
|
||||
if(!g_pbuf){ g_pbuf=falloc(V); g_pidx=malloc(V*sizeof(int)); }
|
||||
float mx=lo[0]; for(int i=1;i<V;i++) if(lo[i]>mx) mx=lo[i];
|
||||
/* Un solo logit NaN/+Inf avvelenava tutto (#369): +Inf diventava mx, NaN/Inf-mx
|
||||
* -> expf NaN -> s NaN -> ogni prob NaN -> dist_sample cade sul fallback
|
||||
* `g_pbuf[i]>0` (NaN>0 e' falso ovunque) e ritorna 0 PER SEMPRE, in silenzio.
|
||||
* Difesa: mx solo sui finiti; un logit non finito contribuisce prob 0;
|
||||
* se la distribuzione degenera (tutti non finiti / somma non valida) si
|
||||
* ripiega sull'argmax dei finiti e si avvisa UNA volta, mai in silenzio. */
|
||||
int mxi=-1; float mx=0;
|
||||
for(int i=0;i<V;i++) if(isfinite(lo[i]) && (mxi<0 || lo[i]>mx)){ mx=lo[i]; mxi=i; }
|
||||
double s=0; float invt=1.f/(g_temp>1e-4f?g_temp:1e-4f);
|
||||
for(int i=0;i<V;i++){ g_pbuf[i]=expf((lo[i]-mx)*invt); s+=g_pbuf[i]; }
|
||||
if(mxi>=0){
|
||||
for(int i=0;i<V;i++){ g_pbuf[i]=isfinite(lo[i])?expf((lo[i]-mx)*invt):0.f; s+=g_pbuf[i]; }
|
||||
}
|
||||
if(mxi<0 || !isfinite(s) || s<=0.0){ /* distribuzione inutilizzabile */
|
||||
static int warned=0;
|
||||
if(!warned){ warned=1; fprintf(stderr,
|
||||
"[SAMPLE] warning: non-finite logits (NaN/Inf) — falling back to argmax; "
|
||||
"output may be degraded. This usually means a numerical blow-up upstream.\n"); }
|
||||
int a=(mxi>=0)?mxi:0; /* mxi = argmax dei logit FINITI (robusto
|
||||
* anche se lo[0] e' NaN, dove argmax_v fallirebbe) */
|
||||
for(int i=0;i<V;i++) g_pbuf[i]=0.f; g_pbuf[a]=1.f;
|
||||
return; /* delta su un token valido, niente top-p su NaN */
|
||||
}
|
||||
for(int i=0;i<V;i++) g_pbuf[i]/=(float)s;
|
||||
if(g_nuc>0 && g_nuc<1.f){
|
||||
for(int i=0;i<V;i++) g_pidx[i]=i;
|
||||
@@ -4558,6 +4643,11 @@ static void profile_print(Model *m, double elapsed){
|
||||
edisk_s(),m->t_ewait,m->t_emm,m->t_attn,m->t_kvb,m->t_head,elapsed-accounted);
|
||||
printf("ATTENTION: projection/RoPE %.3fs | score-softmax-value %.3fs | output projection %.3fs\n",
|
||||
m->t_aproj,m->t_acore,m->t_aout);
|
||||
if(g_prof)printf("P0-EXEC: routed CPU %.3fs / %.2f GB/s (%llu row) | routed GPU critical %.3fs | router %.3fs | residual P2P %.3fs / %llu hop | orchestration %.3fs\n",
|
||||
m->t_ecpu,m->t_ecpu>0?m->cpu_expert_bytes/1e9/m->t_ecpu:0.0,
|
||||
(unsigned long long)m->cpu_expert_rows,m->t_egpu,m->t_route,m->t_p2p,(unsigned long long)m->n_p2p,
|
||||
elapsed-m->t_ewait-m->t_emm-m->t_attn-m->t_head-m->t_route-m->t_p2p>0?
|
||||
elapsed-m->t_ewait-m->t_emm-m->t_attn-m->t_head-m->t_route-m->t_p2p:0);
|
||||
#ifdef COLI_METAL
|
||||
if(g_metal_enabled){ uint64_t ok=0,fb=0,ex=0; double su=0,gp=0,sc=0;
|
||||
coli_metal_moe_counts(&ok,&fb,&ex); coli_metal_moe_times(&su,&gp,&sc);
|
||||
@@ -4571,6 +4661,8 @@ static void profile_print(Model *m, double elapsed){
|
||||
|
||||
static void profile_reset(Model *m){
|
||||
m->t_ewait=m->t_emm=m->t_attn=m->t_kvb=m->t_head=0;
|
||||
m->t_ecpu=m->t_egpu=m->t_route=m->t_p2p=0;m->n_p2p=0;
|
||||
m->cpu_expert_bytes=0;m->cpu_expert_rows=0;
|
||||
m->t_aproj=m->t_acore=m->t_aout=0;
|
||||
atomic_store_explicit(&g_edisk_ns,0,memory_order_relaxed);
|
||||
}
|
||||
@@ -4607,19 +4699,31 @@ static void prof_report(Model *m, const ProfBase *b, double elapsed, int tokens,
|
||||
for(int i=0;i<=c->n_layers;i++){ if(m->npin)pinned+=m->npin[i]; if(m->ecn)lru+=m->ecn[i]; }
|
||||
double io_w=m->t_ewait-b->ewait; /* stall the compute thread felt */
|
||||
double io_svc=edisk_s()-b->edisk; /* read service on the loading threads (overlaps compute) */
|
||||
uint64_t dhp=m->hit_pin-b->hit_pin, dhe=m->hit_ecache-b->hit_ecache; /* split #336 */
|
||||
fprintf(f,"[PROF] expert I/O: %.3f GB fetched (%.1f MB/token, %.2f GB/s over the run%s) | "
|
||||
"hit %.1f%% (%llu hit / %llu load) | %.1f loads/token | %.1fs read service / %.1fs felt wait\n",
|
||||
"hit %.1f%% (%llu pin + %llu lru / %llu load) | %.1f loads/token | %.1fs read service / %.1fs felt wait\n",
|
||||
io/1e9, tokens>0?io/1e6/tokens:0.0, io/1e9/elapsed,
|
||||
g_mmap?"; COLI_MMAP=1: page cache may serve part":"",
|
||||
hitp,(unsigned long long)dh,(unsigned long long)dm, tokens>0?(double)dq/tokens:0.0,
|
||||
hitp,(unsigned long long)dhp,(unsigned long long)dhe,(unsigned long long)dm, tokens>0?(double)dq/tokens:0.0,
|
||||
io_svc,io_w);
|
||||
fprintf(f,"[PROF] resident experts: %d pinned (%.1f GB) + %d in LRU (%.1f GB, cap %d/layer)\n",
|
||||
pinned,pinned*eb/1e9,lru,lru*eb/1e9,m->ecap);
|
||||
double emm=m->t_emm-b->emm, attn=m->t_attn-b->attn, head=m->t_head-b->head;
|
||||
double other=elapsed-io_w-emm-attn-head; if(other<0) other=0;
|
||||
double emm=m->t_emm-b->emm, ecpu=m->t_ecpu-b->ecpu, egpu=m->t_egpu-b->egpu;
|
||||
double route=m->t_route-b->route,p2p=m->t_p2p-b->p2p;
|
||||
uint64_t np2p=m->n_p2p-b->n_p2p;
|
||||
int64_t cpu_bytes=m->cpu_expert_bytes-b->cpu_bytes;
|
||||
uint64_t cpu_rows=m->cpu_expert_rows-b->cpu_rows;
|
||||
double attn=m->t_attn-b->attn, head=m->t_head-b->head;
|
||||
double other=elapsed-io_w-emm-attn-head-route-p2p; if(other<0) other=0;
|
||||
double f_io=io_w/elapsed, f_emm=emm/elapsed, f_attn=attn/elapsed;
|
||||
fprintf(f,"[PROF] time shares: expert-I/O %.0f%% | expert-matmul %.0f%% | attention %.0f%% | lm_head %.0f%% | other %.0f%%\n",
|
||||
100*f_io,100*f_emm,100*f_attn,100*head/elapsed,100*other/elapsed);
|
||||
double slow=ecpu>egpu?ecpu:egpu,fast=ecpu<egpu?ecpu:egpu;
|
||||
fprintf(f,"[PROF] P0 execution: routed CPU %.3fs / %.2f GB/s (%llu row) | routed GPU critical %.3fs | tier straggler %.2fx | "
|
||||
"router %.3fs | residual P2P %.3fs (%llu hop, %.3f ms/hop) | orchestration %.3fs\n",
|
||||
ecpu,ecpu>0?cpu_bytes/1e9/ecpu:0.0,(unsigned long long)cpu_rows,
|
||||
egpu,fast>1e-9?slow/fast:0.0,route,p2p,(unsigned long long)np2p,
|
||||
np2p?p2p*1e3/np2p:0.0,other);
|
||||
if(f_io>=0.30){
|
||||
fprintf(f,"[PROF] verdict: I/O-bound — %.0f%% of the time waits on expert reads (hit %.0f%%).",100*f_io,hitp);
|
||||
if(hitp<90) fprintf(f," More cache is the lever: raise RAM_GB (or add RAM).");
|
||||
@@ -4646,7 +4750,7 @@ static void run_replay(Model *m, const int *full, int nfull, int np){
|
||||
if(np<2||nfull<=np){ fprintf(stderr,"REPLAY requires a non-empty prompt and continuation\n"); return; }
|
||||
kv_alloc(m,nfull+2);
|
||||
float *logit=step(m,full,np-1,0); free(logit);
|
||||
m->hits=m->miss=m->ereq=m->gpu_expert_calls=0;
|
||||
m->hits=m->miss=m->ereq=m->gpu_expert_calls=0; m->hit_pin=m->hit_ecache=0;
|
||||
profile_reset(m);
|
||||
ProfBase pb; prof_base(m,&pb);
|
||||
double t0=now_s(); int steps=0;
|
||||
@@ -4696,7 +4800,7 @@ static void run_text(Model *m, const char *snap, const char *prompt, int ngen){
|
||||
}
|
||||
prefill_t=now_s()-prefill_t;
|
||||
printf("PROFILO PREFILL (%.2fs):\n",prefill_t); profile_print(m,prefill_t);
|
||||
m->hits=m->miss=m->ereq=m->gpu_expert_calls=0;
|
||||
m->hits=m->miss=m->ereq=m->gpu_expert_calls=0; m->hit_pin=m->hit_ecache=0;
|
||||
m->n_emit=m->n_fw=0;
|
||||
g_last_repin=0;
|
||||
profile_reset(m);
|
||||
@@ -4709,8 +4813,9 @@ static void run_text(Model *m, const char *snap, const char *prompt, int ngen){
|
||||
double tot=m->hits+m->miss;
|
||||
int nsp=0; for(int i=0;i<c->n_layers;i++) if(m->L[i].sparse) nsp++;
|
||||
printf("\n---\nprefill %d tokens in %.2fs | decode %d tokens in %.2fs (%.2f tok/s) | "
|
||||
"expert hit rate %.1f%% | RSS %.2f GB",
|
||||
np,prefill_t,produced,dt,produced/dt,tot?100.0*m->hits/tot:0.0,rss_gb());
|
||||
"expert hit rate %.1f%% (pin %.1f%% + lru %.1f%%) | RSS %.2f GB", /* split #336: quale tier serve gli hit */
|
||||
np,prefill_t,produced,dt,produced/dt,tot?100.0*m->hits/tot:0.0,
|
||||
tot?100.0*m->hit_pin/tot:0.0, tot?100.0*m->hit_ecache/tot:0.0, rss_gb());
|
||||
if(g_cache_route && m->route_slots)
|
||||
printf(" | swap %.1f%% (%llu/%llu)",
|
||||
100.0*m->route_swaps/m->route_slots,
|
||||
@@ -4819,7 +4924,68 @@ static int repin_pick(Model *m, RepinCand *out, int maxc){
|
||||
}
|
||||
return nb;
|
||||
}
|
||||
/* ---- RSS GUARD (#403) -----------------------------------------------------
|
||||
* La proiezione di cap_for_ram e' una STIMA: sul GB10 (#403) le generazioni
|
||||
* lunghe l'hanno sforata di ~40 GB (proiettato 74.4, reale 115.6 -> 3 kill del
|
||||
* kernel). La run D dell'issue prova che un cap piu' basso CONTIENE la crescita:
|
||||
* questa guardia lo fa da sola, sull'RSS MISURATO invece che sul proiettato.
|
||||
* Al safe point (stessa sede di repin: nessun moe in volo), ogni ~16 token
|
||||
* emessi: se l'RSS supera il budget, svuota gli slot LRU meno usati e abbassa
|
||||
* ecap perche' non ricrescano. Gli slab sono >128KB (mmap'd da glibc): la free
|
||||
* restituisce le pagine al kernel subito, quindi l'RSS scende davvero.
|
||||
* Lo slot NON viene compattato via: resta al suo posto con eid=-1/used=0 (primo
|
||||
* candidato al riuso), perche' con PILOT_REAL il worker tiene puntatori dentro
|
||||
* ecache[] durante i suoi pread e uno spostamento li invaliderebbe; per lo
|
||||
* stesso motivo gli slot eid<0 (riservati/in caricamento) non si toccano e la
|
||||
* selezione avviene sotto g_pilot_mx. resident_bytes resta invariato: gli slot
|
||||
* LRU non sono mai contati li' (solo pin e densa).
|
||||
* EN: evict = free the slab in place (eid=-1, used=0, never compact: PILOT_REAL
|
||||
* EN: holds pointers into ecache[] across its preads), skip eid<0 reservations,
|
||||
* EN: select under g_pilot_mx. RSS_GUARD_GB=<gb> forces an explicit ceiling. */
|
||||
static double g_ram_budget_gb=0; /* budget risolto, scritto da cap_for_ram */
|
||||
static uint64_t g_rssg_last=0;
|
||||
static void rss_guard(Model *m){
|
||||
double lim = getenv("RSS_GUARD_GB") ? atof(getenv("RSS_GUARD_GB")) : g_ram_budget_gb;
|
||||
if(lim<=0) return;
|
||||
if(m->n_emit - g_rssg_last < 16) return;
|
||||
g_rssg_last = m->n_emit;
|
||||
double rss=rss_gb();
|
||||
if(rss <= lim*1.02+0.3) return; /* tolleranza: 2% + 300MB */
|
||||
Cfg *c=&m->c;
|
||||
int64_t need=(int64_t)((rss-lim)*1e9), freed=0; int dropped=0;
|
||||
for(int pass=0; pass<8 && freed<need; pass++){
|
||||
for(int l=0; l<=c->n_layers && freed<need; l++){
|
||||
if(!m->ecache || !m->ecache[l]) continue;
|
||||
pthread_mutex_lock(&g_pilot_mx);
|
||||
int nn=m->ecn[l], lru=-1;
|
||||
for(int z=0;z<nn;z++){ /* solo slot pubblicati e con slab */
|
||||
ESlot *cand=&m->ecache[l][z];
|
||||
if(cand->eid<0 || !cand->slab) continue;
|
||||
if(lru<0 || cand->used<m->ecache[l][lru].used) lru=z;
|
||||
}
|
||||
if(lru<0){ pthread_mutex_unlock(&g_pilot_mx); continue; }
|
||||
ESlot *s=&m->ecache[l][lru];
|
||||
s->eid=-1; /* nascosto: nessun hit/evict altrui */
|
||||
pthread_mutex_unlock(&g_pilot_mx);
|
||||
int64_t sb=s->slab_cap + s->fslab_cap*4;
|
||||
#ifdef COLI_METAL
|
||||
if(s->slab && g_metal_enabled) coli_metal_unregister(s->slab);
|
||||
#endif
|
||||
compat_aligned_free(s->slab); free(s->fslab);
|
||||
s->slab=NULL; s->fslab=NULL; s->slab_cap=s->fslab_cap=0;
|
||||
QT *q[3]={&s->g,&s->u,&s->d};
|
||||
for(int k=0;k<3;k++){ q[k]->qf=NULL; q[k]->q8=NULL; q[k]->q4=NULL; q[k]->s=NULL; }
|
||||
s->used=0; /* primo candidato al riuso */
|
||||
freed += sb; dropped++;
|
||||
}
|
||||
if(m->ecap>2) m->ecap--; /* il tetto scende: niente ricrescita */
|
||||
}
|
||||
if(dropped)
|
||||
fprintf(stderr,"[RAM-GUARD] RSS %.1f GB over the %.1f GB budget (#403): "
|
||||
"dropped %d cached experts, cap -> %d\n", rss, lim, dropped, m->ecap);
|
||||
}
|
||||
static void repin_pass_limit(Model *m,int limit){
|
||||
rss_guard(m); /* #403: il budget si fa rispettare sull'RSS MISURATO */
|
||||
if(g_repin<=0) return;
|
||||
if(m->n_emit - g_last_repin < (uint64_t)g_repin) return;
|
||||
g_last_repin = m->n_emit;
|
||||
@@ -5168,7 +5334,7 @@ static void run_serve_mux(Model *m, const char *snap){
|
||||
g_draft=0; /* one scheduler owns every forward; MTP/speculation is not ragged-safe */
|
||||
int maxctx=getenv("CTX")?atoi(getenv("CTX")):4096;
|
||||
int nctx=getenv("KV_SLOTS")?atoi(getenv("KV_SLOTS")):1;
|
||||
if(nctx<1||nctx>16){fprintf(stderr,"KV_SLOTS deve essere tra 1 e 16\n");exit(2);}
|
||||
if(nctx<1||nctx>512){fprintf(stderr,"KV_SLOTS must be between 1 and 512\n");exit(2);}
|
||||
g_kvsave=getenv("KVSAVE")?atoi(getenv("KVSAVE")):1;
|
||||
KVState *initial=m->kv; free(initial->kv_start); free(initial);
|
||||
ServeCtx *ctx=calloc(nctx,sizeof(*ctx)); ServeReq *req=calloc(nctx,sizeof(*req));
|
||||
@@ -5224,7 +5390,7 @@ static void run_serve_mux(Model *m, const char *snap){
|
||||
}
|
||||
active=0; for(int i=0;i<nctx;i++) active+=req[i].active;
|
||||
if(!active){ if(eof) break; continue; }
|
||||
DecodeRow rows[16]; int slots[16], S=0;
|
||||
DecodeRow rows[512]; int slots[512], S=0;
|
||||
for(int i=0;i<nctx;i++) if(req[i].active){
|
||||
rows[S]=(DecodeRow){&ctx[i].kv,req[i].pending,ctx[i].len}; slots[S++]=i;
|
||||
}
|
||||
@@ -5919,6 +6085,7 @@ static void cap_for_ram(Model *m, double ram_gb, int ebits, int max_ctx){
|
||||
if(auto_b){ ram_gb = g_mem_avail_boot*0.88; /* misurata PRIMA del load: il residente gia'
|
||||
* allocato viene sottratto sotto, non due volte */
|
||||
if(ram_gb<4){ fprintf(stderr,"[RAM] MemAvailable is unreadable or too low; assuming 8 GB\n"); ram_gb=8; } }
|
||||
g_ram_budget_gb = ram_gb; /* #403: la RSS-guard usa il budget RISOLTO */
|
||||
/* slack ONESTO, non forfettario (l'OOM del 2026-07-04 veniva da qui):
|
||||
* ws[64] slab del working-set (si materializzano TUTTI nel prefill batch-union),
|
||||
* KV cache a max_ctx, kvb_all della ricostruzione k/v in attention,
|
||||
@@ -6008,9 +6175,9 @@ static void cap_for_ram(Model *m, double ram_gb, int ebits, int max_ctx){
|
||||
* self-test, and would "generate" from "$P$G". So on Windows a PROMPT carrying
|
||||
* cmd's $-metacodes is ignored; set COLI_PROMPT to pass a real prompt from cmd. */
|
||||
static const char *coli_user_prompt(void){
|
||||
const char *p = getenv("COLI_PROMPT");
|
||||
const char *p = getenv_utf8("COLI_PROMPT");
|
||||
if(p) return p;
|
||||
p = getenv("PROMPT");
|
||||
p = getenv_utf8("PROMPT");
|
||||
#ifdef _WIN32
|
||||
if(p) for(const char *q=p; q[0]; q++)
|
||||
if(q[0]=='$' && q[1] && strchr("ABCDEFGHLNPQSTV_+|$", q[1]&~0x20)){ p=NULL; break; }
|
||||
@@ -6231,8 +6398,9 @@ int main(int argc, char **argv){
|
||||
int cap = argc>1?atoi(argv[1]):64;
|
||||
int ebits= argc>2?atoi(argv[2]):8;
|
||||
int dbits= argc>3?atoi(argv[3]):ebits;
|
||||
if(getenv("SERVE") && (kv_slot_count()<1 || kv_slot_count()>16)){
|
||||
fprintf(stderr,"KV_SLOTS must be between 1 and 16\n"); return 2;
|
||||
int kv_limit=(getenv("SERVE_BATCH")&&atoi(getenv("SERVE_BATCH")))?512:16;
|
||||
if(getenv("SERVE") && (kv_slot_count()<1 || kv_slot_count()>kv_limit)){
|
||||
fprintf(stderr,"KV_SLOTS must be between 1 and %d\n",kv_limit); return 2;
|
||||
}
|
||||
#ifdef COLI_CUDA
|
||||
if(getenv("COLI_CUDA") && atoi(getenv("COLI_CUDA"))){
|
||||
@@ -6303,11 +6471,22 @@ int main(int argc, char **argv){
|
||||
#endif
|
||||
}
|
||||
if(getenv("DSA_TOPK")) m.c.index_topk=atoi(getenv("DSA_TOPK")); /* override per test */
|
||||
/* Il path MUX (SERVE_BATCH=1, cioe' `coli serve`) forza g_draft=0 sotto —
|
||||
* la speculazione non e' ragged-safe nel batch multi-slot. Segnalarlo QUI,
|
||||
* altrimenti "MTP active (draft=8)" mentirebbe: il messaggio e' stampato
|
||||
* prima della scelta del path (run_serve_mux, sotto), e con DRAFT=8 diceva
|
||||
* "active" per poi disabilitarlo in silenzio (#358, LordMZTE). */
|
||||
int mux_will_disable_mtp = getenv("SERVE") && getenv("SERVE_BATCH") && atoi(getenv("SERVE_BATCH"));
|
||||
int eff_draft = mux_will_disable_mtp ? 0 : g_draft;
|
||||
printf("loaded in %.2fs | resident dense: %.2f MB | layers=%d experts=%d | MTP %s (draft=%d)\n",
|
||||
now_s()-t0, m.resident_bytes/(1024.0*1024.0), m.c.n_layers, m.c.n_experts,
|
||||
m.has_mtp?"ACTIVE":"absent", g_draft);
|
||||
m.has_mtp?(mux_will_disable_mtp?"DISABLED (multiplexed serve)":"ACTIVE"):"absent", eff_draft);
|
||||
/* anche su stderr: e' il canale che le UI (coli) mostrano all'utente */
|
||||
fprintf(stderr,"[MTP] %s (draft=%d)\n", m.has_mtp?"active: native speculative decoding":"absent", g_draft);
|
||||
if(mux_will_disable_mtp && m.has_mtp)
|
||||
fprintf(stderr,"[MTP] disabled in multiplexed serve (SERVE_BATCH=1): speculation is not "
|
||||
"ragged-safe across KV slots. Single-client interactive use (`coli chat`) keeps MTP.\n");
|
||||
else
|
||||
fprintf(stderr,"[MTP] %s (draft=%d)\n", m.has_mtp?"active: native speculative decoding":"absent", eff_draft);
|
||||
#ifdef __linux__
|
||||
{ /* Only warn for a GENUINE 9p mount (WSL Windows drives, magic 0x01021997), where
|
||||
* fadvise is a no-op. The old check was `snap` starting with "/mnt/", which
|
||||
@@ -6457,8 +6636,9 @@ int main(int argc, char **argv){
|
||||
double tot=m.hits+m.miss;
|
||||
printf("N-gram speculation (DRAFT=%d): %.2f tokens/forward (%llu forwards per %llu tokens)\n",
|
||||
g_draft, m.n_fw?(double)m.n_emit/m.n_fw:1.0, (unsigned long long)m.n_fw, (unsigned long long)m.n_emit);
|
||||
printf("Expert cache hit rate: %.1f%% (hit=%llu miss=%llu) | RSS: %.2f GB | %.1f tok/s\n",
|
||||
tot?100.0*m.hits/tot:0.0, (unsigned long long)m.hits, (unsigned long long)m.miss, rss_gb(), n_new/dt);
|
||||
printf("Expert cache hit rate: %.1f%% (%llu pin + %llu lru / %llu miss) | RSS: %.2f GB | %.1f tok/s\n",
|
||||
tot?100.0*m.hits/tot:0.0, (unsigned long long)m.hit_pin, (unsigned long long)m.hit_ecache,
|
||||
(unsigned long long)m.miss, rss_gb(), n_new/dt);
|
||||
profile_print(&m,dt);
|
||||
if(g_prof) prof_report(&m,&pb,dt,n_new,stdout);
|
||||
#ifdef COLI_CUDA
|
||||
|
||||
+12
-1
@@ -271,6 +271,14 @@ def parse_tool_calls(reply, tools=None):
|
||||
salvaged.append(name)
|
||||
calls.append({"id": "call_" + uuid.uuid4().hex[:24], "type": "function",
|
||||
"function": {"name": name, "arguments": json.dumps(args, ensure_ascii=False)}})
|
||||
if tools and not calls and re.search(r"</?tool_call>|</?arg_key>|</?arg_value>", reply):
|
||||
# Diagnosi per la #401: il client ha dichiarato i tools e il modello ha PROVATO la
|
||||
# sintassi, ma il parse rigoroso non ha agganciato nulla (tipico output int4 storpiato).
|
||||
# EN: #401 field diagnosis: tools were declared and the model attempted the syntax,
|
||||
# EN: but the strict parse matched nothing (typically quantization-mangled output).
|
||||
sys.stderr.write("[api] tools declared and tool-call markers present, but no call "
|
||||
"parsed -- output may be quantization-mangled; try COLI_TOOL_SALVAGE=1\n")
|
||||
sys.stderr.flush()
|
||||
text = _BOX_RE.sub("", reply)
|
||||
if THINK_CLOSE in text:
|
||||
text = text.split(THINK_CLOSE, 1)[1]
|
||||
@@ -406,7 +414,10 @@ def generation_options(body, limit):
|
||||
maximum = body.get("max_tokens")
|
||||
maximum_param = "max_tokens"
|
||||
if maximum is None:
|
||||
maximum = min(256, limit)
|
||||
# Client omitted max_tokens: honor the operator's configured budget (--max-tokens /
|
||||
# --ngen), not an arbitrary 256 — `coli serve --ngen 32768` must mean 32768 (#382).
|
||||
# Generation still ends at EOS, so this is a cap, not a target.
|
||||
maximum = limit
|
||||
temperature = body.get("temperature")
|
||||
top_p = body.get("top_p")
|
||||
temperature = 0.7 if temperature is None else temperature
|
||||
|
||||
+23
-1
@@ -203,6 +203,22 @@ def physical_cpu_count():
|
||||
return os.cpu_count() or 1
|
||||
|
||||
|
||||
def cpu_socket_count():
|
||||
"""Return the number of physical CPU sockets visible to this process."""
|
||||
if not sys.platform.startswith("linux"):
|
||||
return 1
|
||||
try:
|
||||
result = subprocess.run(["lscpu", "-p=socket"], text=True,
|
||||
capture_output=True, check=True, timeout=5)
|
||||
sockets = {int(line) for line in result.stdout.splitlines()
|
||||
if line and not line.startswith("#")}
|
||||
if sockets:
|
||||
return len(sockets)
|
||||
except (OSError, ValueError, subprocess.SubprocessError):
|
||||
pass
|
||||
return 1
|
||||
|
||||
|
||||
POLICIES = {
|
||||
"quality": {"preserve_quantization": True, "preserve_router": True},
|
||||
"balanced": {"preserve_quantization": True, "preserve_router": True},
|
||||
@@ -212,11 +228,12 @@ POLICIES = {
|
||||
|
||||
def build_plan(model, ram_gb=0, context=4096, gpu_indices=None, vram_gb=0,
|
||||
available_memory=None, available_disk=None, gpus=None,
|
||||
policy="quality", physical_cpus=None):
|
||||
policy="quality", physical_cpus=None, cpu_sockets=None):
|
||||
if policy not in POLICIES:
|
||||
raise ValueError(f"unknown policy: {policy}")
|
||||
info = analyze_model(model)
|
||||
physical_cpus = physical_cpu_count() if physical_cpus is None else physical_cpus
|
||||
cpu_sockets = cpu_socket_count() if cpu_sockets is None else cpu_sockets
|
||||
cfg = info["config"]
|
||||
available_memory = memory_available() if available_memory is None else available_memory
|
||||
if available_disk is None:
|
||||
@@ -286,6 +303,7 @@ def build_plan(model, ram_gb=0, context=4096, gpu_indices=None, vram_gb=0,
|
||||
"quality_preserving": policy != "experimental-fast"},
|
||||
"model": {key: value for key, value in info.items() if key != "config"},
|
||||
"cpu": {"physical_cores": max(1, int(physical_cpus)),
|
||||
"sockets": max(1, int(cpu_sockets)),
|
||||
"thread_policy": "physical-cores"},
|
||||
"tiers": {
|
||||
"disk": {"role": "cold-backing", "model_bytes": info["model_bytes"],
|
||||
@@ -318,6 +336,10 @@ def environment_for_plan(plan, env=None, cuda_enabled=True):
|
||||
# ("Affinity not supported on this configuration"): non impostarle li'.
|
||||
result.setdefault("OMP_PROC_BIND", "spread")
|
||||
result.setdefault("OMP_PLACES", "cores")
|
||||
if sys.platform.startswith("linux") and plan["cpu"].get("sockets", 1) > 1:
|
||||
# Selectively interleave large expert/dense slabs across memory controllers.
|
||||
# Unlike blanket numactl interleave, this leaves CUDA staging buffers local.
|
||||
result.setdefault("COLI_NUMA", "1")
|
||||
if plan["policy"]["name"] == "balanced":
|
||||
result.setdefault("REPIN", "64")
|
||||
ram = plan["tiers"]["ram"]
|
||||
|
||||
+47
-14
@@ -59,6 +59,12 @@ static double bench_ns(void (*fn)(const float*,int,int,int*,int*),
|
||||
return ts[N_REPEAT/2];
|
||||
}
|
||||
|
||||
/* Sort an array of doubles ascending (median-of-medians aggregation below). */
|
||||
static void dsort(double *a, int n){
|
||||
for(int s=1;s<n;s++){ double k=a[s]; int b=s-1;
|
||||
while(b>=0 && a[b]>k){ a[b+1]=a[b]; b--; } a[b+1]=k; }
|
||||
}
|
||||
|
||||
/* the NEW algorithm calls the real partial_select_desc + replicates the production
|
||||
* threshold derivation and position scans. */
|
||||
static void keep_new(const float *isc, int nk, int keep, int *dst, int *nd_out){
|
||||
@@ -74,6 +80,7 @@ static void keep_new(const float *isc, int nk, int keep, int *dst, int *nd_out){
|
||||
|
||||
/* deterministic score fill for three shapes */
|
||||
static uint32_t brng = 0xA5A5A5A5u;
|
||||
static void brng_seed(uint32_t s){ brng = s; }
|
||||
static double brand(void){ brng ^= brng << 13; brng ^= brng >> 17; brng ^= brng << 5;
|
||||
return (double)(brng >> 8) * (1.0 / 16777216.0); }
|
||||
static void fill_realistic(float *isc, int nk){ /* few hot, long distinct tail */
|
||||
@@ -87,11 +94,24 @@ static void fill_plateau(float *isc, int nk){ /* tie blocks -> boundary path
|
||||
for(int i=0;i<nk;i++) isc[i]=(float)(-(double)(i/7));
|
||||
}
|
||||
|
||||
/* Multi-seed aggregation. Per @KingIcyCreamProjects (#357 thread): with a single frozen
|
||||
* input per cell, quickselect's deterministic median-of-three pivot means one lucky input
|
||||
* can spike a single nk row (an observed ~75x at nk=8192 on a 9950X3D that was really a
|
||||
* ~13-40x algorithm). Two bugs compounded it: (1) the old bench drew ONE input per cell;
|
||||
* (2) brng was never reset, so each cell's input depended on every prior cell's draws --
|
||||
* reordering nks[] silently shifted all later inputs. Both fixed here: brng is reseeded
|
||||
* per (shape, nk, seed), and we take the MEDIAN of N_SEEDS independent inputs, each itself
|
||||
* a median over N_REPEAT timing reps. A lucky pivot now moves one of the N_SEEDS samples,
|
||||
* not the reported number. */
|
||||
|
||||
int main(void){
|
||||
int keep = 2048; /* GLM-5.2 index_topk */
|
||||
int nks[] = {2049, 4096, 8192, 16384, 32768, 65536};
|
||||
const int N_SEEDS = 11; /* odd so the median is a real sample, not interpolated */
|
||||
float *isc = malloc((size_t)65536*sizeof(float));
|
||||
int *dst = malloc((size_t)65536*sizeof(int)); int nd;
|
||||
double *old_seeds = malloc((size_t)N_SEEDS*sizeof(double));
|
||||
double *new_seeds = malloc((size_t)N_SEEDS*sizeof(double));
|
||||
|
||||
struct { const char *name; void (*fill)(float*,int); } shapes[] = {
|
||||
{ "realistic", fill_realistic },
|
||||
@@ -99,27 +119,40 @@ int main(void){
|
||||
{ "plateau", fill_plateau },
|
||||
};
|
||||
|
||||
printf("bench_dsa_select: DSA top-keep, old (qsort) vs new (partial-select) keep=%d\n", keep);
|
||||
printf("bench_dsa_select: DSA top-keep, old (qsort) vs new (partial-select) keep=%d (median of %d seeds x %d reps)\n",
|
||||
keep, N_SEEDS, N_REPEAT);
|
||||
printf("%-12s %7s %14s %14s %9s\n", "shape", "nk", "old ns/call", "new ns/call", "speedup");
|
||||
printf("------------------------------------------------------------------------\n");
|
||||
|
||||
for(size_t sh=0; sh<sizeof(shapes)/sizeof(shapes[0]); sh++){
|
||||
for(size_t ni=0; ni<sizeof(nks)/sizeof(nks[0]); ni++){
|
||||
int nk=nks[ni];
|
||||
shapes[sh].fill(isc,nk);
|
||||
/* warmup both paths so caches/branch predictors are primed */
|
||||
for(int w=0; w<50; w++){ keep_old(isc,nk,keep,dst,&nd); keep_new(isc,nk,keep,dst,&nd); }
|
||||
/* sanity: both must keep exactly `keep` (correctness is test_dsa_select's
|
||||
* job, but a count divergence here would make the timing meaningless) */
|
||||
keep_old(isc,nk,keep,dst,&nd); int na=nd;
|
||||
keep_new(isc,nk,keep,dst,&nd); int nb=nd;
|
||||
if(na!=keep || nb!=keep){
|
||||
printf("%-12s %7d (BAD COUNTS: old=%d new=%d, skipped)\n",
|
||||
shapes[sh].name, nk, na, nb);
|
||||
int bad = 0;
|
||||
for(int sd=0; sd<N_SEEDS; sd++){
|
||||
/* reseed per (shape,nk,seed) so each cell's input is reproducible and
|
||||
* independent of cell ordering, and so lucky pivots are sampled, not fixed. */
|
||||
brng_seed(0xA5A5A5A5u + (uint32_t)(sd*0x9E3779B9u));
|
||||
shapes[sh].fill(isc,nk);
|
||||
/* warmup both paths so caches/branch predictors are primed */
|
||||
for(int w=0; w<50; w++){ keep_old(isc,nk,keep,dst,&nd); keep_new(isc,nk,keep,dst,&nd); }
|
||||
/* sanity: both must keep exactly `keep` (correctness is test_dsa_select's
|
||||
* job, but a count divergence here would make the timing meaningless) */
|
||||
keep_old(isc,nk,keep,dst,&nd); int na=nd;
|
||||
keep_new(isc,nk,keep,dst,&nd); int nb=nd;
|
||||
if(na!=keep || nb!=keep){ bad++; continue; }
|
||||
old_seeds[sd] = bench_ns(keep_old,isc,nk,keep);
|
||||
new_seeds[sd] = bench_ns(keep_new,isc,nk,keep);
|
||||
}
|
||||
if(bad == N_SEEDS){
|
||||
printf("%-12s %7d (BAD COUNTS on all %d seeds, skipped)\n",
|
||||
shapes[sh].name, nk, bad);
|
||||
continue;
|
||||
}
|
||||
double t_old=bench_ns(keep_old,isc,nk,keep);
|
||||
double t_new=bench_ns(keep_new,isc,nk,keep);
|
||||
/* report median-of-seed-medians: robust to a single lucky/unlucky pivot */
|
||||
dsort(old_seeds, N_SEEDS);
|
||||
dsort(new_seeds, N_SEEDS);
|
||||
double t_old = old_seeds[N_SEEDS/2];
|
||||
double t_new = new_seeds[N_SEEDS/2];
|
||||
printf("%-12s %7d %14.0f %14.0f %8.2fx\n",
|
||||
shapes[sh].name, nk, t_old, t_new, t_old/t_new);
|
||||
}
|
||||
@@ -127,6 +160,6 @@ int main(void){
|
||||
}
|
||||
|
||||
printf("bench_dsa_select: done (lower ns is better; speedup = old/new)\n");
|
||||
free(isc); free(dst);
|
||||
free(isc); free(dst); free(old_seeds); free(new_seeds);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import unittest
|
||||
|
||||
from tools.benchmark_cuda_fixture import parse_output
|
||||
from tools.benchmark_cuda_fixture import parse_output, parse_p0
|
||||
|
||||
|
||||
SAMPLE = """
|
||||
REPLAY decode: 4 tokens | 12.34 tok/s
|
||||
PROFILE: expert-disk 1.25s | expert-matmul 2.50s | attention 0.75s | lm_head 0.10s | other -0.05s
|
||||
P0-EXEC: routed CPU 1.200s / 123.40 GB/s (456 row) | routed GPU critical 0.150s | router 0.200s | residual P2P 0.030s / 75 hop | orchestration 0.100s
|
||||
"""
|
||||
|
||||
|
||||
@@ -19,6 +20,9 @@ class ParseOutputTest(unittest.TestCase):
|
||||
with self.assertRaisesRegex(RuntimeError, "benchmark output missing"):
|
||||
parse_output("REPLAY decode: 4 tokens | 12.34 tok/s", "engine failed")
|
||||
|
||||
def test_extracts_p0_profile(self):
|
||||
self.assertEqual(parse_p0(SAMPLE), [1.2, 123.4, 456.0, 0.15, 0.2, 0.03, 75.0, 0.1])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/* Regression for non-finite-logit poisoning of sampling.
|
||||
*
|
||||
* A single NaN or +Inf in the logits (a bad streamed expert tile, or an fp
|
||||
* overflow in the matmul at a low-RAM eviction boundary) used to make softmax
|
||||
* produce an all-NaN g_pbuf; dist_sample then never satisfied cum>=u and fell
|
||||
* through to return token 0 — so the engine silently emitted an unbroken run of
|
||||
* token 0 with no error, on the DEFAULT serve path (TEMP>0, 0<NUCLEUS<1).
|
||||
*
|
||||
* Fix under test: argmax_v() skips NaN (picks the max finite/+Inf entry instead
|
||||
* of being pinned to index 0), and dist_build() detects a non-finite softmax sum
|
||||
* and collapses to a one-hot over the finite argmax (warning once) rather than
|
||||
* dividing every entry into NaN. Degrade + diagnose, never silently corrupt.
|
||||
*
|
||||
* No model file needed: exercises argmax_v / dist_build / dist_sample directly. */
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#define main coli_glm_main_unused
|
||||
#include "../glm.c"
|
||||
#undef main
|
||||
|
||||
static int approx1(double x){ return x > 0.999 && x < 1.001; }
|
||||
|
||||
int main(void){
|
||||
/* --- argmax_v must skip NaN (greedy decode + speculative-verify paths) --- */
|
||||
{ float lo[8]={NAN,1.f,5.f,2.f,NAN,-3.f,4.f,0.f};
|
||||
assert(argmax_v(lo,8)==2 && "pick max finite (idx2=5.0), not NaN-pinned idx0"); }
|
||||
{ float lo[8]={3.f,INFINITY,1.f,2.f,0.f,-1.f,2.5f,1.5f};
|
||||
assert(argmax_v(lo,8)==1 && "pick the +Inf position"); }
|
||||
{ float lo[8]; for(int i=0;i<8;i++) lo[i]=NAN;
|
||||
assert(argmax_v(lo,8)==0 && "all-NaN: no crash, defined fallback"); }
|
||||
|
||||
g_temp=0.7f; g_nuc=0.9f; /* the default serve/chat sampling path */
|
||||
|
||||
/* --- dist_build: a NaN logit must yield a finite one-hot, not all-NaN --- */
|
||||
{ float lo[8]={0.5f,1.f,NAN,8.f,0.2f,-1.f,0.f,0.3f}; /* max finite = idx3 (8.0) */
|
||||
dist_build(lo,8);
|
||||
double sum=0; int nan=0;
|
||||
for(int i=0;i<8;i++){ if(!(g_pbuf[i]==g_pbuf[i])) nan=1; sum+=g_pbuf[i]; }
|
||||
assert(!nan && "g_pbuf must be finite after a NaN logit");
|
||||
assert(approx1(sum) && "g_pbuf must normalize to 1");
|
||||
assert(approx1(g_pbuf[3]) && "mass must land on the max finite logit (idx3)");
|
||||
assert(dist_sample(8,-1)==3 && "sampler emits the finite argmax, not token 0"); }
|
||||
|
||||
/* --- NaN at index 0: poisons the max scan itself (the old mx=lo[0] seed),
|
||||
* the failure mode that starts before the sum (review note on #369) --- */
|
||||
{ float lo[8]={NAN,1.f,0.5f,6.f,0.2f,-1.f,0.f,0.3f}; /* max finite = idx3 (6.0) */
|
||||
dist_build(lo,8);
|
||||
double sum=0; int nan=0;
|
||||
for(int i=0;i<8;i++){ if(!(g_pbuf[i]==g_pbuf[i])) nan=1; sum+=g_pbuf[i]; }
|
||||
assert(!nan && "NaN at lo[0] must not poison via the mx seed");
|
||||
assert(approx1(sum) && "still normalizes to 1");
|
||||
assert(dist_sample(8,-1)==3 && "emits the max finite logit, not token 0"); }
|
||||
|
||||
/* --- all-NaN logits: worst case — must stay finite, no crash --- */
|
||||
{ float lo[8]; for(int i=0;i<8;i++) lo[i]=NAN;
|
||||
dist_build(lo,8);
|
||||
for(int i=0;i<8;i++) assert(g_pbuf[i]==g_pbuf[i] && "all-NaN: g_pbuf stays finite"); }
|
||||
|
||||
/* --- regression: clean logits still produce a valid distribution --- */
|
||||
{ float lo[8]={0.1f,0.2f,3.0f,0.4f,0.5f,0.6f,0.7f,0.8f}; /* peak = idx2 */
|
||||
dist_build(lo,8);
|
||||
double sum=0; int nan=0;
|
||||
for(int i=0;i<8;i++){ if(!(g_pbuf[i]==g_pbuf[i])) nan=1; sum+=g_pbuf[i]; }
|
||||
assert(!nan && "clean softmax stays finite");
|
||||
assert(approx1(sum) && "clean softmax must sum to 1");
|
||||
assert(g_pbuf[2]>=g_pbuf[0] && "peak token keeps the most mass"); }
|
||||
|
||||
printf("OK test_logit_nan: argmax_v NaN-skip + dist_build finite-collapse\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
"""End-to-end tool-calling test for the OpenAI gateway (#401).
|
||||
|
||||
Unlike the unit tests in test_openai_server.py (which call parse_tool_calls /
|
||||
render_chat directly), this suite runs openai_server.py as a real subprocess
|
||||
against a mock engine that speaks the actual SERVE wire protocol
|
||||
(READY / SUBMIT / DATA / DONE), then talks to it over real HTTP. It pins down
|
||||
the full path a coding client exercises: tool declaration rendering, marker
|
||||
suppression in streamed deltas (across chunk boundaries), tool_calls in both
|
||||
response shapes, and the <|observation|><tool_response> round trip.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
SERVER = Path(__file__).resolve().parent.parent / "openai_server.py"
|
||||
MODEL_ID = "glm-5.2-colibri"
|
||||
|
||||
# Mock engine: replies are keyed on the prompt so one process covers every case.
|
||||
# Prompts received are appended to MOCK_LOG for assertions on the rendering.
|
||||
MOCK_ENGINE = r'''#!/usr/bin/env python3
|
||||
import sys, os
|
||||
out, inp = sys.stdout.buffer, sys.stdin.buffer
|
||||
out.write(b"\x01\x01READY\x01\x01\n" + b"STAT 0 0 0 0 0\n"); out.flush()
|
||||
|
||||
def reply(rid, text, chunks=1):
|
||||
data = text.encode("utf-8")
|
||||
n = max(1, len(data) // chunks)
|
||||
for i in range(0, len(data), n):
|
||||
part = data[i:i+n]
|
||||
out.write(("DATA %s %d\n" % (rid, len(part))).encode() + part + b"\n"); out.flush()
|
||||
out.write(("DONE %s STAT %d 1.0 50.0 10.0 42 0\n" % (rid, len(text.split()))).encode())
|
||||
out.flush()
|
||||
|
||||
while True:
|
||||
line = inp.readline()
|
||||
if not line: break
|
||||
f = line.decode().strip().split()
|
||||
if not f or f[0] != "SUBMIT": continue
|
||||
rid, plen = f[1], int(f[3])
|
||||
prompt = inp.read(plen).decode("utf-8", "replace"); inp.read(1)
|
||||
with open(os.environ["MOCK_LOG"], "a") as log:
|
||||
log.write(prompt + "\n\x00\n")
|
||||
if "<tool_response>" in prompt:
|
||||
reply(rid, "25 degrees and sunny in Rome.")
|
||||
elif "weather in Rome" in prompt:
|
||||
reply(rid, "<tool_call>get_weather<arg_key>city</arg_key>"
|
||||
"<arg_value>Rome</arg_value></tool_call>")
|
||||
elif "weather in Milan" in prompt:
|
||||
# split across many tiny DATA chunks: streamed marker suppression must
|
||||
# hold even when a marker straddles a chunk boundary
|
||||
reply(rid, "Checking. <tool_call>get_weather<arg_key>city</arg_key>"
|
||||
"<arg_value>Milan</arg_value></tool_call>", chunks=20)
|
||||
else:
|
||||
reply(rid, "Hello from the mock engine.")
|
||||
'''
|
||||
|
||||
TOOLS = [{"type": "function", "function": {
|
||||
"name": "get_weather",
|
||||
"description": "Current weather for a city",
|
||||
"parameters": {"type": "object",
|
||||
"properties": {"city": {"type": "string"}},
|
||||
"required": ["city"]}}}]
|
||||
|
||||
|
||||
@unittest.skipUnless(os.name == "posix",
|
||||
"the mock engine is a shebang script the gateway execs directly; "
|
||||
"Windows CreateProcess cannot run it. The gateway logic under test "
|
||||
"is platform-independent and covered by the POSIX CI jobs.")
|
||||
class ToolCallingE2E(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.tmp = tempfile.TemporaryDirectory()
|
||||
mock = Path(cls.tmp.name) / "mock_engine.py"
|
||||
mock.write_text(MOCK_ENGINE)
|
||||
mock.chmod(0o755)
|
||||
cls.mock_log = Path(cls.tmp.name) / "prompts.log"
|
||||
cls.mock_log.touch()
|
||||
with socket.socket() as probe: # free port, then hand it to the server
|
||||
probe.bind(("127.0.0.1", 0))
|
||||
cls.port = probe.getsockname()[1]
|
||||
env = dict(os.environ, MOCK_LOG=str(cls.mock_log))
|
||||
cls.server = subprocess.Popen(
|
||||
[sys.executable, str(SERVER), "--model", cls.tmp.name,
|
||||
"--engine", str(mock), "--port", str(cls.port)],
|
||||
env=env, stderr=subprocess.DEVNULL)
|
||||
cls.base = f"http://127.0.0.1:{cls.port}/v1"
|
||||
for _ in range(100):
|
||||
try:
|
||||
urllib.request.urlopen(cls.base + "/models", timeout=2)
|
||||
return
|
||||
except OSError:
|
||||
if cls.server.poll() is not None:
|
||||
raise RuntimeError("gateway exited during startup")
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
raise RuntimeError("gateway did not come up")
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.server.terminate()
|
||||
cls.server.wait(timeout=5)
|
||||
cls.tmp.cleanup()
|
||||
|
||||
def post(self, body, stream=False):
|
||||
req = urllib.request.Request(
|
||||
self.base + "/chat/completions", json.dumps(body).encode(),
|
||||
{"Content-Type": "application/json"})
|
||||
resp = urllib.request.urlopen(req, timeout=30)
|
||||
if not stream:
|
||||
return json.loads(resp.read())
|
||||
events = []
|
||||
for raw in resp:
|
||||
line = raw.decode().strip()
|
||||
if line.startswith("data: ") and line != "data: [DONE]":
|
||||
events.append(json.loads(line[6:]))
|
||||
return events
|
||||
|
||||
def test_tool_call_non_stream(self):
|
||||
r = self.post({"model": MODEL_ID, "tools": TOOLS,
|
||||
"messages": [{"role": "user",
|
||||
"content": "What is the weather in Rome?"}]})
|
||||
choice = r["choices"][0]
|
||||
self.assertEqual(choice["finish_reason"], "tool_calls")
|
||||
calls = choice["message"]["tool_calls"]
|
||||
self.assertEqual(len(calls), 1)
|
||||
self.assertEqual(calls[0]["function"]["name"], "get_weather")
|
||||
self.assertEqual(json.loads(calls[0]["function"]["arguments"]), {"city": "Rome"})
|
||||
self.assertNotIn("<tool_call>", choice["message"].get("content") or "")
|
||||
|
||||
def test_tool_call_streamed_markers_suppressed(self):
|
||||
events = self.post({"model": MODEL_ID, "tools": TOOLS, "stream": True,
|
||||
"messages": [{"role": "user",
|
||||
"content": "What is the weather in Milan?"}]},
|
||||
stream=True)
|
||||
deltas = [e["choices"][0]["delta"] for e in events if e["choices"]]
|
||||
text = "".join(d.get("content") or "" for d in deltas)
|
||||
self.assertNotIn("<tool_call>", text)
|
||||
self.assertNotIn("<arg_key>", text)
|
||||
calls = [d["tool_calls"] for d in deltas if d.get("tool_calls")]
|
||||
self.assertEqual(len(calls), 1)
|
||||
self.assertEqual(calls[0][0]["function"]["name"], "get_weather")
|
||||
self.assertEqual(json.loads(calls[0][0]["function"]["arguments"]),
|
||||
{"city": "Milan"})
|
||||
finish = [e["choices"][0]["finish_reason"] for e in events
|
||||
if e["choices"] and e["choices"][0].get("finish_reason")]
|
||||
self.assertEqual(finish, ["tool_calls"])
|
||||
|
||||
def test_tool_result_round_trip(self):
|
||||
r = self.post({"model": MODEL_ID, "tools": TOOLS, "messages": [
|
||||
{"role": "user", "content": "What is the weather in Rome?"},
|
||||
{"role": "assistant", "content": None, "tool_calls": [
|
||||
{"id": "call_x", "type": "function",
|
||||
"function": {"name": "get_weather",
|
||||
"arguments": "{\"city\": \"Rome\"}"}}]},
|
||||
{"role": "tool", "tool_call_id": "call_x",
|
||||
"content": "25 degrees, sunny"}]})
|
||||
choice = r["choices"][0]
|
||||
self.assertEqual(choice["finish_reason"], "stop")
|
||||
self.assertFalse(choice["message"].get("tool_calls"))
|
||||
self.assertIn("25 degrees", choice["message"]["content"])
|
||||
rendered = self.mock_log.read_text().split("\x00")[-2]
|
||||
self.assertIn("<|observation|><tool_response>25 degrees, sunny</tool_response>",
|
||||
rendered)
|
||||
self.assertIn("# Tools", rendered)
|
||||
self.assertIn('"get_weather"', rendered)
|
||||
|
||||
def test_no_tools_plain_text(self):
|
||||
r = self.post({"model": MODEL_ID,
|
||||
"messages": [{"role": "user", "content": "Hi!"}]})
|
||||
choice = r["choices"][0]
|
||||
self.assertEqual(choice["finish_reason"], "stop")
|
||||
self.assertIn("mock engine", choice["message"]["content"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,39 @@
|
||||
#include "../backend_cuda.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
int main(){
|
||||
int dev=0;if(!coli_cuda_init(&dev,1))return 77;
|
||||
constexpr int S=3,H=2,Q=2,R=1,V=2,K=3,D=H*V,O=3,T=3;
|
||||
std::vector<float> w(H*(Q+V)*K),p(O*D),q(S*H*(Q+R));
|
||||
for(size_t i=0;i<w.size();i++)w[i]=((int)(i%11)-5)*.07f;
|
||||
for(size_t i=0;i<p.size();i++)p[i]=((int)(i%7)-3)*.09f;
|
||||
for(size_t i=0;i<q.size();i++)q[i]=((int)(i%13)-6)*.05f;
|
||||
ColiCudaTensor *tw=nullptr,*tp=nullptr;
|
||||
if(!coli_cuda_tensor_upload(&tw,w.data(),nullptr,0,K,H*(Q+V),dev)||
|
||||
!coli_cuda_tensor_upload(&tp,p.data(),nullptr,0,D,O,dev))return 1;
|
||||
int n[S]={1,2,3};std::vector<std::vector<float>> l(S),r(S);
|
||||
const float *lp[S],*rp[S];
|
||||
const void *keys[S];
|
||||
for(int s=0;s<S;s++){
|
||||
l[s].resize(n[s]*K);r[s].resize(n[s]*R);
|
||||
for(size_t i=0;i<l[s].size();i++)l[s][i]=((int)((i+s*3)%9)-4)*.08f;
|
||||
for(size_t i=0;i<r[s].size();i++)r[s][i]=((int)((i+s)%5)-2)*.06f;
|
||||
lp[s]=l[s].data();rp[s]=r[s].data();keys[s]=&l[s];
|
||||
}
|
||||
float got[S*O],ref[S*O],warm[S*O];
|
||||
int first[S]={1,1,1};
|
||||
if(!coli_cuda_attention_project_ragged(tw,tp,warm,q.data(),keys,lp,rp,first,
|
||||
S,H,Q,R,V,K,1,.2f))return 2;
|
||||
if(!coli_cuda_attention_project_ragged(tw,tp,got,q.data(),keys,lp,rp,n,S,H,Q,R,V,K,T,.2f))return 2;
|
||||
for(int s=0;s<S;s++)if(!coli_cuda_attention_project_batch(tw,tp,ref+s*O,
|
||||
q.data()+s*H*(Q+R),lp[s],rp[s],1,H,Q,R,V,K,n[s],.2f))return 3;
|
||||
double e=0,z=0;for(int i=0;i<S*O;i++){
|
||||
double d=got[i]-ref[i];e+=d*d;z+=(double)ref[i]*ref[i];
|
||||
}
|
||||
double rms=std::sqrt(e/(z+1e-30));std::printf("ragged_relative_rms=%.9g\n",rms);
|
||||
coli_cuda_tensor_free(tw);coli_cuda_tensor_free(tp);coli_cuda_shutdown();
|
||||
return rms<1e-6?0:4;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ from resource_plan import (
|
||||
GB,
|
||||
analyze_model,
|
||||
build_plan,
|
||||
cpu_socket_count,
|
||||
environment_for_plan,
|
||||
format_plan,
|
||||
memory_available,
|
||||
@@ -66,15 +67,19 @@ class ResourcePlanTest(unittest.TestCase):
|
||||
# 0 slots/layer. The value must be a sane positive number of bytes.
|
||||
self.assertGreater(memory_available(), 0)
|
||||
|
||||
def test_cpu_socket_count_is_positive(self):
|
||||
self.assertGreaterEqual(cpu_socket_count(), 1)
|
||||
|
||||
def test_builds_bounded_three_tier_plan(self):
|
||||
gpus = [{"index": 0, "name": "test-gpu", "total_bytes": 12 * GB,
|
||||
"free_bytes": 10 * GB}]
|
||||
plan = build_plan(self.model, ram_gb=16, context=32, vram_gb=20,
|
||||
available_memory=32 * GB, available_disk=100 * GB, gpus=gpus,
|
||||
physical_cpus=24)
|
||||
physical_cpus=24, cpu_sockets=2)
|
||||
self.assertEqual(plan["version"], 2)
|
||||
self.assertEqual(plan["policy"]["name"], "quality")
|
||||
self.assertEqual(plan["cpu"]["physical_cores"], 24)
|
||||
self.assertEqual(plan["cpu"]["sockets"], 2)
|
||||
self.assertTrue(plan["policy"]["preserve_quantization"])
|
||||
self.assertFalse(plan["tiers"]["vram"]["requires_host_backing"])
|
||||
self.assertEqual(plan["tiers"]["ram"]["budget_bytes"], 16 * GB)
|
||||
@@ -105,7 +110,7 @@ class ResourcePlanTest(unittest.TestCase):
|
||||
{"index": 1, "name": "b", "total_bytes": 12 * GB, "free_bytes": 10 * GB},
|
||||
]
|
||||
plan = build_plan(self.model, ram_gb=16, available_memory=32 * GB,
|
||||
available_disk=1, gpus=gpus)
|
||||
available_disk=1, gpus=gpus, cpu_sockets=2)
|
||||
env = environment_for_plan(plan, {"RAM_GB": "12", "PIN": "stats.txt",
|
||||
"COLI_GPUS": "1"})
|
||||
self.assertEqual(env["RAM_GB"], "12")
|
||||
@@ -125,6 +130,16 @@ class ResourcePlanTest(unittest.TestCase):
|
||||
"OMP_PROC_BIND": "close"})
|
||||
self.assertEqual(explicit_threads["OMP_NUM_THREADS"], "7")
|
||||
self.assertEqual(explicit_threads["OMP_PROC_BIND"], "close")
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
self.assertEqual(env["COLI_NUMA"], "1")
|
||||
explicit_numa = environment_for_plan(plan, {"COLI_NUMA": "0"})
|
||||
self.assertEqual(explicit_numa["COLI_NUMA"], "0")
|
||||
|
||||
def test_single_socket_plan_does_not_enable_numa(self):
|
||||
plan = build_plan(self.model, available_memory=16 * GB, available_disk=1,
|
||||
gpus=[], physical_cpus=8, cpu_sockets=1)
|
||||
self.assertNotIn("COLI_NUMA", environment_for_plan(plan))
|
||||
def test_cpu_binary_does_not_apply_gpu_tier(self):
|
||||
plan = build_plan(self.model, available_memory=16 * GB, available_disk=1,
|
||||
gpus=[{"index": 0, "name": "a", "total_bytes": 8 * GB,
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/* Regressione #369: un solo logit NaN o +Inf faceva emettere a dist_build/dist_sample
|
||||
* il token 0 PER SEMPRE, in silenzio (il fallback `g_pbuf[i]>0` e' falso su NaN ovunque).
|
||||
* Ora la distribuzione degenere ripiega sull'argmax dei logit FINITI e avvisa una volta.
|
||||
*
|
||||
* Il test verifica: (a) con logit sani il campionamento resta corretto; (b) con NaN/+Inf
|
||||
* iniettato il token scelto e' l'argmax dei FINITI (mai 0 per default), su ogni posizione
|
||||
* del NaN inclusa lo[0]; (c) nessun NaN sopravvive in g_pbuf. */
|
||||
#define main coli_glm_main_unused
|
||||
#include "../glm.c"
|
||||
#undef main
|
||||
#include <stdio.h>
|
||||
|
||||
static uint32_t rs=0x1234abcd; static uint32_t xr(){rs^=rs<<13;rs^=rs>>17;rs^=rs<<5;return rs;}
|
||||
|
||||
static int pbuf_has_nan(int V){ for(int i=0;i<V;i++) if(!isfinite(g_pbuf[i])) return 1; return 0; }
|
||||
|
||||
int main(void){
|
||||
int fail=0, V=2000;
|
||||
float *lo=malloc(V*sizeof(float));
|
||||
g_temp=0.7f; g_nuc=0.90f; /* il path serve di default */
|
||||
|
||||
/* (a) logit sani: il campionato deve avere prob > 0 e nessun NaN nel buffer */
|
||||
for(int i=0;i<V;i++) lo[i]=(float)((int)(xr()%2000)-1000)/100.f;
|
||||
int known=1337; lo[known]=50.f; /* picco netto */
|
||||
dist_build(lo,V);
|
||||
if(pbuf_has_nan(V)){ printf(" FAIL: NaN in g_pbuf con logit sani\n"); fail=1; }
|
||||
if(g_pbuf[known]<=0.f){ printf(" FAIL: il picco ha prob 0\n"); fail=1; }
|
||||
if(!fail) printf(" logit sani: distribuzione valida, picco vivo ok\n");
|
||||
|
||||
/* (b) NaN/+Inf iniettato in varie posizioni; il finito-argmax deve vincere */
|
||||
float bad[3]; bad[0]=NAN; bad[1]=INFINITY; bad[2]=-INFINITY;
|
||||
const char *bn[3]={"NaN","+Inf","-Inf"};
|
||||
for(int b=0;b<3;b++){
|
||||
for(int pos=0;pos<3;pos++){ /* lo[0], meta', ultimo */
|
||||
for(int i=0;i<V;i++) lo[i]=(float)((int)(xr()%400)-200)/100.f;
|
||||
int amax=777; lo[amax]=9.0f; /* massimo FINITO atteso */
|
||||
int at = pos==0?0 : pos==1?V/2 : V-1;
|
||||
if(at==amax) amax=amax+1; /* non sovrapporre */
|
||||
lo[amax]=9.0f;
|
||||
lo[at]=bad[b]; /* veleno */
|
||||
dist_build(lo,V);
|
||||
if(pbuf_has_nan(V)){ printf(" FAIL: NaN sopravvive (%s @ %d)\n",bn[b],at); fail=1; continue; }
|
||||
/* con -Inf il fallback puo' non scattare (max finito resta), ma il buffer
|
||||
* deve restare valido e sommare ~1: con +Inf/NaN scatta il delta su amax */
|
||||
int picked=-1; float pv=-1;
|
||||
for(int i=0;i<V;i++) if(g_pbuf[i]>pv){pv=g_pbuf[i];picked=i;}
|
||||
if(b<2 && picked!=amax){ /* NaN e +Inf: delta esatto su amax */
|
||||
printf(" FAIL: %s @ %d -> picked %d, atteso argmax finito %d\n",bn[b],at,picked,amax); fail=1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!fail) printf(" NaN/+Inf iniettato: argmax dei finiti vince, mai 0/NaN ok\n");
|
||||
|
||||
/* (c) caso estremo: TUTTI non finiti -> non deve crashare, buffer valido */
|
||||
for(int i=0;i<V;i++) lo[i]=NAN;
|
||||
dist_build(lo,V);
|
||||
if(pbuf_has_nan(V)){ printf(" FAIL: tutti-NaN lascia NaN nel buffer\n"); fail=1; }
|
||||
else printf(" tutti non-finiti: nessun crash, buffer valido ok\n");
|
||||
|
||||
printf(fail?"test_sample_nan: FAIL\n":"test_sample_nan: ok\n");
|
||||
return fail;
|
||||
}
|
||||
@@ -17,6 +17,12 @@ PROFILE_RE = re.compile(
|
||||
r"\| attention ([0-9.]+)s .* lm_head ([0-9.]+)s \| other ([0-9.-]+)s"
|
||||
)
|
||||
PROFILE_KEYS = ("disk", "expert_matmul", "attention", "lm_head", "other")
|
||||
P0_RE = re.compile(
|
||||
r"P0-EXEC: routed CPU ([0-9.]+)s / ([0-9.]+) GB/s \(([0-9]+) row\) \| routed GPU critical ([0-9.]+)s \| "
|
||||
r"router ([0-9.]+)s \| residual P2P ([0-9.]+)s / ([0-9]+) hop \| orchestration ([0-9.]+)s"
|
||||
)
|
||||
P0_KEYS = ("routed_cpu", "routed_cpu_gb_s", "routed_cpu_rows", "routed_gpu_critical",
|
||||
"router", "p2p", "p2p_hops", "orchestration")
|
||||
|
||||
|
||||
def parse_output(stdout: str, stderr: str = "") -> tuple[float, list[float]]:
|
||||
@@ -30,11 +36,20 @@ def parse_output(stdout: str, stderr: str = "") -> tuple[float, list[float]]:
|
||||
return float(speed.group(1)), [disk] + [float(value) for value in rest]
|
||||
|
||||
|
||||
def execute(engine: str, env: dict[str, str]) -> tuple[float, list[float]]:
|
||||
def parse_p0(stdout: str) -> list[float]:
|
||||
"""Extract the optional PROF=1 execution-layer breakdown."""
|
||||
row = P0_RE.search(stdout)
|
||||
if not row:
|
||||
raise RuntimeError("benchmark output missing P0-EXEC profile")
|
||||
return [float(value) for value in row.groups()]
|
||||
|
||||
|
||||
def execute(engine: str, env: dict[str, str]) -> tuple[float, list[float], list[float]]:
|
||||
run = subprocess.run(
|
||||
[engine, "4", "4", "4"], env=env, text=True, capture_output=True, check=True
|
||||
)
|
||||
return parse_output(run.stdout, run.stderr)
|
||||
speed, profile = parse_output(run.stdout, run.stderr)
|
||||
return speed, profile, parse_p0(run.stdout)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -63,6 +78,8 @@ def main() -> None:
|
||||
OMP_NUM_THREADS=str(args.threads),
|
||||
OMP_PROC_BIND="spread",
|
||||
OMP_PLACES="cores",
|
||||
DRAFT="0",
|
||||
PROF="1",
|
||||
)
|
||||
|
||||
execute(args.engine, base | {"STATS": str(stats)})
|
||||
@@ -86,13 +103,15 @@ def main() -> None:
|
||||
execute(args.engine, base | extra) # warm-up
|
||||
speeds = {name: [] for name in modes}
|
||||
profiles = {name: [] for name in modes}
|
||||
p0_profiles = {name: [] for name in modes}
|
||||
names = list(modes)
|
||||
for run_index in range(args.runs):
|
||||
order = names[run_index % len(names):] + names[:run_index % len(names)]
|
||||
for name in order:
|
||||
speed, profile = execute(args.engine, base | modes[name])
|
||||
speed, profile, p0 = execute(args.engine, base | modes[name])
|
||||
speeds[name].append(speed)
|
||||
profiles[name].append(profile)
|
||||
p0_profiles[name].append(p0)
|
||||
|
||||
result = {}
|
||||
for name in names:
|
||||
@@ -103,6 +122,10 @@ def main() -> None:
|
||||
key: statistics.median(row[index] for row in profiles[name])
|
||||
for index, key in enumerate(PROFILE_KEYS)
|
||||
},
|
||||
"median_p0": {
|
||||
key: statistics.median(row[index] for row in p0_profiles[name])
|
||||
for index, key in enumerate(P0_KEYS)
|
||||
},
|
||||
}
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
|
||||
@@ -286,6 +286,17 @@ def main():
|
||||
# testa MTP a int4 = acceptance ~0-4% (misurato, issue #8): il draft sbaglia sempre
|
||||
# e la speculazione non parte mai. A int8: 39-59%, 2.2-2.8 token/forward.
|
||||
a.ebits = 8 if (a.mtp or a.indexer) else 4
|
||||
if a.mtp and a.ebits < 8 and a.group_size <= 0:
|
||||
# Non solo lossy: eh_proj ha ~20-30x di asimmetria di scala fra le due meta' di
|
||||
# colonna, quindi l'int4 per-riga (UNA scala per riga) arrotonda a ZERO l'intera
|
||||
# meta' embedding -> il draft non vede il token -> acceptance ~0% (issue #8).
|
||||
# EN: not merely lossy: eh_proj has ~20-30x column-scale asymmetry, so per-row
|
||||
# EN: int4 rounds its ENTIRE embedding half to exact zeros -> the draft cannot
|
||||
# EN: see the input token -> ~0% acceptance (issue #8). A container converted
|
||||
# EN: this way is repairable in place with tools/repair_mtp_int8.py.
|
||||
print(f"WARNING: --mtp with --ebits {a.ebits} and per-row scales ZEROES eh_proj's "
|
||||
"embedding half -> MTP acceptance ~0% (issue #8). Use the default --ebits 8, "
|
||||
"or add --group-size 128 for group-scaled int4.")
|
||||
if a.xbits is None: a.xbits = a.ebits
|
||||
|
||||
# Build per-type bits map. If a type-specific arg is set, use it; otherwise the
|
||||
@@ -299,6 +310,16 @@ def main():
|
||||
if bits_map:
|
||||
print(f"[MIXED] precision map: " + ", ".join(f"{k}={v}bit" for k,v in sorted(bits_map.items())))
|
||||
|
||||
# Il PIANO risolto, PRIMA di toccare qualunque cosa (#383): --mtp/--indexer cambiano il
|
||||
# default di ebits a 8 (testa int4 = acceptance ~0%, issue #8) e il ramo grouped e'
|
||||
# gated su bits<=4 — combinazioni sorprendenti devono mostrarsi al secondo 1 di un job
|
||||
# da ore, non nel size-check dopo. EN: print the RESOLVED plan before doing anything.
|
||||
mode = "MTP head only" if a.mtp else "DSA indexer only" if a.indexer else "main model"
|
||||
grp = f"grouped gs={a.group_size} (fmt=4)" if (a.group_size and a.ebits <= 4) else \
|
||||
(f"PER-ROW (grouped branch needs bits<=4; ebits={a.ebits} disables it)" if a.group_size else "per-row")
|
||||
print(f"[PLAN] mode: {mode} | source: {'local ' + a.indir if a.indir else 'download ' + a.repo} | "
|
||||
f"experts {a.ebits}-bit, embed/lm_head {a.io_bits}-bit, x {a.xbits}-bit | {grp}")
|
||||
|
||||
if a.selftest_nvfp4:
|
||||
import torch
|
||||
# 1) LUT e2m1: i 16 codici devono decodificare esattamente ai valori attesi.
|
||||
@@ -379,14 +400,99 @@ def main():
|
||||
if a.indir: # conversione locale (test)
|
||||
shards = sorted(glob.glob(os.path.join(a.indir, "*.safetensors")))
|
||||
from safetensors.numpy import save_file
|
||||
# #383: se l'indice c'e', i passaggi --mtp/--indexer convertono SOLO gli shard
|
||||
# che contengono i tensori richiesti (3 invece di scandire tutti i 141 — ogni
|
||||
# scansione a vuoto apre comunque uno shard da 5 GB). Senza indice: scansione
|
||||
# completa come prima.
|
||||
# EN: #383: when the index is present, the --mtp/--indexer passes convert ONLY
|
||||
# the shards that hold the requested tensors (3 instead of scanning all 141 —
|
||||
# every empty scan still opens a 5 GB shard). Without the index: full scan as
|
||||
# before.
|
||||
if a.mtp or a.indexer:
|
||||
idxp = os.path.join(a.indir, "model.safetensors.index.json")
|
||||
if os.path.exists(idxp):
|
||||
wmap = json.load(open(idxp))["weight_map"]
|
||||
if a.mtp:
|
||||
want = {v for k, v in wmap.items() if k.startswith(f"model.layers.{a.n_layers}.")}
|
||||
else:
|
||||
want = {v for k, v in wmap.items() if "indexer" in k and 0 <= layer_idx(k) < a.n_layers}
|
||||
keep = [sp for sp in shards if os.path.basename(sp) in want]
|
||||
print(f"[PLAN] index: {len(keep)}/{len(shards)} local shard(s) hold the requested tensors")
|
||||
shards = keep
|
||||
# BUG #355: questo ramo ignorava --mtp/--indexer. Con --mtp scriveva
|
||||
# out-NNNNN (gli STESSI nomi di una conversione normale) in ebits=8 e
|
||||
# keep_mtp=False -> il "secondo passaggio MTP" nella stessa outdir
|
||||
# SOVRASCRIVEVA il container gia' finito con una riconversione int8
|
||||
# completa, in silenzio (137/141 shard distrutti prima di accorgersene).
|
||||
# Ora il ramo locale rispecchia il download path: prefisso corretto,
|
||||
# flag passate, shard vuoti saltati.
|
||||
prefix = "out-mtp-" if a.mtp else "out-idx-" if a.indexer else "out-"
|
||||
# RIPRESA (#383): i nomi out-NNNNN contano gli shard EMESSI, non l'indice di
|
||||
# input (gli shard senza tensori rilevanti non producono file), quindi "il
|
||||
# file esiste" non basta per saltare il lavoro gia' fatto. Un manifest
|
||||
# sidecar ricorda input -> output (o "vuoto") e con quali parametri: la
|
||||
# ripresa salta solo cio' che combacia, e parametri diversi sulla stessa
|
||||
# outdir vengono rifiutati invece di mescolare container (il modo #355).
|
||||
# EN: RESUME (#383): out-NNNNN names count EMITTED shards, not the input
|
||||
# EN: index (shards with no relevant tensors emit no file), so "the file
|
||||
# EN: exists" is not enough to skip completed work. A sidecar manifest
|
||||
# EN: records input -> output (or "empty") plus the conversion parameters:
|
||||
# EN: resume skips only what matches, and different parameters on the same
|
||||
# EN: outdir are refused instead of mixing containers (the #355 failure mode).
|
||||
params = {"ebits": a.ebits, "io_bits": a.io_bits, "xbits": a.xbits,
|
||||
"group_size": a.group_size, "n_layers": a.n_layers, "bits_map": bits_map}
|
||||
prog_path = os.path.join(a.outdir, f".{prefix}progress.json")
|
||||
prog = {}
|
||||
if os.path.exists(prog_path):
|
||||
try: prog = json.loads(open(prog_path).read())
|
||||
except (OSError, ValueError): prog = {}
|
||||
if prog and prog.get("params") != params:
|
||||
print(f"ERROR: {prog_path} records a conversion with {prog.get('params')};\n"
|
||||
f" this run uses {params}. Refusing to mix conversions in the same "
|
||||
f"outdir — use a fresh --outdir (or delete the manifest and the "
|
||||
f"{prefix}*.safetensors shards to redo).")
|
||||
return
|
||||
done = prog.setdefault("shards", {}); prog["params"] = params
|
||||
n = 0; fresh = 0; skipped = 0
|
||||
for i, sp in enumerate(shards):
|
||||
out = {}; convert_shard(sp, out, a.n_layers, a.ebits, a.io_bits, a.xbits, group_size=a.group_size, bits_map=bits_map)
|
||||
save_file(out, os.path.join(a.outdir, f"out-{i:05d}.safetensors"))
|
||||
# copia config + tokenizer
|
||||
for fn in ["config.json"]:
|
||||
src = os.path.join(a.indir, fn)
|
||||
if os.path.exists(src): shutil.copy(src, a.outdir)
|
||||
print(f"converted {len(shards)} shards -> {a.outdir}")
|
||||
key = os.path.basename(sp)
|
||||
prev = done.get(key) # None = mai visto; "" = visto, vuoto; nome = emesso
|
||||
if prev is not None and (prev == "" or os.path.exists(os.path.join(a.outdir, prev))):
|
||||
if prev: n += 1
|
||||
skipped += 1
|
||||
continue
|
||||
out = {}
|
||||
convert_shard(sp, out, a.n_layers, a.ebits, a.io_bits, a.xbits,
|
||||
keep_mtp=a.mtp, keep_idx=a.indexer,
|
||||
group_size=a.group_size, bits_map=bits_map)
|
||||
if not out: # shard senza MTP/idx: niente file (come il download path)
|
||||
done[key] = ""
|
||||
else:
|
||||
name = f"{prefix}{n:05d}.safetensors"
|
||||
save_file(out, os.path.join(a.outdir, name))
|
||||
done[key] = name; n += 1; fresh += 1
|
||||
tmp_prog = prog_path + ".tmp" # scrittura atomica: una ripresa non vede mai un manifest mezzo scritto
|
||||
with open(tmp_prog, "w") as f: json.dump(prog, f, indent=1) # EN: atomic write: a resume never sees a half-written manifest
|
||||
os.replace(tmp_prog, prog_path)
|
||||
if skipped: print(f"[RESUME] {skipped} shard(s) already done in {a.outdir}, skipped")
|
||||
# metadati per la conversione principale: gli stessi quattro file del download
|
||||
# path — senza tokenizer.json chat/serve non partono. I passaggi mtp/idx vanno
|
||||
# nella stessa outdir di un container gia' completo di metadati.
|
||||
# EN: metadata for the main pass: the same four files as the download path —
|
||||
# EN: chat/serve won't start without tokenizer.json. The mtp/idx passes target
|
||||
# EN: an outdir whose container already has its metadata.
|
||||
if not a.mtp and not a.indexer:
|
||||
copied, missing = [], []
|
||||
for fn in ["config.json", "tokenizer.json", "tokenizer_config.json", "generation_config.json"]:
|
||||
src = os.path.join(a.indir, fn)
|
||||
if os.path.exists(src): shutil.copy(src, a.outdir); copied.append(fn)
|
||||
else: missing.append(fn)
|
||||
print(f"[META] copied from {a.indir}: {', '.join(copied) if copied else 'nothing'}")
|
||||
if missing:
|
||||
print(f"[META] WARNING: not found in {a.indir}: {', '.join(missing)}"
|
||||
+ (" — chat/serve need tokenizer.json" if "tokenizer.json" in missing else ""))
|
||||
tag = "MTP" if a.mtp else "indexer" if a.indexer else "main"
|
||||
print(f"converted {fresh} {tag} shard(s), {n} in container -> {a.outdir} ({prefix}NNNNN)")
|
||||
return
|
||||
|
||||
# reale: scarica shard per shard, converte, cancella
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
"""Repair an existing colibri int4 container whose MTP head was quantized at int4.
|
||||
|
||||
WHY THIS EXISTS
|
||||
`model.layers.<N>.eh_proj.weight` [D, 2D] multiplies the MTP concat
|
||||
[embedding_norm ; hidden_norm], and its two column halves differ in scale by
|
||||
~20-30x per row (embedding-half absmax ~0.05, hidden-half ~1.5 on GLM-5.2).
|
||||
Per-row int4 uses ONE scale (= absmax/7) per row, so every embedding-half
|
||||
weight lands below half a quantization step and np.rint rounds the ENTIRE
|
||||
embedding half to exact zeros (packed bytes 0x88). The MTP head then drafts
|
||||
garbage: acceptance ~0% (issue #8 measured 0-4% at int4; 39-59% at int8 —
|
||||
which is why `convert_fp8_to_int4.py --mtp` defaults to --ebits 8).
|
||||
|
||||
A container converted (or downloaded) with an int4 MTP head does not need a
|
||||
full re-conversion: this script re-downloads ONLY the affected dense tensors
|
||||
(~355 MB of HTTP range reads against the FP8 source repo), requantizes them
|
||||
at int8 with the converter's exact math, and patches the local shards in
|
||||
place. Originals are kept beside as *.bak-int4.
|
||||
|
||||
WHAT IT TOUCHES
|
||||
The MTP layer's dense tensors only (eh_proj, q_a/q_b/kv_a/kv_b/o_proj,
|
||||
shared_experts.*): the ones that stream into RAM once and stay resident.
|
||||
Routed experts (model.layers.<N>.mlp.experts.*) are NOT touched — they are
|
||||
statistically like the main layers' experts and int4 is acceptable there.
|
||||
The engine auto-detects int8 vs int4 by blob size (qt_from_disk), so no
|
||||
engine or config change is needed. Cost: ~+133 MB on disk / resident RAM.
|
||||
|
||||
USAGE
|
||||
python3 tools/repair_mtp_int8.py --snap /path/to/glm52_i4 # repair
|
||||
python3 tools/repair_mtp_int8.py --snap /path/to/glm52_i4 --dry-run # inspect only
|
||||
|
||||
--source-repo defaults to zai-org/GLM-5.2-FP8 (the checkpoint the public
|
||||
int4 containers were converted from). Requires numpy and network access;
|
||||
no torch, no HF token (public repo, anonymous range reads).
|
||||
"""
|
||||
import argparse, glob, json, os, ssl, struct, sys, urllib.request
|
||||
import numpy as np
|
||||
|
||||
# macOS python.org builds ship no CA bundle: use certifi when available (Linux
|
||||
# system Pythons generally have working system certs and skip this).
|
||||
try:
|
||||
import certifi
|
||||
_SSL_CTX = ssl.create_default_context(cafile=certifi.where())
|
||||
except ImportError:
|
||||
_SSL_CTX = ssl.create_default_context()
|
||||
|
||||
|
||||
# ---------- HTTP range reads against the source repo ----------
|
||||
def http_range(url, start, length, tries=5):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "colibri-mtp-repair",
|
||||
"Range": f"bytes={start}-{start+length-1}"})
|
||||
for attempt in range(tries):
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30, context=_SSL_CTX) as r:
|
||||
data = r.read()
|
||||
if len(data) == length:
|
||||
return data
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception as ex:
|
||||
if attempt == tries - 1:
|
||||
raise RuntimeError(f"range read failed for {url}: {ex}")
|
||||
raise RuntimeError(f"short range read for {url}")
|
||||
|
||||
|
||||
class SourceRepo:
|
||||
def __init__(self, repo, revision="main"):
|
||||
self.base = f"https://huggingface.co/{repo}/resolve/{revision}/"
|
||||
with urllib.request.urlopen(self.base + "model.safetensors.index.json", timeout=30, context=_SSL_CTX) as r:
|
||||
self.wmap = json.loads(r.read())["weight_map"]
|
||||
self._hdr = {}
|
||||
|
||||
def _shard_header(self, shard):
|
||||
if shard not in self._hdr:
|
||||
n = struct.unpack("<Q", http_range(self.base + shard, 0, 8))[0]
|
||||
self._hdr[shard] = (json.loads(http_range(self.base + shard, 8, n)), 8 + n)
|
||||
return self._hdr[shard]
|
||||
|
||||
def meta(self, name):
|
||||
shard = self.wmap.get(name)
|
||||
if not shard:
|
||||
return None
|
||||
hdr, _ = self._shard_header(shard)
|
||||
return hdr[name]
|
||||
|
||||
def fetch_f32(self, name):
|
||||
"""Download one tensor and dequantize to f32 [O, I] (BF16 or FP8+block scales)."""
|
||||
shard = self.wmap[name]
|
||||
hdr, base = self._shard_header(shard)
|
||||
m = hdr[name]
|
||||
o0, o1 = m["data_offsets"]
|
||||
raw = http_range(self.base + shard, base + o0, o1 - o0)
|
||||
if m["dtype"] == "BF16":
|
||||
u = np.frombuffer(raw, dtype=np.uint16).astype(np.uint32) << 16
|
||||
return u.view(np.float32).reshape(m["shape"]).astype(np.float32)
|
||||
if m["dtype"] == "F8_E4M3":
|
||||
b = np.frombuffer(raw, dtype=np.uint8).astype(np.uint16)
|
||||
sign = np.where(b & 0x80, -1.0, 1.0)
|
||||
e = (b >> 3) & 0xF
|
||||
mant = (b & 7).astype(np.float64)
|
||||
v = (sign * np.where(e > 0, (1 + mant / 8) * np.exp2(e.astype(np.float64) - 7),
|
||||
mant / 8 * np.exp2(-6.0))).reshape(m["shape"])
|
||||
sn = name + "_scale_inv"
|
||||
sshard = self.wmap[sn]
|
||||
shdr, sbase = self._shard_header(sshard)
|
||||
sm = shdr[sn]
|
||||
so0, so1 = sm["data_offsets"]
|
||||
sc = np.frombuffer(http_range(self.base + sshard, sbase + so0, so1 - so0),
|
||||
dtype=np.float32).reshape(sm["shape"])
|
||||
O, I = m["shape"]
|
||||
scf = np.repeat(np.repeat(sc, 128, axis=0)[:O], 128, axis=1)[:, :I]
|
||||
return (v * scf).astype(np.float32)
|
||||
raise ValueError(f"{name}: unsupported source dtype {m['dtype']}")
|
||||
|
||||
|
||||
# ---------- quantization: identical to convert_fp8_to_int4.quant_int8 ----------
|
||||
def quant_int8(w):
|
||||
amax = np.abs(w).max(axis=1, keepdims=True)
|
||||
s = np.maximum(amax / 127, 1e-8)
|
||||
q = np.clip(np.rint(w / s), -128, 127).astype(np.int8)
|
||||
return q.reshape(-1).view(np.uint8).copy(), s[:, 0].astype(np.float32)
|
||||
|
||||
|
||||
# ---------- local safetensors IO (no deps; preserves byte-identity of untouched tensors) ----------
|
||||
def read_shard(path):
|
||||
with open(path, "rb") as fh:
|
||||
n = struct.unpack("<Q", fh.read(8))[0]
|
||||
hdr = json.loads(fh.read(n))
|
||||
base = 8 + n
|
||||
order = sorted(((k, v) for k, v in hdr.items() if k != "__metadata__"),
|
||||
key=lambda kv: kv[1]["data_offsets"][0])
|
||||
out = {}
|
||||
for k, v in order:
|
||||
fh.seek(base + v["data_offsets"][0])
|
||||
out[k] = (v["dtype"], v["shape"], fh.read(v["data_offsets"][1] - v["data_offsets"][0]))
|
||||
return out, hdr.get("__metadata__")
|
||||
|
||||
|
||||
def write_shard(path, tensors, meta):
|
||||
hdr = {}
|
||||
off = 0
|
||||
for k, (dt, shape, raw) in tensors.items():
|
||||
hdr[k] = {"dtype": dt, "shape": shape, "data_offsets": [off, off + len(raw)]}
|
||||
off += len(raw)
|
||||
if meta:
|
||||
hdr["__metadata__"] = meta
|
||||
hj = json.dumps(hdr).encode()
|
||||
hj += b" " * ((8 - len(hj) % 8) % 8)
|
||||
with open(path, "wb") as fh:
|
||||
fh.write(struct.pack("<Q", len(hj)))
|
||||
fh.write(hj)
|
||||
for _, (_, _, raw) in tensors.items():
|
||||
fh.write(raw)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--snap", required=True, help="local colibri int4 container directory")
|
||||
ap.add_argument("--source-repo", default="zai-org/GLM-5.2-FP8")
|
||||
ap.add_argument("--revision", default="main")
|
||||
ap.add_argument("--dry-run", action="store_true", help="report what would change, touch nothing")
|
||||
a = ap.parse_args()
|
||||
|
||||
cfg = json.load(open(os.path.join(a.snap, "config.json")))
|
||||
L = cfg["num_hidden_layers"]
|
||||
pref = f"model.layers.{L}."
|
||||
src = SourceRepo(a.source_repo, a.revision)
|
||||
|
||||
# find the MTP layer's dense int4 tensors across the local shards
|
||||
plan = {} # shard path -> [tensor names to repair]
|
||||
already_ok, skipped_experts = [], 0
|
||||
for f in sorted(glob.glob(os.path.join(a.snap, "*.safetensors"))):
|
||||
with open(f, "rb") as fh:
|
||||
n = struct.unpack("<Q", fh.read(8))[0]
|
||||
hdr = json.loads(fh.read(n))
|
||||
for name, v in hdr.items():
|
||||
if not name.startswith(pref) or name.endswith(".qs") or v.get("dtype") != "U8":
|
||||
continue
|
||||
if ".mlp.experts." in name:
|
||||
skipped_experts += 1
|
||||
continue
|
||||
m = src.meta(name)
|
||||
if m is None:
|
||||
print(f" ?? {name}: not in source repo, skipping")
|
||||
continue
|
||||
O, I = m["shape"]
|
||||
nb = v["data_offsets"][1] - v["data_offsets"][0]
|
||||
if nb == O * I:
|
||||
already_ok.append(name)
|
||||
elif nb == O * ((I + 1) // 2):
|
||||
plan.setdefault(f, []).append((name, O, I))
|
||||
else:
|
||||
print(f" ?? {name}: unexpected blob size {nb} for shape {O}x{I}, skipping")
|
||||
|
||||
n_fix = sum(len(v) for v in plan.values())
|
||||
print(f"MTP layer {L}: {n_fix} dense tensor(s) at per-row int4 to repair, "
|
||||
f"{len(already_ok)} already int8, {skipped_experts} routed-expert tensors left as-is")
|
||||
if not n_fix:
|
||||
print("nothing to do."); return
|
||||
if a.dry_run:
|
||||
for f, names in plan.items():
|
||||
for nm, O, I in names:
|
||||
print(f" would repair {nm} [{O},{I}] in {os.path.basename(f)}")
|
||||
return
|
||||
|
||||
for f, names in plan.items():
|
||||
print(f"patching {os.path.basename(f)} ({len(names)} tensors)")
|
||||
tensors, meta = read_shard(f)
|
||||
for nm, O, I in names:
|
||||
print(f" {nm}: fetching source + requantizing at int8...", flush=True)
|
||||
w = src.fetch_f32(nm)
|
||||
assert w.shape == (O, I), f"{nm}: source shape {w.shape} != container {O}x{I}"
|
||||
q, s = quant_int8(w)
|
||||
tensors[nm] = ("U8", [len(q)], q.tobytes())
|
||||
tensors[nm + ".qs"] = ("F32", [O], s.tobytes())
|
||||
# verify first row against the source
|
||||
loc = q[:I].view(np.int8).astype(np.float64) * s[0]
|
||||
ref = w[0].astype(np.float64)
|
||||
cos = float(ref @ loc / (np.linalg.norm(ref) * np.linalg.norm(loc) + 1e-30))
|
||||
print(f" row-0 cosine vs source: {cos:.5f}")
|
||||
bak = f + ".bak-int4"
|
||||
write_shard(f + ".new", tensors, meta)
|
||||
os.replace(f, bak)
|
||||
os.replace(f + ".new", f)
|
||||
print(f" saved; original kept as {os.path.basename(bak)}")
|
||||
print("done. Re-run with --dry-run to confirm (should report 'already int8').")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Single source of truth for the colibrì version number."""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
+1
-1
@@ -43,7 +43,7 @@ Format: `VAR` — default — effect.
|
||||
| `URING` | `0` (off) | Linux-only queued expert I/O. `URING=1` implies `PIPE=1`, forces cold reads through io-wq (`IOSQE_ASYNC`), replaces blocking loader pthreads and spin waits with batched SQEs/CQEs, and batches `PILOT_REAL` loads on a separate ring. Use `DIRECT=1` for cold NVMe to avoid page-cache copy/readahead limits. Fails clearly if the kernel denies io_uring; incompatible with `COLI_MMAP=1`. |
|
||||
| `DIRECT` | `0` (off) | Use `O_DIRECT`/unbuffered reads for expert slabs. Helps sustained NVMe; keeps the zero-copy GPU path. |
|
||||
| `COLI_NO_OMP_TUNE` | off | **Kill-switch** for the OpenMP hot-thread tuning (`OMP_WAIT_POLICY=active` spin + proc-bind). Set `=1` when the CPU is mostly waiting on the GPU (Metal) so spin doesn't steal the shared power budget. |
|
||||
| `COLI_NUMA` | off (Linux only) | `COLI_NUMA=1` interleaves expert slabs across NUMA nodes via `mbind` (raw syscall, no libnuma). Helps multi-socket hosts (+7–40% expert matmul); silent no-op on single-node or non-Linux. |
|
||||
| `COLI_NUMA` | auto in generated plans on multi-socket Linux; otherwise off | `COLI_NUMA=1` selectively interleaves large expert and dense slabs across NUMA nodes via `mbind` (raw syscall, no libnuma). Helps multi-socket hosts (+7–40% expert matmul); silent no-op on single-node or non-Linux. Explicit `COLI_NUMA=0` overrides the generated plan. |
|
||||
| `MLOCK` | `-1` (auto: on for macOS) | Wire the streamed expert cache into physical RAM (`mlock`) to dodge the memory compressor. `0` off, `1` force. |
|
||||
| `CAP_RAISE` | `1` (on) | Let the engine raise the expert-cache cap above `topk` when RAM allows (bigger batches). `0` fixes the cap. |
|
||||
| `PREFETCH` | `0` | Prefetch depth for streamed experts. |
|
||||
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
# Windows 11 native install — a complete walkthrough (no WSL)
|
||||
|
||||
A start-to-finish, reproducible path from a fresh Windows 11 machine to GLM-5.2 generating tokens, with the GPU tier. Every step and every failure mode below was hit and verified on real hardware: Core Ultra 9 285K (AVX-VNNI) / RTX 5080 (sm_120) / 128 GB RAM / Windows 11 24H2 (issue #306). Steps are ordered so the long downloads run while you build.
|
||||
|
||||
## 0. What you need
|
||||
|
||||
| Piece | Why | Get it |
|
||||
|---|---|---|
|
||||
| git, Python 3 | clone + `coli` launcher | winget / python.org |
|
||||
| MinGW-w64 gcc + make | builds the engine (MSVC can't) | `scoop install mingw-winlibs`, MSYS2, or portable **w64devkit** (no admin, unzip and go) |
|
||||
| CUDA Toolkit ≥ 12.8 | GPU tier; ≥12.8 required for Blackwell/sm_120 | `winget install Nvidia.CUDA` |
|
||||
| MSVC Build Tools (C++ workload) | nvcc's host compiler for the CUDA DLL | `winget install Microsoft.VisualStudio.2022.BuildTools` + "Desktop development with C++" |
|
||||
| ~400 GB free on a local NVMe | the int4 model (~370–384 GB) | NTFS is fine; **never** a network mount |
|
||||
|
||||
RAM: 16 GB minimum, more = bigger expert cache = faster. The build itself needs none of the CUDA/MSVC pieces — do the CPU build first, add the GPU tier later.
|
||||
|
||||
## 1. Start the model download first (it's the long pole)
|
||||
|
||||
```powershell
|
||||
python -m pip install -U "huggingface_hub[hf_transfer]"
|
||||
$env:HF_HUB_ENABLE_HF_TRANSFER = "1"
|
||||
hf download <model-repo> --local-dir D:\glm52_i4
|
||||
```
|
||||
|
||||
Use the container recommended in the README (with **int8 MTP heads** — int4 heads silently give 0% draft acceptance). The download is resumable: if it stops, rerun the same command. Expect hours; everything below fits inside them.
|
||||
|
||||
## 2. Build the engine (CPU)
|
||||
|
||||
From a normal PowerShell, in the repo's `c\` directory:
|
||||
|
||||
```powershell
|
||||
make glm.exe ARCH=native # ARCH=native unlocks AVX-VNNI on Alder Lake+/Arrow Lake
|
||||
make iobench.exe # disk benchmark, useful before committing to the download
|
||||
```
|
||||
|
||||
Warnings about `#pragma comment` and unused variables are normal (MSVC-isms gcc ignores). The engine banner should print `idot: avx-vnni` on VNNI-capable CPUs — if it says avx2, you built without `ARCH=native`.
|
||||
|
||||
### ⚠️ Smart App Control will block your fresh binary
|
||||
|
||||
On Windows 11 machines with **Smart App Control** enforced (`VerifiedAndReputablePolicyState = 1`), running your self-compiled `glm.exe` fails with:
|
||||
|
||||
```
|
||||
Program 'glm.exe' failed to run: An Application Control policy has blocked this file
|
||||
```
|
||||
|
||||
This is not Defender and not Mark-of-the-Web — SAC blocks *all* unsigned, unknown binaries, which includes anything you compile yourself. **Fix:** Windows Security → App & browser control → Smart App Control settings → **Off**, then **reboot** (the policy only reloads on restart). Note SAC is one-way: re-enabling later requires resetting Windows. If the settings page is missing, the registry equivalent is setting `HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy\VerifiedAndReputablePolicyState` to `0` (admin PowerShell), then rebooting. Check your current state before touching anything:
|
||||
|
||||
```powershell
|
||||
(Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy").VerifiedAndReputablePolicyState
|
||||
# 0 = off, 1 = enforced, 2 = evaluation
|
||||
```
|
||||
|
||||
## 3. Build the CUDA DLL (GPU tier)
|
||||
|
||||
nvcc needs MSVC as host compiler, so this one step must run from a shell with the MSVC environment: open **"x64 Native Tools Command Prompt for VS 2022"** from the Start menu (plain PowerShell will fail the `cl` check). Then:
|
||||
|
||||
```cmd
|
||||
make cuda-dll CUDA_ARCH=sm_120 # match your GPU: sm_120 Blackwell, sm_89 Ada, ...
|
||||
make glm.exe CUDA_DLL=1 ARCH=native # relink host with the runtime loader
|
||||
```
|
||||
|
||||
Two pitfalls, both fixed on current `dev` (#314) but worth knowing on older checkouts:
|
||||
|
||||
- **Spaces in `CUDA_HOME`** (`C:\Program Files\...`) used to break the recipe → fixed; nvcc now comes from PATH and `"$(NVCC)"` is quoted.
|
||||
- **`make glm.exe CUDA_DLL=1` after a CPU-only build** used to report `up to date` and silently keep the CPU-only binary (GPU tier never engages, no error). Current `dev` has a build-config stamp that forces the relink. On older trees: delete `glm.exe` first.
|
||||
|
||||
Sanity check: first GPU run should print `[CUDA] device 0: <your GPU>, ... sm_XX` and `[CUDA] mode: routed experts + resident dense tensors`.
|
||||
|
||||
## 4. First run
|
||||
|
||||
```powershell
|
||||
cd <repo>\c
|
||||
$env:OMP_NUM_THREADS = "<physical cores>"
|
||||
python coli run "Explain what a mixture-of-experts model is." --model D:\glm52_i4 --ngen 48
|
||||
```
|
||||
|
||||
The first run is cold — expect the profile to be dominated by `expert-disk` while the cache warms; hit rate climbs run over run. GPU tier on top:
|
||||
|
||||
```powershell
|
||||
$env:COLI_CUDA="1"; $env:COLI_GPU="0"; $env:CUDA_DENSE="1"; $env:CUDA_EXPERT_GB="4"
|
||||
python coli run "..." --model D:\glm52_i4 --ngen 64
|
||||
```
|
||||
|
||||
Size `CUDA_EXPERT_GB` so dense (~10 GB) + experts + working set stays under your VRAM. Note MTP speculation is off by default under CUDA (#293, float-accumulation divergence between draft and verify) — `COLI_CUDA_MTP=1` opts back in.
|
||||
|
||||
## 5. Reference numbers from this walkthrough's hardware
|
||||
|
||||
285K / RTX 5080 / 128 GB / NVMe at 5.85 GB/s random-read (19 MB blocks, `iobench`): 0.26 tok/s cold CPU → 0.30 warm CPU (MTP 2.2–2.3 tok/forward) → 0.42 tok/s GPU tier + auto-pin, expert hit 66%, ~65% of wall time in expert-disk. Disk-bound is the expected shape at ~25% expert residency — a faster disk and more RAM move the floor, the GPU moves the compute.
|
||||
|
||||
## Quick failure index
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| `An Application Control policy has blocked this file` | Smart App Control | §2 — turn SAC off + **reboot** |
|
||||
| `cuda-dll ... Error 1` immediately | old tree: spaced CUDA_HOME / MSVC rejects `-Wextra` | update to current `dev` (#314) |
|
||||
| `glm.exe is up to date` but GPU never engages | old tree: stale CPU-only binary | update to `dev`, or delete `glm.exe` and rebuild |
|
||||
| `cl.exe (MSVC) not in PATH` | built from plain PowerShell | use the x64 Native Tools prompt |
|
||||
| `nvcc fatal: unsupported gpu architecture 'sm_120'` | CUDA < 12.8 | install CUDA 12.8+ |
|
||||
| MTP `0% (0/0)` on CPU path | int4 MTP heads in the container | use the int8-MTP container |
|
||||
| MTP `draft=0` under CUDA | intended default since #293 | `COLI_CUDA_MTP=1` to opt in |
|
||||
+76
@@ -49,6 +49,82 @@ errors before streaming headers are sent. `GET /health` exposes
|
||||
active/queued/completed/rejected counters, and successful generation responses
|
||||
include `x-colibri-queue-wait-ms`.
|
||||
|
||||
## Connect a coding CLI or editor
|
||||
|
||||
The API is OpenAI-compatible, so most coding CLIs and editor extensions work by
|
||||
pointing them at Colibri as an *OpenAI-compatible* provider. Three settings:
|
||||
|
||||
- **Base URL** — `http://localhost:8000/v1`
|
||||
- **Model** — `glm-5.2-colibri` (or whatever you pass to `--model-id`)
|
||||
- **API key** — any non-empty string, e.g. `local`
|
||||
|
||||
Colibri needs **no** API key by default, but many clients refuse to start without
|
||||
one — give them any dummy value. The key is only enforced if you set `COLI_API_KEY`.
|
||||
|
||||
Smoke-test the endpoint first (no key needed unless you set one):
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8000/v1/chat/completions \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"model":"glm-5.2-colibri","messages":[{"role":"user","content":"hi"}]}'
|
||||
```
|
||||
|
||||
**aider**
|
||||
|
||||
```bash
|
||||
export OPENAI_API_BASE=http://localhost:8000/v1
|
||||
export OPENAI_API_KEY=local
|
||||
aider --model openai/glm-5.2-colibri # the openai/ prefix routes to OPENAI_API_BASE
|
||||
```
|
||||
|
||||
**crush** — add a provider to `crush.json` (`~/.config/crush/crush.json`, or
|
||||
`%USERPROFILE%\AppData\Local\crush\crush.json` on Windows):
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://charm.land/crush.json",
|
||||
"providers": {
|
||||
"colibri": {
|
||||
"name": "Colibri",
|
||||
"type": "openai-compat",
|
||||
"base_url": "http://localhost:8000/v1/",
|
||||
"api_key": "local",
|
||||
"models": [
|
||||
{ "name": "GLM-5.2 (Colibri)", "id": "glm-5.2-colibri",
|
||||
"context_window": 131072, "default_max_tokens": 1024 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `"api_key": "local"` dummy is what satisfies clients that demand a key.
|
||||
`context_window` is only the client's budget display — set it to whatever your
|
||||
KV configuration actually allows.
|
||||
|
||||
**Continue, Cline / Roo, `llm`, the OpenAI SDKs, …** — set the provider's base
|
||||
URL to `http://localhost:8000/v1`, the model to `glm-5.2-colibri`, and any dummy
|
||||
key (`OPENAI_API_KEY` / `OPENAI_BASE_URL` for env-based tools).
|
||||
|
||||
> **Set your expectations before connecting an agentic CLI.** Two costs dominate,
|
||||
> and the first one is invisible until you know it's there:
|
||||
>
|
||||
> 1. **Prefill.** Coding agents (crush, aider in repo-map mode, Cline, …) send a
|
||||
> large system prompt plus tool definitions — often 10–20k tokens — *before
|
||||
> your first word*. Prefill on the CPU-streaming path runs at a few tokens per
|
||||
> second (it is attention-bound, see #153), so a 15k-token agent preamble is
|
||||
> **an hour of silent "thinking" before the first output token**. The client
|
||||
> looks hung; it isn't. Smoke-test with the tiny `curl` above first — if that
|
||||
> answers in about a minute, the pipeline works and what you're paying for is
|
||||
> prompt size.
|
||||
> 2. **Decode.** Roughly 1 tok/s for a large model, so multi-turn agent loops
|
||||
> (which re-pay the growing context every turn) compound the cost.
|
||||
>
|
||||
> Practical guidance: single surgical asks with a short context work; iterative
|
||||
> agent sessions against a disk-streaming 744B model do not resemble a hosted
|
||||
> API and mostly won't be worth the wait. If your client lets you trim or disable
|
||||
> its system preamble and tool catalog, do it.
|
||||
|
||||
## Isolated KV contexts
|
||||
|
||||
`coli serve --kv-slots N` allocates up to 16 independent sequence contexts.
|
||||
|
||||
+6
-2
@@ -116,8 +116,12 @@ can match an RTX 5090 on expert matmul ([#101](https://github.com/JustVugg/colib
|
||||
so **the GPU tier earns its VRAM only when the CPU is the weak link**. On
|
||||
multi-socket hosts, NUMA placement is a further lever: interleaving the resident
|
||||
weights across nodes measured **+13% (2-socket) and +40% (4-socket CPU-only)**
|
||||
([#82](https://github.com/JustVugg/colibri/issues/82)) — but never blanket-interleave
|
||||
a GPU host (measured 10× regression via the DMA staging pages).
|
||||
([#82](https://github.com/JustVugg/colibri/issues/82)). On a 2-socket Xeon Silver
|
||||
4510 host with 6× RTX 5090, selective `COLI_NUMA=1` raised effective CPU-expert
|
||||
bandwidth from **42.42 to 58.26/65.89 GB/s** and greedy decode from **7.66 to
|
||||
9.02/9.17 tok/s** (64 tokens, `TEMP=0 DRAFT=0`, byte-identical output). Do not
|
||||
blanket-interleave a GPU host: it also spreads DMA staging pages and has measured
|
||||
up to a 10× regression; generated plans enable only the selective slab policy.
|
||||
|
||||
## Quality benchmark
|
||||
|
||||
|
||||
+99
-17
@@ -1,25 +1,107 @@
|
||||
# Windows 11 (native, no WSL)
|
||||
# Windows 11 native install — a complete walkthrough (no WSL)
|
||||
|
||||
colibrì builds and runs natively on Windows 11 x86-64 with MinGW-w64. The port
|
||||
adds a `_WIN32` compatibility layer in `c/compat.h` that maps POSIX I/O to the
|
||||
Windows API (pread → ReadFile+OVERLAPPED, posix_fadvise no-op, aligned
|
||||
allocation, MoveFileEx rename, GlobalMemoryStatusEx RAM detection). All platform
|
||||
differences stay in `compat.h`; the engine source is unchanged.
|
||||
A start-to-finish, reproducible path from a fresh Windows 11 machine to GLM-5.2 generating tokens, with the GPU tier. Every step and every failure mode below was hit and verified on real hardware: Core Ultra 9 285K (AVX-VNNI) / RTX 5080 (sm_120) / 128 GB RAM / Windows 11 24H2 (issue #306). Steps are ordered so the long downloads run while you build.
|
||||
|
||||
**Toolchain:** GCC via [winlibs](https://winlibs.com/) or MSYS2 MinGW-w64.
|
||||
Tested with GCC 16.1.0 (x86_64-ucrt-posix-seh).
|
||||
## 0. What you need
|
||||
|
||||
| Piece | Why | Get it |
|
||||
|---|---|---|
|
||||
| git, Python 3 | clone + `coli` launcher | winget / python.org |
|
||||
| MinGW-w64 gcc + make | builds the engine (MSVC can't) | `scoop install mingw-winlibs`, MSYS2, or portable **w64devkit** (no admin, unzip and go) |
|
||||
| CUDA Toolkit ≥ 12.8 | GPU tier; ≥12.8 required for Blackwell/sm_120 | `winget install Nvidia.CUDA` |
|
||||
| MSVC Build Tools (C++ workload) | nvcc's host compiler for the CUDA DLL | `winget install Microsoft.VisualStudio.2022.BuildTools` + "Desktop development with C++" |
|
||||
| ~400 GB free on a local NVMe | the int4 model (~370–384 GB) | NTFS is fine; **never** a network mount |
|
||||
|
||||
RAM: 16 GB minimum, more = bigger expert cache = faster. The build itself needs none of the CUDA/MSVC pieces — do the CPU build first, add the GPU tier later.
|
||||
|
||||
## 1. Start the model download first (it's the long pole)
|
||||
|
||||
```powershell
|
||||
# One-time toolchain install (pick one):
|
||||
scoop install mingw-winlibs # portable, no shell needed
|
||||
# or: pacman -S mingw-w64-x86_64-gcc make # via MSYS2
|
||||
python -m pip install -U "huggingface_hub[hf_transfer]"
|
||||
$env:HF_HUB_ENABLE_HF_TRANSFER = "1"
|
||||
hf download <model-repo> --local-dir D:\glm52_i4
|
||||
```
|
||||
|
||||
# Build (from c/ directory):
|
||||
make glm.exe # GLM-5.2 engine (static, no DLL dependencies)
|
||||
make olmoe.exe # OLMoE engine (same shims)
|
||||
make iobench.exe # disk I/O benchmark
|
||||
make test-c # run C tests
|
||||
make test-python # run Python tests (requires python)
|
||||
Use the container recommended in the README (with **int8 MTP heads** — int4 heads silently give 0% draft acceptance). The download is resumable: if it stops, rerun the same command. Expect hours; everything below fits inside them.
|
||||
|
||||
## 2. Build the engine (CPU)
|
||||
|
||||
From a normal PowerShell, in the repo's `c\` directory:
|
||||
|
||||
```powershell
|
||||
make glm.exe ARCH=native # ARCH=native unlocks AVX-VNNI on Alder Lake+/Arrow Lake
|
||||
make iobench.exe # disk benchmark, useful before committing to the download
|
||||
```
|
||||
|
||||
Warnings about `#pragma comment` and unused variables are normal (MSVC-isms gcc ignores). The engine banner should print `idot: avx-vnni` on VNNI-capable CPUs — if it says avx2, you built without `ARCH=native`.
|
||||
|
||||
### ⚠️ Smart App Control will block your fresh binary
|
||||
|
||||
On Windows 11 machines with **Smart App Control** enforced (`VerifiedAndReputablePolicyState = 1`), running your self-compiled `glm.exe` fails with:
|
||||
|
||||
```
|
||||
Program 'glm.exe' failed to run: An Application Control policy has blocked this file
|
||||
```
|
||||
|
||||
This is not Defender and not Mark-of-the-Web — SAC blocks *all* unsigned, unknown binaries, which includes anything you compile yourself. **Fix:** Windows Security → App & browser control → Smart App Control settings → **Off**, then **reboot** (the policy only reloads on restart). Note SAC is one-way: re-enabling later requires resetting Windows. If the settings page is missing, the registry equivalent is setting `HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy\VerifiedAndReputablePolicyState` to `0` (admin PowerShell), then rebooting. Check your current state before touching anything:
|
||||
|
||||
```powershell
|
||||
(Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy").VerifiedAndReputablePolicyState
|
||||
# 0 = off, 1 = enforced, 2 = evaluation
|
||||
```
|
||||
|
||||
## 3. Build the CUDA DLL (GPU tier)
|
||||
|
||||
nvcc needs MSVC as host compiler, so this one step must run from a shell with the MSVC environment: open **"x64 Native Tools Command Prompt for VS 2022"** from the Start menu (plain PowerShell will fail the `cl` check). Then:
|
||||
|
||||
```cmd
|
||||
make cuda-dll CUDA_ARCH=sm_120 # match your GPU: sm_120 Blackwell, sm_89 Ada, ...
|
||||
make glm.exe CUDA_DLL=1 ARCH=native # relink host with the runtime loader
|
||||
```
|
||||
|
||||
Two pitfalls, both fixed on current `dev` (#314) but worth knowing on older checkouts:
|
||||
|
||||
- **Spaces in `CUDA_HOME`** (`C:\Program Files\...`) used to break the recipe → fixed; nvcc now comes from PATH and `"$(NVCC)"` is quoted.
|
||||
- **`make glm.exe CUDA_DLL=1` after a CPU-only build** used to report `up to date` and silently keep the CPU-only binary (GPU tier never engages, no error). Current `dev` has a build-config stamp that forces the relink. On older trees: delete `glm.exe` first.
|
||||
|
||||
Sanity check: first GPU run should print `[CUDA] device 0: <your GPU>, ... sm_XX` and `[CUDA] mode: routed experts + resident dense tensors`.
|
||||
|
||||
## 4. First run
|
||||
|
||||
```powershell
|
||||
cd <repo>\c
|
||||
$env:OMP_NUM_THREADS = "<physical cores>"
|
||||
python coli run "Explain what a mixture-of-experts model is." --model D:\glm52_i4 --ngen 48
|
||||
```
|
||||
|
||||
The first run is cold — expect the profile to be dominated by `expert-disk` while the cache warms; hit rate climbs run over run. GPU tier on top:
|
||||
|
||||
```powershell
|
||||
$env:COLI_CUDA="1"; $env:COLI_GPU="0"; $env:CUDA_DENSE="1"; $env:CUDA_EXPERT_GB="4"
|
||||
python coli run "..." --model D:\glm52_i4 --ngen 64
|
||||
```
|
||||
|
||||
Size `CUDA_EXPERT_GB` so dense (~10 GB) + experts + working set stays under your VRAM. Note MTP speculation is off by default under CUDA (#293, float-accumulation divergence between draft and verify) — `COLI_CUDA_MTP=1` opts back in.
|
||||
|
||||
## 5. Reference numbers from this walkthrough's hardware
|
||||
|
||||
285K / RTX 5080 / 128 GB / NVMe at 5.85 GB/s random-read (19 MB blocks, `iobench`): 0.26 tok/s cold CPU → 0.30 warm CPU (MTP 2.2–2.3 tok/forward) → 0.42 tok/s GPU tier + auto-pin, expert hit 66%, ~65% of wall time in expert-disk. Disk-bound is the expected shape at ~25% expert residency — a faster disk and more RAM move the floor, the GPU moves the compute.
|
||||
|
||||
## Quick failure index
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| `An Application Control policy has blocked this file` | Smart App Control | §2 — turn SAC off + **reboot** |
|
||||
| `cuda-dll ... Error 1` immediately | old tree: spaced CUDA_HOME / MSVC rejects `-Wextra` | update to current `dev` (#314) |
|
||||
| `glm.exe is up to date` but GPU never engages | old tree: stale CPU-only binary | update to `dev`, or delete `glm.exe` and rebuild |
|
||||
| `cl.exe (MSVC) not in PATH` | built from plain PowerShell | use the x64 Native Tools prompt |
|
||||
| `nvcc fatal: unsupported gpu architecture 'sm_120'` | CUDA < 12.8 | install CUDA 12.8+ |
|
||||
| MTP `0% (0/0)` on CPU path | int4 MTP heads in the container | use the int8-MTP container |
|
||||
| MTP `draft=0` under CUDA | intended default since #293 | `COLI_CUDA_MTP=1` to opt in |
|
||||
|
||||
---
|
||||
|
||||
## Reference: build flags & warmup
|
||||
|
||||
# AVX-VNNI: Intel Alder Lake+ (and Meteor Lake+) CPUs have a 128-bit int8
|
||||
# dot-product instruction (VPDPBUSD) the engine can use for ~1.3x faster
|
||||
|
||||
Reference in New Issue
Block a user