Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,33 @@ the right experts get. It works because routing has measurable structure (see
the [expert atlas](https://github.com/JustVugg/colibri/issues/175)) — and
structure is cacheable.

The engine is a single C file (`c/glm.c`) plus small headers. No BLAS, no Python
at runtime, no GPU required.
The engine is a single C file (`c/colibri.c`) plus small headers. No BLAS, no
Python at runtime, no GPU required.

### Browser WebGPU expert workers

The optional bridge lets a browser with WebGPU execute exported expert FFNs.
Start the bridge and serve `web/public` from the same origin:

```bash
./coli webgpu --host 0.0.0.0 --control-port 8765 --data-port 8766
```

Open `web/public/webgpu-worker.html`, choose the bridge WebSocket URL, and load
a manifest exported from a source checkpoint:

```bash
uv run python c/tools/export_webgpu_expert.py \
--source /models/glm52_fp8 --output web/public \
--layer 0-2 --expert 0-7
```

Point the native coordinator at the bridge with `--webgpu-workers BRIDGE_IP:8766`.
The bridge uses the same `COLIEX01` request/response framing as native expert
workers, including the layer, hidden, intermediate, expert IDs, and raw
little-endian f32 activations. This first format is intentionally f32 for
cross-worker validation; quantized WebGPU buffers can be added without
changing the activation protocol.

## How it works

Expand Down
7 changes: 5 additions & 2 deletions c/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ else
PYTHON ?= python3
endif
CUDA_OBJ =
TEST_BINS = tests/test_json$(EXE) tests/test_st$(EXE) tests/test_st_mirror$(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_temp_env$(EXE) tests/test_tok_o200k$(EXE) tests/test_kv_alloc$(EXE) tests/test_int3$(EXE) tests/test_int3_load$(EXE) tests/test_i4_acc512$(EXE) tests/test_compat_direct$(EXE) tests/test_dsa_select$(EXE) tests/test_logit_nan$(EXE) tests/test_pipe_block$(EXE) tests/test_e8_kernel$(EXE)
TEST_BINS = tests/test_json$(EXE) tests/test_st$(EXE) tests/test_st_mirror$(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_temp_env$(EXE) tests/test_tok_o200k$(EXE) tests/test_kv_alloc$(EXE) tests/test_int3$(EXE) tests/test_int3_load$(EXE) tests/test_i4_acc512$(EXE) tests/test_compat_direct$(EXE) tests/test_dsa_select$(EXE) tests/test_logit_nan$(EXE) tests/test_pipe_block$(EXE) tests/test_e8_kernel$(EXE) tests/test_webgpu_protocol$(EXE)
ifneq (,$(LINUX))
TEST_BINS += tests/test_uring$(EXE)
endif
Expand Down Expand Up @@ -282,7 +282,7 @@ $(file >.build-config,$(BUILD_CONFIG))
endif
.build-config: ;

colibri$(EXE): colibri.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h quant.h sample.h kv_persist.h telemetry.h $(CUDA_OBJ) $(METAL_OBJ) .build-config
colibri$(EXE): colibri.c webgpu.h st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h quant.h sample.h kv_persist.h telemetry.h $(CUDA_OBJ) $(METAL_OBJ) .build-config
$(CC) $(CFLAGS) colibri.c $(CUDA_OBJ) $(METAL_OBJ) -o colibri$(EXE) $(LDFLAGS)

# Windows runtime loader object: resolves coli_cuda_* from coli_cuda.dll.
Expand Down Expand Up @@ -465,6 +465,9 @@ tests/test_uring$(EXE): tests/test_uring.c colibri.c st.h uring.h json.h tok.h t
tests/test_pipe_block$(EXE): tests/test_pipe_block.c colibri.c st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)

tests/test_webgpu_protocol$(EXE): tests/test_webgpu_protocol.c colibri.c webgpu.h st.h uring.h json.h tok.h tok_unicode.h compat.h grammar.h tier.h quant.h sample.h kv_persist.h telemetry.h
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)

test-c: $(TEST_BINS)
$(PYTHON) tools/run_tests.py $(TEST_BINS)

Expand Down
13 changes: 13 additions & 0 deletions c/coli
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ def env_for(a):
e.setdefault("PIPE", "1")
e.setdefault("PILOT_REAL", "1")
e["COLI_POLICY"]=a.policy
if getattr(a, "webgpu_workers", None): e["WEBGPU_WORKERS"] = a.webgpu_workers
if a.ram: e["RAM_GB"]=str(a.ram)
if a.ngen: e["NGEN"]=str(a.ngen)
if a.topp: e["TOPP"]=str(a.topp)
Expand Down Expand Up @@ -836,6 +837,11 @@ def cmd_serve(a):
try: os.unlink(serve_pidfile(a.port))
except OSError: pass

def cmd_webgpu_coordinator(a):
return subprocess.call([sys.executable, os.path.join(HERE, "webgpu.py"),
"--host", a.host, "--control-port", str(a.control_port),
"--data-port", str(a.data_port)])

def cmd_stop(a):
"""Shut down a running `coli serve` AND its engine — one command, no pkill.
The engine re-execs itself for OMP tuning, so its process is named `exe`,
Expand Down Expand Up @@ -963,6 +969,8 @@ def main():
common.add_argument("--cap", type=int, default=8); common.add_argument("--ngen", type=int, default=1024) # rete di sicurezza: la fine vera la decidono gli stop token
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)
common.add_argument("--webgpu-workers", default=os.environ.get("WEBGPU_WORKERS"),
help="WebGPU proxy endpoint, host:port")
ap=argparse.ArgumentParser(prog="coli", parents=[common], description="colibri — run GLM-5.2 locally")
ap.add_argument("--version", action="version", version=f"colibri {_version}")
sub=ap.add_subparsers(dest="cmd")
Expand All @@ -988,6 +996,10 @@ def main():
ps.add_argument("--max-queue",type=int,default=int(os.environ.get("COLI_MAX_QUEUE","8")))
ps.add_argument("--queue-timeout",type=float,default=float(os.environ.get("COLI_QUEUE_TIMEOUT","300")))
ps.add_argument("--kv-slots",type=int,default=int(os.environ.get("COLI_KV_SLOTS","1")))
pcluster=sub.add_parser("webgpu", help="serve the browser WebGPU coordinator bridge")
pcluster.add_argument("--host",default="127.0.0.1")
pcluster.add_argument("--control-port",type=int,default=8765)
pcluster.add_argument("--data-port",type=int,default=8766)
pst=sub.add_parser("stop", parents=[common], help="shut down a running coli serve and its engine")
pst.add_argument("--port",type=int,default=8000); pst.add_argument("--dry-run",action="store_true")
pw=sub.add_parser("web", parents=[common], help="serve + open the dashboard in a browser")
Expand Down Expand Up @@ -1015,6 +1027,7 @@ def main():
handler={"build":cmd_build,"info":cmd_info,"plan":cmd_plan,"doctor":cmd_doctor,
"run":cmd_run,"chat":cmd_chat,"serve":cmd_serve,"stop":cmd_stop,"bench":cmd_bench,
"convert":cmd_convert,"web":cmd_web}.get(a.cmd)
if a.cmd=="webgpu": handler=cmd_webgpu_coordinator
if handler: sys.exit(handler(a) or 0)
banner(); print(__doc__)

Expand Down
23 changes: 23 additions & 0 deletions c/colibri.c
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
#include <unistd.h>
#if defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)
#include <sys/select.h> /* select() serve-loop polling (#68); not on native MinGW */
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#endif
#if defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)
#include <sys/resource.h>
Expand Down Expand Up @@ -75,6 +79,11 @@ static const float *g_pre_sh;
/* routing precalcolata dalla GPU (Metal layer CB o device router CUDA, #431):
* moe() la usa e salta la FASE A. NULL = router su CPU. */
static const int *g_pre_idx; static const float *g_pre_w; static const int *g_pre_keff;
#if !defined(_WIN32)
typedef struct { int fd; char host[128]; int port; } WebGPUWorker;
static WebGPUWorker g_webgpu_worker;
static int g_webgpu_enabled;
#endif
#ifdef __APPLE__
#include <mach/mach.h> /* host_statistics64: MemAvailable di macOS */
#endif
Expand Down Expand Up @@ -1670,6 +1679,8 @@ static int expert_load(Model *m, int layer, int eid, ESlot *s, int fatal, int de
return rc;
}

#include "webgpu.h"

#ifdef __linux__
/* io_uring expert batches. One owner prepares all reads for a block, submits
* them in one syscall, and reaps CQEs on demand. The kernel, rather than a set
Expand Down Expand Up @@ -3027,6 +3038,12 @@ static void moe(Model *m, Layer *l, int layer, float *x, int S, float *out, int
int shared_on_gpu=0; (void)shared_on_gpu; /* set by the Metal path when Phase E was fused */
for(int base=0;base<nu;base+=64){
int nb = nu-base<64 ? nu-base : 64;
#if !defined(_WIN32)
if(g_webgpu_enabled){
webgpu_moe_batch(m,layer,x,S,out,idxs,ws,keff,K,uniq,base,nb);
continue;
}
#endif
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];
Expand Down Expand Up @@ -6533,6 +6550,12 @@ int main(int argc, char **argv){
#endif
printf("== GLM C engine (glm_moe_dsa), cache=%d experts/layer | experts@%d-bit dense@%d-bit | idot: " IDOT_KERNEL " ==\n", cap, ebits, dbits);
g_mem_avail_boot = mem_available_gb();
#if !defined(_WIN32)
if(getenv("WEBGPU_WORKERS") && *getenv("WEBGPU_WORKERS")){
webgpu_init();
atexit(webgpu_close);
}
#endif
Model m; double t0=now_s(); model_init(&m,snap,cap,ebits,dbits);
if(!g_direct_heat_explicit){ /* COLI_DISKCLASS_WINDOW default, needs m.c (topk/n_layers) */
/* CURRENT-STATE CALIBRATION: the "8" multiplier (recency window ~= the last 8
Expand Down
94 changes: 94 additions & 0 deletions c/tests/test_webgpu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import socket
import struct
import sys
import threading
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from webgpu import (WebGPURegistry, _ProxyHandler, _ProxyServer, _recv_exact,
_ws_frame, _ws_read_frame, pack_batch, pack_response,
parse_batch, parse_response)


class WebGPURuntimeTests(unittest.TestCase):
def test_batch_and_response_preserve_raw_activation_bytes(self):
batch = {"layer": 4, "hidden": 2, "intermediate": 3,
"items": [{"expert_id": 7, "rows": 1, "activations": b"abcdefgh"}]}
decoded = parse_batch(pack_batch(batch))
self.assertEqual(decoded["intermediate"], 3)
self.assertEqual(decoded["items"][0]["activations"], b"abcdefgh")
version, status, count, _ = parse_response(pack_response(decoded, [b"12345678"]))
self.assertEqual((version, status, count), (1, 0, 1))

def test_native_and_webgpu_workers_share_the_same_request_layout(self):
batch = {"layer": 4, "hidden": 2, "intermediate": 3,
"items": [{"expert_id": 7, "rows": 1, "activations": b"abcdefgh"}]}
native_wire = (b"COLIEX01" + struct.pack("!5I", 1, 4, 2, 3, 1)
+ struct.pack("!2I", 7, 1) + b"abcdefgh")
self.assertEqual(pack_batch(batch), native_wire)
self.assertEqual(parse_batch(native_wire), batch | {"version": 1})

def test_dispatches_batch_to_a_browser_connection(self):
coordinator, browser = socket.socketpair()
registry = WebGPURegistry()
connection = registry.register(coordinator, {"node_id": "browser-a", "expert_ids": ["4:7"]})

def browser_worker():
opcode, payload = _ws_read_frame(browser)
self.assertEqual(opcode, 2)
incoming = parse_batch(payload)
browser.sendall(_ws_frame(2, pack_response(incoming, [b"12345678"])))

worker = threading.Thread(target=browser_worker)
worker.start()
batch = {"layer": 4, "hidden": 2, "intermediate": 3,
"items": [{"expert_id": 7, "rows": 1, "activations": b"abcdefgh"}]}
try:
version, status, count, _ = parse_response(registry.dispatch(pack_batch(batch)))
self.assertEqual((version, status, count), (1, 0, 1))
worker.join(timeout=1)
self.assertFalse(worker.is_alive())
finally:
registry.unregister(connection)
browser.close()

def test_native_tcp_client_can_use_webgpu_proxy_wire_contract(self):
registry = WebGPURegistry()
coordinator, browser = socket.socketpair()
connection = registry.register(coordinator, {"node_id": "browser-a", "expert_ids": ["4:7"]})
proxy = _ProxyServer(("127.0.0.1", 0), _ProxyHandler)
proxy.registry = registry
thread = threading.Thread(target=proxy.serve_forever, daemon=True)
thread.start()

def browser_worker():
opcode, payload = _ws_read_frame(browser)
self.assertEqual(opcode, 2)
incoming = parse_batch(payload)
browser.sendall(_ws_frame(2, pack_response(incoming, [b"12345678"])))

worker = threading.Thread(target=browser_worker)
worker.start()
native = socket.create_connection(proxy.server_address)
batch = {"layer": 4, "hidden": 2, "intermediate": 3,
"items": [{"expert_id": 7, "rows": 1, "activations": b"abcdefgh"}]}
try:
native.sendall(pack_batch(batch))
header = _recv_exact(native, 20)
_, status, count = parse_response(header)[0:3]
payload = _recv_exact(native, 8 + 8)
self.assertEqual((status, count), (0, 1))
self.assertEqual(parse_response(header + payload)[3][-8:], b"12345678")
worker.join(timeout=1)
self.assertFalse(worker.is_alive())
finally:
native.close()
proxy.shutdown(); proxy.server_close()
registry.unregister(connection)
browser.close()


if __name__ == "__main__":
unittest.main()
52 changes: 52 additions & 0 deletions c/tests/test_webgpu_protocol.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* Compatibility gate for native expert workers and WebGPU proxies.
* Both consume COLIEX01: network-order u32 headers plus raw f32 bytes. */
#define main coli_engine_main_unused
#include "../colibri.c"
#undef main

#include <assert.h>

#if defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)
static void test_shared_wire_header(void)
{
int sockets[2];
assert(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == 0);
uint32_t value;
float input[2] = {1.0f, -2.0f};
assert(webgpu_io(sockets[0], (void *)COLI_WEBGPU_MAGIC, 8, 1) == 0);
value = 1; assert(webgpu_u32(sockets[0], &value, 1) == 0);
value = 4; assert(webgpu_u32(sockets[0], &value, 1) == 0);
value = 2; assert(webgpu_u32(sockets[0], &value, 1) == 0);
value = 3; assert(webgpu_u32(sockets[0], &value, 1) == 0);
value = 1; assert(webgpu_u32(sockets[0], &value, 1) == 0);
value = 7; assert(webgpu_u32(sockets[0], &value, 1) == 0);
value = 1; assert(webgpu_u32(sockets[0], &value, 1) == 0);
assert(webgpu_io(sockets[0], input, sizeof(input), 1) == 0);

char magic[8];
assert(webgpu_io(sockets[1], magic, 8, 0) == 0);
assert(memcmp(magic, COLI_WEBGPU_MAGIC, 8) == 0);
value = 0; assert(webgpu_u32(sockets[1], &value, 0) == 0 && value == 1);
value = 0; assert(webgpu_u32(sockets[1], &value, 0) == 0 && value == 4);
value = 0; assert(webgpu_u32(sockets[1], &value, 0) == 0 && value == 2);
value = 0; assert(webgpu_u32(sockets[1], &value, 0) == 0 && value == 3);
value = 0; assert(webgpu_u32(sockets[1], &value, 0) == 0 && value == 1);
value = 0; assert(webgpu_u32(sockets[1], &value, 0) == 0 && value == 7);
value = 0; assert(webgpu_u32(sockets[1], &value, 0) == 0 && value == 1);
float received[2] = {0};
assert(webgpu_io(sockets[1], received, sizeof(received), 0) == 0);
assert(received[0] == 1.0f && received[1] == -2.0f);
close(sockets[0]); close(sockets[1]);
}
#endif

int main(void)
{
#if defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)
test_shared_wire_header();
puts("webgpu protocol compatibility: ok");
#else
puts("webgpu protocol compatibility: skipped on Windows");
#endif
return 0;
}
70 changes: 70 additions & 0 deletions c/tools/export_webgpu_expert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Export selected dense MoE experts as browser-uploadable little-endian f32 buffers."""

import argparse
import json
from pathlib import Path


def parse_indices(values):
result = []
for value in values:
for part in value.split(","):
if "-" in part:
start, end = (int(item) for item in part.split("-", 1))
result.extend(range(start, end + 1))
elif part.strip():
result.append(int(part))
return sorted(set(result))


def export(source, output, layers, experts):
try:
import numpy as np
from safetensors import safe_open
except ImportError as error:
raise SystemExit("install exporter dependencies with: uv pip install numpy safetensors") from error
source, output = Path(source), Path(output)
shards = sorted(source.glob("*.safetensors"))
if not shards:
raise SystemExit(f"no safetensors files found in {source}")
index = {}
for shard in shards:
with safe_open(str(shard), framework="pt", device="cpu") as handle:
for name in handle.keys():
index.setdefault(name, shard)
manifest = {"schema_version": 1, "dtype": "f32", "experts": {}}
for layer in layers:
for expert in experts:
prefix = f"model.layers.{layer}.mlp.experts.{expert}"
names = {key: f"{prefix}.{key}_proj.weight" for key in ("gate", "up", "down")}
missing = [name for name in names.values() if name not in index]
if missing:
raise SystemExit(f"missing tensors for {layer}:{expert}: {', '.join(missing)}")
arrays = {}
for key, name in names.items():
with safe_open(str(index[name]), framework="pt", device="cpu") as handle:
arrays[key] = np.asarray(handle.get_tensor(name).detach().cpu().numpy(), dtype="<f4")
gate, up, down = arrays["gate"], arrays["up"], arrays["down"]
if gate.ndim != 2 or up.shape != gate.shape or down.shape != (gate.shape[1], gate.shape[0]):
raise SystemExit(f"unexpected shapes for {layer}:{expert}: {gate.shape}, {up.shape}, {down.shape}")
manifest.setdefault("hidden", int(gate.shape[1])); manifest.setdefault("intermediate", int(gate.shape[0]))
if (manifest["hidden"], manifest["intermediate"]) != (gate.shape[1], gate.shape[0]):
raise SystemExit("all exported experts must have the same dimensions")
directory = output / "experts" / f"layer-{layer:04d}" / f"expert-{expert:04d}"
directory.mkdir(parents=True, exist_ok=True)
paths = {}
for key, array in arrays.items():
path = directory / f"{key}.f32"; array.tofile(path); paths[key] = str(path.relative_to(output))
manifest["experts"][f"{layer}:{expert}"] = paths
(output / "webgpu-manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
print(json.dumps({"manifest": str(output / "webgpu-manifest.json"), "experts": len(manifest["experts"]),
"hidden": manifest["hidden"], "intermediate": manifest["intermediate"]}, indent=2))


if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", required=True); parser.add_argument("--output", required=True)
parser.add_argument("--layer", action="append", required=True); parser.add_argument("--expert", action="append", required=True)
args = parser.parse_args()
export(args.source, args.output, parse_indices(args.layer), parse_indices(args.expert))
Loading
Loading