web dashboard: live expert-tier panel (VRAM/RAM/disk), one-process hosting, coli web (#126)

* Web dashboard: live expert-tier panel (VRAM/RAM/disk), static hosting, coli web command

- Engine: TIERS protocol line (counts + GB for VRAM/RAM/disk experts)
  emitted at READY and refreshed after every served turn, computed from
  the live pin/LRU/CUDA state.
- Server: dispatcher tracks the latest snapshot, /health exposes it as
  "tiers"; the built web UI (web/dist) is served from the same port
  (read-only, traversal-safe, SPA fallback) so the dashboard needs no
  second process.
- Web: tier bar + legend in the Runtime panel showing where the 19k
  experts live right now, with GB per tier; updates with the existing
  health polling.
- CLI: `coli web` = serve + auto-open the browser once /health answers
  (the 744B load takes minutes; a poller waits for it), --no-browser to
  disable, friendly hint when web/dist isn't built.

* coli web: HERE is a path string, not a Path

* web: same-origin default + auto-connect when served by the engine

The page hosted by coli web talked to http://127.0.0.1:8000/v1 by
default while being loaded from http://localhost:8000 - a different
origin, so the very first probe died on CORS ('Failed to fetch').
When the page is engine-served the default (and a stored stale factory
default) now resolve to window.location.origin + /v1, and the console
auto-probes on load; the Vite dev server keeps the classic default.
The server's own port is also whitelisted in DEFAULT_CORS_ORIGINS for
the localhost/127.0.0.1 cross-name case.

* web: fix auto-connect effect returning Promise (React 19 StrictMode crash)

The async connect() call inside useEffect returned a Promise that React
19 StrictMode stored as the effect cleanup. On re-render, React called
destroy_() on that Promise — 'not a function'. Block body with explicit
return undefined prevents any non-function from leaking into the cleanup
slot.

* web: rich streaming metrics — live token counter, tok/s gauge, TTFT, session totals

During generation: a flashing Zap badge counts tokens in real time.
After: Gauge shows tok/s, Timer shows TTFT (time to first token),
Layers shows prompt→completion token counts, Clock shows queue wait.
Session totals (cumulative prompt + completion) appear in the Runtime
panel. All with lucide icons and tabular-nums for stable layout.

Tier bar gains a smoother cubic-bezier transition and slightly taller
height for visual weight.

* web: hardware environment panel — CPU model, GPU count/VRAM, RAM total/free, cores

Engine emits HWINFO protocol line at startup (CPU name from /proc/cpuinfo,
core count, RAM total/available from /proc/meminfo, GPU count + VRAM from
CUDA). Server parses and caches it, /health exposes as 'hwinfo'. Dashboard
renders it with icons at the top of the Runtime section so the user sees
immediately what the engine is running on.

* web: downgrade to React 18 — React 19 effect cleanup bug crashes the dashboard

React 19 treats the return value of every useEffect callback as a
cleanup function. In practice, any async interaction (streamChat's
rapid onDelta re-renders, the health poller, auto-connect) caused
React 19 to store a Promise as inst.destroy and crash with
'destroy_ is not a function' on the next commit cycle. The bug
reproduced in incognito, without StrictMode, and with every effect
converted to explicit block bodies returning undefined — it is a
React 19 regression in the passive-effect unmount path.

React 18 does not exhibit this behavior. Pin to React 18 until
React 19 is fixed or the codebase migrates to a pattern React 19
handles correctly. All 17 vitest pass, ErrorBoundary retained.

* web: Brain page — the expert cortex, live

A 76x256 canvas grid, one cell per expert (19,456 total): colour =
tier (green VRAM / blue RAM / grey disk), brightness = routing heat
(log-bucketed .coli_usage counts), and a white pulse that flashes on
every expert routed in the current turn and decays — you watch the
model think.

- Engine: EMAP protocol line (1 byte/expert: 2bit tier + 6bit heat,
  hex) at READY and after each turn; HITS bitmap of this turn's routed
  experts (set where eusage increments, cleared on emit).
- Server: parses both, GET /experts serves {rows, cols, map, hits, seq}.
- Web: Brain tab next to Chat; canvas renderer with rAF pulse decay;
  hover tooltip shows layer/expert/tier/heat plus an honest depth-role
  heuristic (early = surface features ... late = output shaping, MTP
  row labelled as the speculative draft head).

* web: Brain page responsive — cells sized from container via ResizeObserver

Cell size derives from the wrapper's actual client box instead of fixed
1400x900, re-rendering on resize; mobile media query tightens padding,
legend and tooltip. The cortex now fills whatever screen it gets.
This commit is contained in:
ZacharyZcR
2026-07-14 22:12:53 +08:00
committed by GitHub
parent d47b095875
commit 2878c60987
11 changed files with 678 additions and 40 deletions
+148 -1
View File
@@ -176,7 +176,12 @@ typedef struct {
int64_t resident_bytes;
} Model;
static void usage_save(Model *m); /* cache che impara: definita accanto a stats_dump */
static void usage_save(Model *m);
static void tiers_emit(Model *m);
static void ehit_mark(Model *m, int layer, int eid);
static void emap_emit(Model *m);
static void hits_emit(Model *m);
static void hwinfo_emit(Model *m); /* cache che impara: definita accanto a stats_dump */
#ifdef COLI_CUDA
static int g_cuda_enabled;
static double g_cuda_expert_gb;
@@ -1783,6 +1788,7 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out){
m->ereq+=keff[s];
for(int kk=0;kk<keff[s];kk++){
m->eusage[layer][idxs[(int64_t)s*K+kk]]++;
ehit_mark(m,layer,idxs[(int64_t)s*K+kk]);
if(m->eheat[layer][idxs[(int64_t)s*K+kk]]<UINT32_MAX) m->eheat[layer][idxs[(int64_t)s*K+kk]]++;
}
for(int d=0;d<D;d++) out[(int64_t)s*D+d]=0;
@@ -1810,6 +1816,7 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out){
keff[s]=Ke; m->ereq+=Ke;
for(int kk=0;kk<Ke;kk++){
m->eusage[layer][idx[kk]]++;
ehit_mark(m,layer,idx[kk]);
if(m->eheat[layer][idx[kk]]<UINT32_MAX) m->eheat[layer][idx[kk]]++;
m->elast[layer][idx[kk]]=++m->eaccess_clock;
}
@@ -3067,6 +3074,10 @@ static void mux_data(Tok *T, unsigned long long id, int token){
static void mux_done(Model *m, ServeCtx *sc, ServeReq *r){
double dt=now_s()-r->started; if(dt<1e-6) dt=1e-6;
double dh=(double)(m->hits-r->hits0), dm=(double)(m->miss-r->miss0);
hwinfo_emit(m);
tiers_emit(m);
emap_emit(m);
hits_emit(m);
printf("DONE %llu STAT %d %.2f %.1f %.2f %d %d\n",r->id,r->emitted,
r->emitted/dt,(dh+dm)>0?100.0*dh/(dh+dm):0.0,rss_gb(),
r->prompt_tokens,r->length_limited);
@@ -3155,6 +3166,9 @@ static void run_serve_mux(Model *m, const char *snap){
for(int i=0;i<nctx;i++) serve_ctx_init(m,&ctx[i],snap,i,maxctx);
setvbuf(stdin,NULL,_IONBF,0);
printf("\x01\x01READY\x01\x01\nSTAT 0 0.00 0.0 %.2f\n",rss_gb()); fflush(stdout);
hwinfo_emit(m);
tiers_emit(m);
emap_emit(m);
int eof=0;
for(;;){
int active=0; for(int i=0;i<nctx;i++) active+=req[i].active;
@@ -3250,6 +3264,7 @@ static void run_serve(Model *m, const char *snap){
#define first (sc->first)
char *line=NULL; size_t cap=0; ssize_t nr; char *buf=malloc(1<<16);
printf("\x01\x01" "READY" "\x01\x01\n"); printf("STAT 0 0.00 0.0 %.2f\n", rss_gb()); fflush(stdout);
tiers_emit(m);
while((nr=getline(&line,&cap,stdin))>0){
if(nr>0 && line[nr-1]=='\n') line[--nr]=0;
if(!strcmp(line,"\x02RESET")){ len=0; first=1; if(m->has_mtp) m->kv_start[m->c.n_layers]=-1;
@@ -3391,6 +3406,138 @@ static int64_t expert_bytes_probe(Model *m, int ebits){
return eb;
}
/* TIERS: fotografia della piramide expert per la dashboard web —
* "TIERS <vram> <ram> <disk> <vram_gb> <ram_gb>" sul canale di protocollo.
* ram = pinnati non-VRAM + LRU corrente; disk = tutto il resto. */
/* BRAIN MAP (dashboard): per-turn expert hit bitmap + full residency/heat map.
* g_ehit[layer][eid]=1 quando l'expert viene instradato in questo turno;
* hits_emit lo serializza e lo azzera. emap_emit fotografa tier+heat di TUTTI. */
static uint8_t **g_ehit;
static void ehit_mark(Model *m, int layer, int eid){
if(!g_ehit){ Cfg *c=&m->c;
g_ehit=calloc(c->n_layers+1,sizeof(uint8_t*));
for(int i=0;i<=c->n_layers;i++) g_ehit[i]=calloc(c->n_experts,1);
}
g_ehit[layer][eid]=1;
}
/* HWINFO: hardware snapshot for the web dashboard — emitted once at READY. */
static void hwinfo_emit(Model *m){
Cfg *c=&m->c;
/* CPU */
char cpu[256]=""; FILE *ci=fopen("/proc/cpuinfo","r");
if(ci){ char ln[256];
while(fgets(ln,sizeof(ln),ci)) if(!strncmp(ln,"model name",10)){
char *p=strchr(ln,':'); if(p){ p++; while(*p==' ')p++;
int n=(int)strlen(p); if(n>0&&p[n-1]=='\n')p[--n]=0;
snprintf(cpu,sizeof(cpu),"%s",p); } break; }
fclose(ci); }
int cores=0;
#ifdef _SC_NPROCESSORS_ONLN
cores=(int)sysconf(_SC_NPROCESSORS_ONLN);
#endif
/* RAM */
double ram_total=0,ram_avail=0;
FILE *mi=fopen("/proc/meminfo","r");
if(mi){ char ln[256]; double mt=0,ma=0;
while(fgets(ln,sizeof(ln),mi)){
if(sscanf(ln,"MemTotal: %lf",&mt)==1) ram_total=mt/1e6;
if(sscanf(ln,"MemAvailable: %lf",&ma)==1) ram_avail=ma/1e6;
} fclose(mi); }
/* GPU */
int ngpu=0; double vram_total=0;
char gpu_name[128]="";
#ifdef COLI_CUDA
ngpu=g_cuda_ndev; vram_total=m->gpu_expert_bytes/1e9;
for(int i=0;i<g_cuda_ndev;i++){
size_t fr=0,to=0; coli_cuda_mem_info(g_cuda_devices[i],&fr,&to);
if(!i) vram_total=(double)to*g_cuda_ndev/1e9;
}
/* GPU name from the first device — already printed at init */
if(g_cuda_ndev>0){
/* We don't have a device-name API; parse from the init log line stored in stderr.
* Simpler: just read the nvidia driver sysfs or use a fixed label. */
snprintf(gpu_name,sizeof(gpu_name),"CUDA device x%d",g_cuda_ndev);
}
#endif
printf("HWINFO %d %.1f %.1f %d %.1f %s|%s\n",
cores,ram_total,ram_avail,ngpu,vram_total,cpu,gpu_name);
fflush(stdout);
}
static void tiers_emit(Model *m){
Cfg *c=&m->c; int nsp=0;
for(int i=0;i<c->n_layers;i++) if(m->L[i].sparse) nsp++;
int total=(nsp+(m->has_mtp?1:0))*c->n_experts;
int pinned=0,lru=0;
for(int i=0;i<=c->n_layers;i++){ pinned+=m->npin?m->npin[i]:0; lru+=m->ecn?m->ecn[i]:0; }
int vram=0; double vram_gb=0;
#ifdef COLI_CUDA
vram=m->gpu_expert_count; vram_gb=m->gpu_expert_bytes/1e9;
#endif
int ram=pinned-vram+lru; if(ram<0) ram=0;
int disk=total-vram-ram; if(disk<0) disk=0;
double eb=(double)expert_bytes_probe(m,m->ebits);
printf("TIERS %d %d %d %.2f %.2f\n",vram,ram,disk,vram_gb,ram*eb/1e9);
fflush(stdout);
}
/* EMAP: 1 byte/expert (2bit tier: 0=disk 1=RAM 2=VRAM | 6bit heat log2-bucket),
* righe = layer sparsi in ordine (+MTP se presente), colonne = n_experts. Hex. */
static void emap_emit(Model *m){
Cfg *c=&m->c;
int rows=0;
for(int i=0;i<c->n_layers;i++) if(m->L[i].sparse) rows++;
int has_mtp = m->has_mtp && m->eusage[c->n_layers];
if(has_mtp) rows++;
int cols=c->n_experts;
char *hex=malloc((size_t)rows*cols*2+1); int w=0;
for(int i=0;i<=c->n_layers;i++){
int is_row = (i<c->n_layers && m->L[i].sparse) || (i==c->n_layers && has_mtp);
if(!is_row) continue;
for(int e=0;e<cols;e++){
int tier=0;
ESlot *P=m->pin[i];
for(int z=0;z<m->npin[i];z++) if(P[z].eid==e){
#ifdef COLI_CUDA
tier = P[z].g.cuda?2:1;
#else
tier = 1;
#endif
break; }
if(!tier && m->ecache && m->ecache[i])
for(int z=0;z<m->ecn[i];z++) if(m->ecache[i][z].eid==e){ tier=1; break; }
uint32_t u = m->eusage[i]?m->eusage[i][e]:0;
int heat=0; while(u){ heat++; u>>=1; } if(heat>63) heat=63;
int b=(tier<<6)|heat;
hex[w++]="0123456789abcdef"[b>>4]; hex[w++]="0123456789abcdef"[b&15];
}
}
hex[w]=0;
printf("EMAP %d %d %s\n",rows,cols,hex); fflush(stdout); free(hex);
}
/* HITS: bitmap 1bit/expert (stesso ordine di EMAP), poi azzera per il turno dopo. */
static void hits_emit(Model *m){
Cfg *c=&m->c; if(!g_ehit) return;
int rows=0;
for(int i=0;i<c->n_layers;i++) if(m->L[i].sparse) rows++;
int has_mtp = m->has_mtp && m->eusage[c->n_layers];
if(has_mtp) rows++;
int cols=c->n_experts, nb=(rows*cols+7)/8;
uint8_t *bm=calloc(nb,1); int bit=0;
for(int i=0;i<=c->n_layers;i++){
int is_row = (i<c->n_layers && m->L[i].sparse) || (i==c->n_layers && has_mtp);
if(!is_row) continue;
for(int e=0;e<cols;e++,bit++)
if(g_ehit[i][e]){ bm[bit>>3]|=1<<(bit&7); g_ehit[i][e]=0; }
}
char *hex=malloc((size_t)nb*2+1); int w=0;
for(int b=0;b<nb;b++){ hex[w++]="0123456789abcdef"[bm[b]>>4]; hex[w++]="0123456789abcdef"[bm[b]&15]; }
hex[w]=0;
printf("HITS %d %d %s\n",rows,cols,hex); fflush(stdout); free(hex); free(bm);
}
/* scarica su file l'istogramma d'uso degli expert: righe "layer eid count" (per PIN).
* Include la riga MTP (layer n_layers). Scrittura atomica (tmp+rename): viene chiamata
* anche a ogni turno di serve e il processo puo' morire in qualsiasi momento. */