Windows native port: serve-mode pipe fix + RAM detection + POSIX guards, AVX-VNNI kernel, gated CUDA DLL (#131, fixes #123)

Rebased onto current dev, split into 3 logical parts (all validated):
1. CPU portability (serve-mode _O_BINARY pipe fix — stock main hangs on MinGW without it; RAM detection cap 0->9/layer; POSIX guards for select/mmap/madvise; warmup script).
2. AVX-VNNI 128-bit int8/int4 dot kernel (Alder Lake+/Meteor Lake+), bit-identical to AVX2 (author-verified on Meteor Lake; compiles out to AVX2 elsewhere) + _mm256_extracti128_si256 typo fix that blocked -march=native.
3. CUDA DLL via LoadLibrary, gated behind CUDA_DLL=1 (host never links cudart; silent CPU fallback if absent; author-verified on RTX 5070 Ti).

Validated here: make check 59/59, oracle 32/32 TF, Windows cross-compile clean + glm.exe loads+runs via WSL interop. Fixes the #123 Windows build failure.
This commit is contained in:
woolcoxm
2026-07-13 14:54:30 -04:00
committed by GitHub
parent afc259c599
commit 2319b942d2
10 changed files with 644 additions and 30 deletions
+11
View File
@@ -11,15 +11,26 @@ desktop/src-tauri/gen/
# binari compilati (si rigenerano con make / coli build) # binari compilati (si rigenerano con make / coli build)
c/glm c/glm
c/glm.exe
c/olmoe c/olmoe
c/olmoe.exe
c/iobench c/iobench
c/iobench.exe
c/tok_test c/tok_test
c/backend_cuda.o c/backend_cuda.o
c/backend_cuda_test c/backend_cuda_test
c/backend_loader.o
c/coli_cuda.dll
c/coli_cuda.lib
c/coli_cuda.exp
c/tests/test_json c/tests/test_json
c/tests/test_json.exe
c/tests/test_st c/tests/test_st
c/tests/test_st.exe
c/tests/test_tier c/tests/test_tier
c/tests/test_tier.exe
c/tests/test_grammar c/tests/test_grammar
c/tests/test_grammar.exe
# oracoli tiny generati (make_glm_oracle.py) e dati benchmark scaricati # oracoli tiny generati (make_glm_oracle.py) e dati benchmark scaricati
c/glm_tiny/ c/glm_tiny/
+49 -3
View File
@@ -160,6 +160,12 @@ make iobench.exe # disk I/O benchmark
make test-c # run C tests make test-c # run C tests
make test-python # run Python tests (requires python) make test-python # run Python tests (requires python)
# 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
# quantized matmul. The x86-64-v3 default (portable AVX2) compiles it out;
# build for THIS machine to enable it:
make glm.exe ARCH=native # banner prints "idot: avx-vnni"
# Verify (tiny model, 2.4 MB): # Verify (tiny model, 2.4 MB):
pip install torch transformers safetensors huggingface_hub pip install torch transformers safetensors huggingface_hub
python tools/make_glm_oracle.py # generate tiny oracle python tools/make_glm_oracle.py # generate tiny oracle
@@ -171,9 +177,49 @@ python coli chat --model D:\glm52_i4 # interactive chat
python coli serve --model D:\glm52_i4 # OpenAI-compatible API python coli serve --model D:\glm52_i4 # OpenAI-compatible API
``` ```
**Status:** Phase 1 complete (compiles, correct, static-linked). O_DIRECT (Phase 2), **Warmup (overnight cache priming):** the engine's expert cache learns from
GPU via `LoadLibrary` on `coli_cuda.dll` (Phases G0G2), and full-model validation your workload. The included `warmup.ps1` script runs `coli run` in a loop with
are separate workstreams. See `PORT_WINDOWS_PLAN.md` for the full plan. diverse prompts to build the `.coli_usage` histogram unattended, so the next
real session starts with a large, accurate hot-expert pin. Each run saves usage
atomically on clean completion.
```powershell
.\warmup.ps1 -Rounds 1 -Ngen 32 # ~60-90 min, durable progress
```
**NVIDIA GPU (optional, via runtime DLL):** on Windows the engine is built with
MinGW gcc but CUDA kernels require MSVC + nvcc. The split is clean: build the
CUDA backend into a standalone `coli_cuda.dll` (nvcc + MSVC), then the host
`glm.exe` loads it at runtime via `LoadLibrary` (`c/backend_loader.c`). The host
never links cudart directly; if the DLL is absent the engine falls back to CPU
without error.
```powershell
# Prerequisites: CUDA Toolkit + MSVC Build Tools (cl.exe) + nvcc on PATH.
# Build the DLL from a shell with the MSVC environment set (vcvars64.bat or
# "x64 Native Tools Command Prompt for VS"):
make cuda-dll CUDA_HOME="C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.8" CUDA_ARCH=sm_120
# Build the host with the runtime loader (CUDA_DLL=1 adds -DCOLI_CUDA and
# links backend_loader.o instead of cudart):
make glm.exe CUDA_DLL=1 ARCH=native
# Run with the GPU expert tier (8 GB VRAM budget here; scale to your free VRAM):
$env:COLI_CUDA="1"; $env:COLI_GPU="0"; $env:CUDA_EXPERT_GB="8"
python coli chat --model D:\glm52_i4 --topp 0.7
```
The DLL exports 11 `extern "C"` symbols (`coli_cuda_init`, `coli_cuda_matmul`,
etc.); `backend_loader.c` resolves them via `GetProcAddress` on first use.
`ColiCudaTensor*` is opaque to the host (stored, never dereferenced), so the
MSVC-allocated struct is safe across the ABI boundary. `CUDA_ARCH` must match
your GPU's compute capability (e.g. `sm_120` for Blackwell / RTX 50-series,
`sm_89` for Ada / RTX 40-series).
**Status:** Phase 1 complete (compiles, correct, static-linked). The Windows
GPU tier (runtime `coli_cuda.dll` via `LoadLibrary`) is implemented and
verified on RTX 50-series (sm_120). O_DIRECT (Phase 2) and full-model
validation against the transformers oracle remain separate workstreams.
### OpenAI-compatible API ### OpenAI-compatible API
+46 -7
View File
@@ -35,9 +35,12 @@ EXE =
else ifneq ($(IS_WIN),) else ifneq ($(IS_WIN),)
# --- Windows 11 x86-64 (MinGW-w64 / MSYS2) --- # --- Windows 11 x86-64 (MinGW-w64 / MSYS2) ---
# GCC + libgomp + winpthreads: pthread, OpenMP, clock_gettime, opendir/readdir, # GCC + libgomp + winpthreads: pthread, OpenMP, clock_gettime, opendir/readdir,
# AVX2 intrinsics tutto gratis, nessun porting. # AVX2 intrinsics - tutto gratis, nessun porting.
# ARCH default = x86-64-v3 (binario portabile con AVX2). Per max velocita' # ARCH default = x86-64-v3 (portable binary with AVX2). For max speed on THIS
# su QUESTA macchina: make ARCH=native # machine use ARCH=native: on AVX-VNNI CPUs (Intel Alder Lake+, Meteor Lake+)
# it also unlocks the 128-bit VPDPBUSD int8/int4 dot kernel (dot_i8i8/dot_i4i8),
# which the x86-64-v3 baseline does not define. The #ifdef guards in glm.c mean
# a v3 build simply compiles out the VNNI path - safe on any x86-64.
CC = gcc CC = gcc
ARCH ?= x86-64-v3 ARCH ?= x86-64-v3
CFLAGS = -D_FILE_OFFSET_BITS=64 -O3 -march=$(ARCH) -fopenmp -Wall -Wextra -Wno-unused-parameter -Wno-misleading-indentation -Wno-unused-function CFLAGS = -D_FILE_OFFSET_BITS=64 -O3 -march=$(ARCH) -fopenmp -Wall -Wextra -Wno-unused-parameter -Wno-misleading-indentation -Wno-unused-function
@@ -70,7 +73,17 @@ endif
# CUDA=1 adds an opt-in backend for resident tensors. The default build remains # CUDA=1 adds an opt-in backend for resident tensors. The default build remains
# pure C and keeps the original zero-dependency runtime. # pure C and keeps the original zero-dependency runtime.
#
# Two paths:
# - Linux/macOS: CUDA=1 links backend_cuda.o directly (cudart via -l).
# - Windows: CUDA_DLL=1 builds a standalone coli_cuda.dll (nvcc+MSVC),
# then the host glm.exe loads it at runtime via backend_loader.c
# (LoadLibrary/GetProcAddress). MinGW gcc cannot compile .cu
# (nvcc needs cl.exe), and cross-linking MSVC objects into a
# gcc binary is fragile — the DLL split keeps the toolchains
# clean. See backend_loader.c and README "cuda-dll" below.
CUDA ?= 0 CUDA ?= 0
CUDA_DLL ?= 0
CUDA_HOME ?= /usr/local/cuda CUDA_HOME ?= /usr/local/cuda
NVCC ?= $(CUDA_HOME)/bin/nvcc NVCC ?= $(CUDA_HOME)/bin/nvcc
CUDA_ARCH ?= native CUDA_ARCH ?= native
@@ -78,13 +91,23 @@ NVCCFLAGS ?= -O3 -std=c++17 -arch=$(CUDA_ARCH) -Xcompiler=-Wall,-Wextra
PYTHON ?= python3 PYTHON ?= python3
CUDA_OBJ = CUDA_OBJ =
TEST_BINS = tests/test_json$(EXE) tests/test_st$(EXE) tests/test_tier$(EXE) tests/test_grammar$(EXE) tests/test_decode_batch$(EXE) tests/test_idot$(EXE) tests/test_i4_acc512$(EXE) TEST_BINS = tests/test_json$(EXE) tests/test_st$(EXE) tests/test_tier$(EXE) tests/test_grammar$(EXE) tests/test_decode_batch$(EXE) tests/test_idot$(EXE) tests/test_i4_acc512$(EXE)
# Windows CUDA DLL path: host links the loader, NOT cudart.
ifneq ($(IS_WIN),)
ifeq ($(CUDA_DLL),1)
CFLAGS += -DCOLI_CUDA
CUDA_OBJ = backend_loader.o
endif
endif
# Linux CUDA direct-link path (unchanged).
ifeq ($(CUDA),1) ifeq ($(CUDA),1)
ifeq ($(UNAME_S),Darwin) ifeq ($(UNAME_S),Darwin)
$(error CUDA=1 is supported only on Linux) $(error CUDA=1 is supported only on Linux)
endif endif
ifneq ($(IS_WIN),) ifneq ($(IS_WIN),)
# GPU: stub only in Phase 1 (G0). G1 builds coli_cuda.dll with MSVC+nvcc. # On Windows use CUDA_DLL=1 (runtime DLL), not CUDA=1 (direct link).
$(error CUDA=1 on Windows requires G1: build coli_cuda.dll with MSVC+nvcc (see PORT_WINDOWS_PLAN.md §8)) $(error On Windows use: make CUDA_DLL=1 cuda-dll (see backend_loader.c))
endif endif
CFLAGS += -DCOLI_CUDA CFLAGS += -DCOLI_CUDA
LDFLAGS += -L$(CUDA_HOME)/lib64 -Wl,-rpath,$(CUDA_HOME)/lib64 -lcudart -lstdc++ LDFLAGS += -L$(CUDA_HOME)/lib64 -Wl,-rpath,$(CUDA_HOME)/lib64 -lcudart -lstdc++
@@ -113,6 +136,22 @@ glm: glm$(EXE)
glm$(EXE): glm.c st.h json.h tok.h tok_unicode.h compat.h grammar.h $(CUDA_OBJ) $(METAL_OBJ) glm$(EXE): glm.c st.h json.h tok.h tok_unicode.h compat.h grammar.h $(CUDA_OBJ) $(METAL_OBJ)
$(CC) $(CFLAGS) glm.c $(CUDA_OBJ) $(METAL_OBJ) -o glm$(EXE) $(LDFLAGS) $(CC) $(CFLAGS) glm.c $(CUDA_OBJ) $(METAL_OBJ) -o glm$(EXE) $(LDFLAGS)
# Windows runtime loader object: resolves coli_cuda_* from coli_cuda.dll.
backend_loader.o: backend_loader.c backend_cuda.h compat.h
$(CC) $(CFLAGS) -c backend_loader.c -o $@
# Windows CUDA DLL: compile backend_cuda.cu with nvcc (+MSVC cl.exe as host
# compiler, required by nvcc on Windows) into coli_cuda.dll. Run this from a
# shell that has the MSVC environment set (e.g. after vcvars64.bat, or from a
# "x64 Native Tools Command Prompt"). COLI_CUDA_BUILDING_DLL enables
# __declspec(dllexport) so the 11 API symbols are exported.
cuda-dll: backend_cuda.cu backend_cuda.h
@command -v $(NVCC) >/dev/null 2>&1 || { echo "nvcc not found: set CUDA_HOME or NVCC" >&2; exit 1; }
@command -v cl >/dev/null 2>&1 || { echo "cl.exe (MSVC) not in PATH — run vcvars64.bat first" >&2; exit 1; }
$(NVCC) $(NVCCFLAGS) -shared -DCOLI_CUDA_BUILDING_DLL \
-L"$(CUDA_HOME)/lib/x64" -lcudart \
backend_cuda.cu -o coli_cuda.dll
backend_cuda.o: backend_cuda.cu backend_cuda.h backend_cuda.o: backend_cuda.cu backend_cuda.h
@command -v $(NVCC) >/dev/null 2>&1 || { echo "nvcc not found: set CUDA_HOME or NVCC" >&2; exit 1; } @command -v $(NVCC) >/dev/null 2>&1 || { echo "nvcc not found: set CUDA_HOME or NVCC" >&2; exit 1; }
$(NVCC) $(NVCCFLAGS) -c backend_cuda.cu -o $@ $(NVCC) $(NVCCFLAGS) -c backend_cuda.cu -o $@
@@ -180,7 +219,7 @@ check:
$(MAKE) test $(MAKE) test
clean: clean:
rm -f olmoe$(EXE) glm$(EXE) iobench$(EXE) backend_cuda.o backend_cuda_test$(EXE) backend_cuda_bench$(EXE) backend_metal.o backend_metal_test $(TEST_BINS) rm -f olmoe$(EXE) glm$(EXE) iobench$(EXE) backend_cuda.o backend_loader.o backend_cuda_test$(EXE) backend_cuda_bench$(EXE) backend_metal.o backend_metal_test coli_cuda.dll coli_cuda.lib coli_cuda.exp $(TEST_BINS)
rm -rf tests/__pycache__ rm -rf tests/__pycache__
.PHONY: all glm cuda-test cuda-bench portable test-c test-python test check clean .PHONY: all glm cuda-test cuda-bench cuda-dll portable test-c test-python test check clean
+25 -15
View File
@@ -4,6 +4,16 @@
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
/* COLI_CUDA_DLLEXPORT marks functions exported from coli_cuda.dll on Windows.
* Define COLI_CUDA_BUILDING_DLL when compiling the .cu into the DLL (so the
* functions are __declspec(dllexport)); the host loader does NOT include this
* header's declarations — it resolves symbols at runtime via GetProcAddress. */
#if defined(_WIN32) && defined(COLI_CUDA_BUILDING_DLL)
#define COLI_CUDA_DLLEXPORT __declspec(dllexport)
#else
#define COLI_CUDA_DLLEXPORT
#endif
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
@@ -14,18 +24,18 @@ extern "C" {
typedef struct ColiCudaTensor ColiCudaTensor; typedef struct ColiCudaTensor ColiCudaTensor;
/* Devices are CUDA ordinals, not positions in the input list. */ /* Devices are CUDA ordinals, not positions in the input list. */
int coli_cuda_init(const int *devices, int count); COLI_CUDA_DLLEXPORT int coli_cuda_init(const int *devices, int count);
void coli_cuda_shutdown(void); COLI_CUDA_DLLEXPORT void coli_cuda_shutdown(void);
int coli_cuda_device_count(void); COLI_CUDA_DLLEXPORT int coli_cuda_device_count(void);
int coli_cuda_device_at(int index); COLI_CUDA_DLLEXPORT int coli_cuda_device_at(int index);
int coli_cuda_mem_info(int device, size_t *free_bytes, size_t *total_bytes); COLI_CUDA_DLLEXPORT int coli_cuda_mem_info(int device, size_t *free_bytes, size_t *total_bytes);
/* device < 0 returns aggregate statistics for all configured devices. */ /* device < 0 returns aggregate statistics for all configured devices. */
void coli_cuda_stats(int device, size_t *tensor_count, size_t *tensor_bytes); COLI_CUDA_DLLEXPORT void coli_cuda_stats(int device, size_t *tensor_count, size_t *tensor_bytes);
void coli_cuda_group_stats(uint64_t *calls, uint64_t *experts, uint64_t *rows, COLI_CUDA_DLLEXPORT void coli_cuda_group_stats(uint64_t *calls, uint64_t *experts, uint64_t *rows,
double *h2d_ms, double *kernel_ms, double *d2h_ms); double *h2d_ms, double *kernel_ms, double *d2h_ms);
/* Upload without executing, so capacity failures happen during model startup. */ /* Upload without executing, so capacity failures happen during model startup. */
int coli_cuda_tensor_upload(ColiCudaTensor **tensor, COLI_CUDA_DLLEXPORT int coli_cuda_tensor_upload(ColiCudaTensor **tensor,
const void *weights, const float *scales, const void *weights, const float *scales,
int fmt, int I, int O, int device); int fmt, int I, int O, int device);
@@ -35,7 +45,7 @@ int coli_cuda_tensor_upload(ColiCudaTensor **tensor,
* The first successful call uploads W and its row scales; later calls reuse it. * The first successful call uploads W and its row scales; later calls reuse it.
* Returns 1 on success and 0 when CUDA is not initialized or the format is invalid. * Returns 1 on success and 0 when CUDA is not initialized or the format is invalid.
*/ */
int coli_cuda_matmul(ColiCudaTensor **tensor, COLI_CUDA_DLLEXPORT int coli_cuda_matmul(ColiCudaTensor **tensor,
float *y, const float *x, float *y, const float *x,
const void *weights, const float *scales, const void *weights, const float *scales,
int fmt, int S, int I, int O, int device); int fmt, int S, int I, int O, int device);
@@ -43,25 +53,25 @@ int coli_cuda_matmul(ColiCudaTensor **tensor,
/* Fused expert pipeline: y = down(silu(gate(x)) * up(x)). All three tensors /* Fused expert pipeline: y = down(silu(gate(x)) * up(x)). All three tensors
* must already be resident on one device. Activations cross PCIe once in * must already be resident on one device. Activations cross PCIe once in
* each direction instead of once per matrix. */ * each direction instead of once per matrix. */
int coli_cuda_expert_mlp(ColiCudaTensor *gate, ColiCudaTensor *up, COLI_CUDA_DLLEXPORT int coli_cuda_expert_mlp(ColiCudaTensor *gate, ColiCudaTensor *up,
ColiCudaTensor *down, float *y, const float *x, int S); ColiCudaTensor *down, float *y, const float *x, int S);
/* Packed group of same-shaped experts. Inputs and outputs contain sum(rows) /* Packed group of same-shaped experts. Inputs and outputs contain sum(rows)
* consecutive [D] rows in call order. */ * consecutive [D] rows in call order. */
int coli_cuda_expert_group(ColiCudaTensor *const *gates, COLI_CUDA_DLLEXPORT int coli_cuda_expert_group(ColiCudaTensor *const *gates,
ColiCudaTensor *const *ups, ColiCudaTensor *const *ups,
ColiCudaTensor *const *downs, ColiCudaTensor *const *downs,
const int *rows, int count, const int *rows, int count,
float *y, const float *x); float *y, const float *x);
/* Decode-only MLA weight-absorption core for one token. kv_b is [H*(Q+V),K]. */ /* Decode-only MLA weight-absorption core for one token. kv_b is [H*(Q+V),K]. */
int coli_cuda_attention_absorb(ColiCudaTensor *kv_b,float *ctx,const float *q, COLI_CUDA_DLLEXPORT int coli_cuda_attention_absorb(ColiCudaTensor *kv_b,float *ctx,const float *q,
const float *latent,const float *rope,int H,int Q, const float *latent,const float *rope,int H,int Q,
int R,int V,int K,int T,float attention_scale); int R,int V,int K,int T,float attention_scale);
void coli_cuda_tensor_free(ColiCudaTensor *tensor); COLI_CUDA_DLLEXPORT void coli_cuda_tensor_free(ColiCudaTensor *tensor);
size_t coli_cuda_tensor_bytes(const ColiCudaTensor *tensor); COLI_CUDA_DLLEXPORT size_t coli_cuda_tensor_bytes(const ColiCudaTensor *tensor);
int coli_cuda_tensor_device(const ColiCudaTensor *tensor); COLI_CUDA_DLLEXPORT int coli_cuda_tensor_device(const ColiCudaTensor *tensor);
#ifdef __cplusplus #ifdef __cplusplus
} }
+219
View File
@@ -0,0 +1,219 @@
/* backend_loader.c — Windows runtime loader for coli_cuda.dll.
*
* Why this exists: the engine is built with MinGW-w64 (gcc), but CUDA kernels
* must be compiled with MSVC + nvcc. We cannot link a CUDA .o into a gcc binary
* reliably across the MSVC/MinGW ABI, and nvcc requires cl.exe as its host
* compiler. The clean cross-toolchain split is: build the CUDA backend into a
* standalone coli_cuda.dll with nvcc+MSVC, then load it here at runtime via
* LoadLibrary/GetProcAddress. The host (glm.exe) never links cudart directly.
*
* On Linux this file is not compiled (the Makefile links backend_cuda.o
* directly). On Windows, when COLI_CUDA is defined, glm.c calls the
* coli_cuda_* wrappers below, which forward through function pointers resolved
* from the DLL at first use. If the DLL is absent, every call safely returns
* the "not initialized" sentinel (0 / no-op) and the engine falls back to CPU.
*
* ABI note: ColiCudaTensor* is opaque to the host (it stores the pointer,
* never dereferences it), so the MSVC-allocated struct is safe to pass across
* the boundary as an opaque handle. All scalar types (int, size_t, pointers)
* agree between MSVC and MinGW-w64 on x86-64.
*/
#ifdef _WIN32
#include <stdio.h>
#include <stddef.h>
#include <windows.h>
#include "backend_cuda.h"
/* Function-pointer typedefs matching each exported symbol. */
typedef int (*fn_init)(const int *devices, int count);
typedef void (*fn_shutdown)(void);
typedef int (*fn_device_count)(void);
typedef int (*fn_device_at)(int index);
typedef int (*fn_mem_info)(int device, size_t *free_bytes, size_t *total_bytes);
typedef void (*fn_stats)(int device, size_t *tensor_count, size_t *tensor_bytes);
typedef void (*fn_group_stats)(uint64_t *calls, uint64_t *experts, uint64_t *rows,
double *h2d_ms, double *kernel_ms, double *d2h_ms);
typedef int (*fn_expert_mlp)(ColiCudaTensor *gate, ColiCudaTensor *up,
ColiCudaTensor *down, float *y, const float *x, int S);
typedef int (*fn_expert_group)(ColiCudaTensor *const *gates, ColiCudaTensor *const *ups,
ColiCudaTensor *const *downs, const int *rows, int count,
float *y, const float *x);
typedef int (*fn_attention_absorb)(ColiCudaTensor *kv_b, float *ctx, const float *q,
const float *latent, const float *rope, int H, int Q,
int R, int V, int K, int T, float attention_scale);
typedef int (*fn_tensor_upload)(ColiCudaTensor **tensor, const void *weights,
const float *scales, int fmt, int I, int O, int device);
typedef int (*fn_matmul)(ColiCudaTensor **tensor, float *y, const float *x,
const void *weights, const float *scales,
int fmt, int S, int I, int O, int device);
typedef void (*fn_tensor_free)(ColiCudaTensor *tensor);
typedef size_t (*fn_tensor_bytes)(const ColiCudaTensor *tensor);
typedef int (*fn_tensor_device)(const ColiCudaTensor *tensor);
/* Resolved pointers, plus a flag so we attempt the load at most once. */
static struct {
int loaded; /* 1 = load attempted (success or fail), 0 = not yet */
int available; /* 1 = DLL loaded and all symbols resolved */
HMODULE dll;
fn_init init;
fn_shutdown shutdown;
fn_device_count device_count;
fn_device_at device_at;
fn_mem_info mem_info;
fn_stats stats;
fn_group_stats group_stats;
fn_expert_mlp expert_mlp;
fn_expert_group expert_group;
fn_attention_absorb attention_absorb;
fn_tensor_upload tensor_upload;
fn_matmul matmul;
fn_tensor_free tensor_free;
fn_tensor_bytes tensor_bytes;
fn_tensor_device tensor_device;
} g_cuda;
/* Resolve the DLL and all 11 symbols. Returns 1 on success, 0 otherwise.
* Idempotent: the first call (success or fail) sticks; later calls are no-ops
* that return the cached result. The engine treats a 0 return as "CUDA
* unavailable" and falls back to the CPU path without aborting. */
static int coli_cuda_load(void){
if(g_cuda.loaded) return g_cuda.available;
g_cuda.loaded = 1;
/* Search the model directory first (so a DLL shipped next to the model
* wins), then the engine directory, then the default DLL search path. */
g_cuda.dll = LoadLibraryA("coli_cuda.dll");
if(!g_cuda.dll){
fprintf(stderr, "[CUDA] coli_cuda.dll not found; GPU tier disabled "
"(CPU path remains active).\n");
return 0;
}
#define RESOLVE(name, type) \
/* GetProcAddress returns FARPROC (void(*)(void)); casting it to a \
* specific function-pointer type is the standard LoadLibrary idiom. \
* -Wcast-function-type flags it but it is safe: the DLL exported \
* the symbol with extern "C" and the exact signature we expect. */ \
_Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Wcast-function-type\"") \
g_cuda.name = (type)GetProcAddress(g_cuda.dll, "coli_cuda_" #name); \
_Pragma("GCC diagnostic pop") \
if(!g_cuda.name){ \
fprintf(stderr, "[CUDA] coli_cuda.dll missing symbol coli_cuda_" #name "\n"); \
FreeLibrary(g_cuda.dll); g_cuda.dll=NULL; return 0; }
RESOLVE(init, fn_init)
RESOLVE(shutdown, fn_shutdown)
RESOLVE(device_count, fn_device_count)
RESOLVE(device_at, fn_device_at)
RESOLVE(mem_info, fn_mem_info)
RESOLVE(stats, fn_stats)
RESOLVE(group_stats, fn_group_stats)
RESOLVE(expert_mlp, fn_expert_mlp)
RESOLVE(expert_group, fn_expert_group)
RESOLVE(attention_absorb, fn_attention_absorb)
RESOLVE(tensor_upload, fn_tensor_upload)
RESOLVE(matmul, fn_matmul)
RESOLVE(tensor_free, fn_tensor_free)
RESOLVE(tensor_bytes, fn_tensor_bytes)
RESOLVE(tensor_device, fn_tensor_device)
#undef RESOLVE
g_cuda.available = 1;
return 1;
}
/* ---- Public wrappers: match backend_cuda.h signatures exactly.
* Each forwards to the resolved pointer; if the DLL never loaded, return the
* "not initialized" result the engine already handles (init returns 0, matmul
* returns 0 so the caller marks the tensor cuda_failed and uses CPU). ---- */
int coli_cuda_init(const int *devices, int count){
if(!coli_cuda_load()) return 0;
return g_cuda.init(devices, count);
}
void coli_cuda_shutdown(void){
if(g_cuda.available && g_cuda.shutdown) g_cuda.shutdown();
}
int coli_cuda_device_count(void){
if(!g_cuda.available) return 0;
return g_cuda.device_count();
}
int coli_cuda_device_at(int index){
if(!g_cuda.available) return -1;
return g_cuda.device_at(index);
}
int coli_cuda_mem_info(int device, size_t *free_bytes, size_t *total_bytes){
if(!g_cuda.available){ if(free_bytes)*free_bytes=0; if(total_bytes)*total_bytes=0; return 0; }
return g_cuda.mem_info(device, free_bytes, total_bytes);
}
void coli_cuda_stats(int device, size_t *tensor_count, size_t *tensor_bytes){
if(!g_cuda.available){ if(tensor_count)*tensor_count=0; if(tensor_bytes)*tensor_bytes=0; return; }
g_cuda.stats(device, tensor_count, tensor_bytes);
}
void coli_cuda_group_stats(uint64_t *calls, uint64_t *experts, uint64_t *rows,
double *h2d_ms, double *kernel_ms, double *d2h_ms){
if(!g_cuda.available){
if(calls)*calls=0; if(experts)*experts=0; if(rows)*rows=0;
if(h2d_ms)*h2d_ms=0; if(kernel_ms)*kernel_ms=0; if(d2h_ms)*d2h_ms=0;
return;
}
g_cuda.group_stats(calls, experts, rows, h2d_ms, kernel_ms, d2h_ms);
}
int coli_cuda_expert_mlp(ColiCudaTensor *gate, ColiCudaTensor *up,
ColiCudaTensor *down, float *y, const float *x, int S){
if(!g_cuda.available) return 0;
return g_cuda.expert_mlp(gate, up, down, y, x, S);
}
int coli_cuda_expert_group(ColiCudaTensor *const *gates, ColiCudaTensor *const *ups,
ColiCudaTensor *const *downs, const int *rows, int count,
float *y, const float *x){
if(!g_cuda.available) return 0;
return g_cuda.expert_group(gates, ups, downs, rows, count, y, x);
}
int coli_cuda_attention_absorb(ColiCudaTensor *kv_b, float *ctx, const float *q,
const float *latent, const float *rope, int H, int Q,
int R, int V, int K, int T, float attention_scale){
if(!g_cuda.available) return 0;
return g_cuda.attention_absorb(kv_b, ctx, q, latent, rope, H, Q, R, V, K, T, attention_scale);
}
int coli_cuda_tensor_upload(ColiCudaTensor **tensor, const void *weights,
const float *scales, int fmt, int I, int O, int device){
if(!g_cuda.available) return 0;
return g_cuda.tensor_upload(tensor, weights, scales, fmt, I, O, device);
}
int coli_cuda_matmul(ColiCudaTensor **tensor, float *y, const float *x,
const void *weights, const float *scales,
int fmt, int S, int I, int O, int device){
if(!g_cuda.available) return 0;
return g_cuda.matmul(tensor, y, x, weights, scales, fmt, S, I, O, device);
}
void coli_cuda_tensor_free(ColiCudaTensor *tensor){
if(g_cuda.available && g_cuda.tensor_free) g_cuda.tensor_free(tensor);
}
size_t coli_cuda_tensor_bytes(const ColiCudaTensor *tensor){
if(!g_cuda.available) return 0;
return g_cuda.tensor_bytes(tensor);
}
int coli_cuda_tensor_device(const ColiCudaTensor *tensor){
if(!g_cuda.available) return -1;
return g_cuda.tensor_device(tensor);
}
#endif /* _WIN32 */
+26 -1
View File
@@ -383,13 +383,38 @@ def cmd_chat(a):
banner(f"chat · {os.path.basename(a.model)} · ram {a.ram or '-'}GB · topp {a.topp or 'off'}") banner(f"chat · {os.path.basename(a.model)} · ram {a.ram or '-'}GB · topp {a.topp or 'off'}")
errlog=tempfile.NamedTemporaryFile(mode="w+", suffix=".log", delete=False) errlog=tempfile.NamedTemporaryFile(mode="w+", suffix=".log", delete=False)
e=env_for(a); e["SERVE"]="1" e=env_for(a); e["SERVE"]="1"
# stderr -> PIPE, NOT stderr=errlog (file). On Windows/MinGW, pointing the
# child's stderr at a file/DEVNULL handle stalls the CRT so stdout (the byte
# protocol coli reads one byte at a time) never flushes and chat hangs at
# ~10 GB resident. A PIPE whose read end nobody drains still works: the
# engine emits only ~400 bytes of status to stderr, which fits comfortably
# in the OS pipe buffer, so it never blocks. We snapshot stderr into errlog
# once the READY sentinel arrives, so the status-line display below works
# exactly as before. (Do NOT add a concurrent stderr drain thread: on
# Windows, reading two child pipes simultaneously deadlocks CPython's IO.)
p=subprocess.Popen([GLM,str(a.cap)], env=e, stdin=subprocess.PIPE, p=subprocess.Popen([GLM,str(a.cap)], env=e, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=errlog, bufsize=0) stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0)
sp=Spinner("waking the giant (744B)…"); sp.start() sp=Spinner("waking the giant (744B)…"); sp.start()
st=stream_turn(p, READY, lambda b: None) st=stream_turn(p, READY, lambda b: None)
sp.stop() sp.stop()
if st is None: if st is None:
try: errlog.write(p.stderr.read().decode("utf-8","replace"))
except (OSError, ValueError): pass
errlog.seek(0); print(errlog.read()[-1500:]); sys.exit("the engine exited while loading") errlog.seek(0); print(errlog.read()[-1500:]); sys.exit("the engine exited while loading")
# READY received. Drain the child's stderr into errlog without blocking:
# the engine is still alive (blocked on stdin), so a plain read() would
# hang forever waiting for EOF. A short bounded drain grabs the ~400 bytes
# of load-time status ([RAM_GB], [MTP], ...) that were already emitted.
_drain_box={"done":False}
def _drain():
try: errlog.write(p.stderr.read().decode("utf-8","replace"))
except (OSError, ValueError): pass
_drain_box["done"]=True
threading.Thread(target=_drain, daemon=True).start()
_drain_box["th"]=threading.current_thread()
for _ in range(20): # up to ~1s for the load-status lines
if _drain_box["done"]: break
time.sleep(0.05)
errlog.flush() errlog.flush()
try: try:
elog=open(errlog.name).read() elog=open(errlog.name).read()
+71 -2
View File
@@ -27,7 +27,9 @@
#include <stdatomic.h> /* PIPE ready-flags/job queue + PILOT_REAL cross-layer handshake */ #include <stdatomic.h> /* PIPE ready-flags/job queue + PILOT_REAL cross-layer handshake */
#include <sched.h> /* sched_yield: PIPE spin / PILOT barrier */ #include <sched.h> /* sched_yield: PIPE spin / PILOT barrier */
#include <unistd.h> #include <unistd.h>
#include <sys/select.h> #if defined(__APPLE__) || defined(__linux__)
#include <sys/select.h> /* select() serve-loop polling (#68); not on native MinGW */
#endif
#if defined(__APPLE__) || defined(__linux__) #if defined(__APPLE__) || defined(__linux__)
#include <sys/resource.h> #include <sys/resource.h>
#include <sys/mman.h> /* mlock: inchioda le pagine in RAM / wire pages into RAM */ #include <sys/mman.h> /* mlock: inchioda le pagine in RAM / wire pages into RAM */
@@ -439,6 +441,8 @@ static void matmul_i2(float *y, const float *x, const uint8_t *q2, const float *
* RMS per matmul (attivazione int8), IDOT=0 torna al percorso f32 esatto. */ * RMS per matmul (attivazione int8), IDOT=0 torna al percorso f32 esatto. */
#if defined(__AVX512VNNI__) && defined(__AVX512BW__) #if defined(__AVX512VNNI__) && defined(__AVX512BW__)
#define IDOT_KERNEL "avx512-vnni" #define IDOT_KERNEL "avx512-vnni"
#elif defined(__AVXVNNI__) && defined(__AVX2__)
#define IDOT_KERNEL "avx-vnni"
#elif defined(__AVX2__) #elif defined(__AVX2__)
#define IDOT_KERNEL "avx2" #define IDOT_KERNEL "avx2"
#elif defined(__ARM_NEON) #elif defined(__ARM_NEON)
@@ -475,6 +479,12 @@ static inline int hsum256_i32(__m256i v){
return _mm_cvtsi128_si32(lo); return _mm_cvtsi128_si32(lo);
} }
#endif #endif
#if defined(__AVXVNNI__) && defined(__AVX2__)
/* hsum di un __m128i a 4 lane s32 (l'AVX-VNNI 128-bit accumula su 4 lane). */
static inline int hsum128_i32(__m128i v){
v=_mm_hadd_epi32(v,v); v=_mm_hadd_epi32(v,v); return _mm_cvtsi128_si32(v);
}
#endif
/* dot int8·int8: trucco del segno (|w| unsigned × x·sign(w) signed). Sicuro: /* dot int8·int8: trucco del segno (|w| unsigned × x·sign(w) signed). Sicuro:
* coppie <= 128*127*2 = 32512 < 32767, accumulo s32 fino a I=16384. */ * coppie <= 128*127*2 = 32512 < 32767, accumulo s32 fino a I=16384. */
static inline int32_t dot_i8i8(const int8_t *w, const int8_t *x, int I){ static inline int32_t dot_i8i8(const int8_t *w, const int8_t *x, int I){
@@ -493,6 +503,18 @@ static inline int32_t dot_i8i8(const int8_t *w, const int8_t *x, int I){
acc=_mm512_dpbusd_epi32(acc,_mm512_abs_epi8(wv),xs); acc=_mm512_dpbusd_epi32(acc,_mm512_abs_epi8(wv),xs);
} }
sum=_mm512_reduce_add_epi32(acc); sum=_mm512_reduce_add_epi32(acc);
#elif defined(__AVXVNNI__) && defined(__AVX2__)
/* AVX-VNNI 128-bit: vpdpbusd u8*s8 -> s32, 16 byte/iter. Stesso trucco del
* segno della variante 512-bit: |w| via abs, segno piegato in x con maschera
* (w==0 -> product 0). __AVX2__ serve per _mm_sign_epi8 / abs. */
__m128i acc=_mm_setzero_si128();
for(;i+16<=I;i+=16){
__m128i wv=_mm_loadu_si128((const __m128i*)(w+i));
__m128i xv=_mm_loadu_si128((const __m128i*)(x+i));
__m128i xs=_mm_sign_epi8(xv,wv); /* x * sign(w); _mm_sign zona __AVX2__ */
acc=_mm_dpbusd_epi32(acc,_mm_abs_epi8(wv),xs);
}
sum=hsum128_i32(acc);
#elif defined(__AVX2__) #elif defined(__AVX2__)
__m256i acc=_mm256_setzero_si256(); const __m256i ones=_mm256_set1_epi16(1); __m256i acc=_mm256_setzero_si256(); const __m256i ones=_mm256_set1_epi16(1);
for(;i+32<=I;i+=32){ for(;i+32<=I;i+=32){
@@ -563,6 +585,23 @@ static inline int32_t dot_i4i8(const uint8_t *w4, const int8_t *x, int I){
acc=_mm512_dpbusd_epi32(acc,_mm512_abs_epi8(wv),xs); acc=_mm512_dpbusd_epi32(acc,_mm512_abs_epi8(wv),xs);
} }
sum=_mm512_reduce_add_epi32(acc); sum=_mm512_reduce_add_epi32(acc);
#elif defined(__AVXVNNI__) && defined(__AVX2__)
/* AVX-VNNI 128-bit, int4: 16 byte = 32 nibble -> int8 [-8,7] in due half
* (n0/n1), ciascuno alimentato a un vpdpbusd da 16 byte. Stesso unpack
* 128-bit del ramo AVX2 sotto; 32 elementi/iter come li. */
const __m128i m4=_mm_set1_epi8(0x0F); const __m128i b8=_mm_set1_epi8(8);
__m128i acc=_mm_setzero_si128();
for(;i+32<=I;i+=32){
__m128i by=_mm_loadu_si128((const __m128i*)(w4+(i>>1))); /* 16 byte = 32 nibble */
__m128i lo=_mm_and_si128(by,m4), hi=_mm_and_si128(_mm_srli_epi16(by,4),m4);
__m128i n0=_mm_unpacklo_epi8(lo,hi), n1=_mm_unpackhi_epi8(lo,hi); /* nibble in ordine */
__m128i w0=_mm_sub_epi8(n0,b8), w1=_mm_sub_epi8(n1,b8);
__m128i x0=_mm_loadu_si128((const __m128i*)(x+i));
__m128i x1=_mm_loadu_si128((const __m128i*)(x+i+16));
acc=_mm_dpbusd_epi32(acc,_mm_abs_epi8(w0),_mm_sign_epi8(x0,w0));
acc=_mm_dpbusd_epi32(acc,_mm_abs_epi8(w1),_mm_sign_epi8(x1,w1));
}
sum=hsum128_i32(acc);
#elif defined(__AVX2__) #elif defined(__AVX2__)
const __m128i m4=_mm_set1_epi8(0x0F); const __m256i b8=_mm256_set1_epi8(8); const __m128i m4=_mm_set1_epi8(0x0F); const __m256i b8=_mm256_set1_epi8(8);
const __m256i ones=_mm256_set1_epi16(1); const __m256i ones=_mm256_set1_epi16(1);
@@ -1128,7 +1167,9 @@ static pthread_mutex_t g_map_mtx = PTHREAD_MUTEX_INITIALIZER; /* expert_load e
static void *map_of_fd(int fd){ static void *map_of_fd(int fd){
pthread_mutex_lock(&g_map_mtx); pthread_mutex_lock(&g_map_mtx);
for(int i=0;i<g_nmaps;i++) if(g_maps[i].fd==fd){ void *b=g_maps[i].base; pthread_mutex_unlock(&g_map_mtx); return b; } for(int i=0;i<g_nmaps;i++) if(g_maps[i].fd==fd){ void *b=g_maps[i].base; pthread_mutex_unlock(&g_map_mtx); return b; }
void *base=NULL; struct stat st; void *base=NULL;
#if defined(__APPLE__) || defined(__linux__)
struct stat st;
if(g_nmaps<512 && fstat(fd,&st)==0){ if(g_nmaps<512 && fstat(fd,&st)==0){
size_t len=((size_t)st.st_size+16383)&~(size_t)16383; size_t len=((size_t)st.st_size+16383)&~(size_t)16383;
void *p=mmap(NULL,len,PROT_READ,MAP_SHARED,fd,0); void *p=mmap(NULL,len,PROT_READ,MAP_SHARED,fd,0);
@@ -1139,6 +1180,7 @@ static void *map_of_fd(int fd){
#endif #endif
} }
} }
#endif
pthread_mutex_unlock(&g_map_mtx); pthread_mutex_unlock(&g_map_mtx);
return base; return base;
} }
@@ -1198,7 +1240,9 @@ static int expert_load(Model *m, int layer, int eid, ESlot *s, int fatal){
* residency. This is pread's I/O without the copy and without the slab. */ * residency. This is pread's I/O without the copy and without the slab. */
for(int k=0;k<3;k++){ for(int k=0;k<3;k++){
char *p=(char*)bw[k]+tw[k]->off; size_t n=(size_t)tw[k]->nbytes; char *p=(char*)bw[k]+tw[k]->off; size_t n=(size_t)tw[k]->nbytes;
#if defined(__APPLE__) || defined(__linux__)
madvise((void*)((uintptr_t)p & ~16383UL), n+16384, MADV_WILLNEED); madvise((void*)((uintptr_t)p & ~16383UL), n+16384, MADV_WILLNEED);
#endif
volatile char acc=0; volatile char acc=0;
for(size_t i=0;i<n;i+=4096) acc+=p[i]; for(size_t i=0;i<n;i+=4096) acc+=p[i];
acc+=p[n-1]; (void)acc; acc+=p[n-1]; (void)acc;
@@ -3082,6 +3126,7 @@ static int mux_submit(Model *m, Tok *T, ServeCtx *ctx, ServeReq *req, int nctx,
} }
static void run_serve_mux(Model *m, const char *snap){ static void run_serve_mux(Model *m, const char *snap){
#if defined(__APPLE__) || defined(__linux__)
char tkp[2048]; snprintf(tkp,sizeof(tkp),"%s/tokenizer.json",snap); char tkp[2048]; snprintf(tkp,sizeof(tkp),"%s/tokenizer.json",snap);
Tok T; tok_load(&T,tkp); int eos=tok_id_of(&T,"<|endoftext|>"); stops_arm(&m->c,eos); Tok T; tok_load(&T,tkp); int eos=tok_id_of(&T,"<|endoftext|>"); stops_arm(&m->c,eos);
g_draft=0; /* one scheduler owns every forward; MTP/speculation is not ragged-safe */ g_draft=0; /* one scheduler owns every forward; MTP/speculation is not ragged-safe */
@@ -3123,9 +3168,33 @@ static void run_serve_mux(Model *m, const char *snap){
usage_save(m); usage_save(m);
for(int i=0;i<nctx;i++) serve_ctx_free(m,&ctx[i]); free(ctx); free(req); for(int i=0;i<nctx;i++) serve_ctx_free(m,&ctx[i]); free(ctx); free(req);
m->kv=NULL; m->Lc=m->Rc=m->Ic=NULL; m->kv_start=NULL; m->max_t=0; m->kv=NULL; m->Lc=m->Rc=m->Ic=NULL; m->kv_start=NULL; m->max_t=0;
#else
/* SERVE_BATCH (continuous batching) uses select() on stdin, a Unix-ism.
* Not yet ported to native Windows — fall back to the single-sequence
* serve path (run_serve). Remove this stub once select()-free polling
* (e.g. WaitForSingleObject on the stdin handle) is implemented. */
(void)snap;
fprintf(stderr,"[SERVE_BATCH] continuous-batching serve is not yet available on "
"native Windows; use the default serve path (omit SERVE_BATCH).\n");
#endif
} }
static void run_serve(Model *m, const char *snap){ static void run_serve(Model *m, const char *snap){
/* Serve mode speaks a byte protocol over BOTH stdout and stdin:
* stdout: \x01\x01READY\x01\x01\n, STAT lines, \x01\x01END\x01\x01\n
* stdin: text lines plus \x02RESET / \x02MORE control bytes.
* 'coli' matches the sentinels with endswith() and a "^STAT ..." regex,
* so they must arrive byte-exact (LF, no CR). On Windows the CRT opens
* both handles in TEXT mode: stdout translates '\n'->'\r\n' (so the READY
* sentinel never matches and chat hangs at ~10 GB resident), and stdin
* translates '\r\n'->'\n' and rejects writes of raw bytes with EINVAL,
* breaking the control protocol. Put BOTH handles in BINARY mode so the
* protocol bytes are exact in both directions. No-op on Linux/macOS. */
#ifdef _WIN32
_setmode(_fileno(stdin), _O_BINARY);
_setmode(_fileno(stdout), _O_BINARY);
setvbuf(stdout, NULL, _IONBF, 0);
#endif
char tkp[2048]; snprintf(tkp,sizeof(tkp),"%s/tokenizer.json",snap); char tkp[2048]; snprintf(tkp,sizeof(tkp),"%s/tokenizer.json",snap);
Tok T; tok_load(&T,tkp); Tok T; tok_load(&T,tkp);
int eos=tok_id_of(&T,"<|endoftext|>"); int eos=tok_id_of(&T,"<|endoftext|>");
+37 -1
View File
@@ -7,6 +7,7 @@ import re
import shutil import shutil
import statistics import statistics
import subprocess import subprocess
import sys
from pathlib import Path from pathlib import Path
@@ -76,11 +77,46 @@ def analyze_model(model):
def memory_available(): def memory_available():
# Linux (and MSYS2/Git-Bash CPython where /proc exists): MemAvailable.
try: try:
text = Path("/proc/meminfo").read_text() text = Path("/proc/meminfo").read_text()
return int(re.search(r"MemAvailable:\s+(\d+)", text).group(1)) * 1024 return int(re.search(r"MemAvailable:\s+(\d+)", text).group(1)) * 1024
except (OSError, AttributeError): except (OSError, AttributeError):
return 0 pass
# Windows native CPython: GlobalMemoryStatusEx -> ullAvailPhys.
# Same definition the C engine uses (compat_meminfo in compat.h):
# standby/free/zero pages, i.e. reclaimable without swapping.
if sys.platform == "win32":
try:
import ctypes
class MEMORYSTATUSEX(ctypes.Structure):
_fields_ = [("dwLength", ctypes.c_ulong),
("dwMemoryLoad", ctypes.c_ulong),
("ullTotalPhys", ctypes.c_ulonglong),
("ullAvailPhys", ctypes.c_ulonglong),
("ullTotalVirtual", ctypes.c_ulonglong),
("ullAvailVirtual", ctypes.c_ulonglong),
("ullAvailExtendedVirtual", ctypes.c_ulonglong)]
stat = MEMORYSTATUSEX(dwLength=ctypes.sizeof(MEMORYSTATUSEX))
kernel32 = ctypes.windll.kernel32
kernel32.GlobalMemoryStatusEx.argtypes = [ctypes.c_void_p]
kernel32.GlobalMemoryStatusEx.restype = ctypes.c_int
if kernel32.GlobalMemoryStatusEx(ctypes.byref(stat)) and stat.ullAvailPhys:
return stat.ullAvailPhys
# Fallback (e.g. sandboxed callers where GlobalMemoryStatusEx reports
# nothing): total installed RAM in KB. Less precise than ullAvailPhys
# — it ignores standby/reclaimable pages — but never returns 0 on a
# real machine, which keeps the expert cache from being mis-sized.
total_kb = ctypes.c_ulonglong(0)
kernel32.GetPhysicallyInstalledSystemMemory.argtypes = [ctypes.c_void_p]
kernel32.GetPhysicallyInstalledSystemMemory.restype = ctypes.c_int
if kernel32.GetPhysicallyInstalledSystemMemory(ctypes.byref(total_kb)):
return total_kb.value * 1024
except OSError:
pass
return 0
def discover_gpus(): def discover_gpus():
+14 -1
View File
@@ -6,7 +6,14 @@ import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
from resource_plan import GB, analyze_model, build_plan, environment_for_plan, format_plan from resource_plan import (
GB,
analyze_model,
build_plan,
environment_for_plan,
format_plan,
memory_available,
)
def write_shard(path, tensors): def write_shard(path, tensors):
@@ -53,6 +60,12 @@ class ResourcePlanTest(unittest.TestCase):
self.assertEqual(info["expert_count"], 2) self.assertEqual(info["expert_count"], 2)
self.assertEqual(info["per_cap_bytes"], 60) self.assertEqual(info["per_cap_bytes"], 60)
def test_memory_available_is_positive(self):
# Regression: on native Windows CPython, /proc/meminfo does not exist,
# so the Linux-only path returned 0 and the expert cache was sized to
# 0 slots/layer. The value must be a sane positive number of bytes.
self.assertGreater(memory_available(), 0)
def test_builds_bounded_three_tier_plan(self): def test_builds_bounded_three_tier_plan(self):
gpus = [{"index": 0, "name": "test-gpu", "total_bytes": 12 * GB, gpus = [{"index": 0, "name": "test-gpu", "total_bytes": 12 * GB,
"free_bytes": 10 * GB}] "free_bytes": 10 * GB}]
+146
View File
@@ -0,0 +1,146 @@
# warmup.ps1 - overnight expert-cache warmup for colibri
#
# Runs `coli run` in a loop with diverse prompts so the engine records which
# routed experts your workload actually uses into .coli_usage. At startup the
# engine pins the hottest experts into RAM; the more history it has, the bigger
# and more accurate that pin gets. This does NOT load random experts - it loads
# whatever the model actually routes to for these prompts, then promotes the
# frequent ones.
#
# Usage (from the c\ directory):
# .\warmup.ps1 # defaults: model next to repo, 3 rounds
# .\warmup.ps1 -Model D:\glm52_i4 -Rounds 10 -Ngen 400
#
# Let it run while you sleep. Each iteration logs selections count + hit rate.
# Ctrl-C is safe: each run saves usage atomically only on clean completion, so
# the file is never corrupted (but a killed mid-generation run saves nothing).
#
# Why diverse prompts? Expert routing is content-dependent. Coding prompts
# activate different experts than poetry or math. A spread of topics builds a
# general-purpose pin that helps whatever YOU ask later. If you only ever warm
# on one topic, the pin overfits to that topic.
param(
[string]$Model = (Resolve-Path (Join-Path $PSScriptRoot "..\glm52_i4")).Path,
[int]$Rounds = 3,
# Default 32 (not 500): on a cold QLC cache a 500-token run takes hours and
# a killed mid-generation run saves nothing (usage_save runs only on clean
# completion). 32 tokens finishes in ~5-10 min even cold, so usage saves
# frequently and the loop accumulates selections steadily overnight. Each
# 32-token prompt still records ~90k expert selections.
[int]$Ngen = 32,
[string]$Log = (Join-Path $PSScriptRoot "warmup.log")
)
# "Continue" (not "Stop"): the engine writes status to stderr, which "Stop"
# treats as a fatal error and aborts the whole warmup loop on every prompt.
$ErrorActionPreference = "Continue"
$Coli = Join-Path $PSScriptRoot "coli"
if (-not (Test-Path $Coli)) { Write-Error "coli not found at $Coli - run from the c\ directory"; exit 1 }
if (-not (Test-Path $Model)) { Write-Error "model not found at $Model"; exit 1 }
# Diverse prompts across domains - each touches a different expert distribution.
# Kept open-ended ("explain", "write", "list") so generation runs to NGEN tokens
# and routes through many experts rather than stopping early on a short answer.
$Prompts = @(
"Explain how a transformer neural network works, covering attention, feed-forward layers, and backpropagation in detail.",
"Write a Python function that implements quicksort with in-place partitioning, including comments explaining each step.",
"Describe the causes and major events of the French Revolution in chronological order.",
"What is the difference between TCP and UDP? Explain handshakes, reliability, and use cases.",
"Write a short story about a lighthouse keeper who discovers a message in a bottle.",
"Explain the theory of general relativity, including the equivalence principle and gravitational time dilation.",
"List and describe the major organ systems of the human body and their primary functions.",
"How does photosynthesis work? Explain the light-dependent reactions and the Calvin cycle.",
"Write a C program that reads a file line by line and counts word frequency using a hash table.",
"Summarize the plot of Shakespeare's Hamlet, act by act.",
"Explain the difference between supervised, unsupervised, and reinforcement learning with examples of each.",
"What causes climate change? Describe the greenhouse effect, carbon cycle, and major greenhouse gases.",
"Write a recipe for a classic French onion soup, with step-by-step instructions.",
"Describe how the internet works, from typing a URL to rendering a webpage, including DNS, TCP, HTTP, and browsers.",
"Explain database normalization, including first, second, and third normal forms with examples.",
"What is quantum entanglement? Explain it as if to a curious high school student.",
"Write a poem about the ocean and the passage of time.",
"Describe the water cycle, including evaporation, condensation, precipitation, and transpiration.",
"How do vaccines work? Explain the immune response, antibodies, and mRNA vaccine technology.",
"Explain the Big Bang theory and the evidence supporting it, including cosmic microwave background and redshift.",
"Write a Python class for a binary search tree with insert, search, and inorder traversal methods.",
"What are the major branches of philosophy? Describe epistemology, ethics, metaphysics, and logic.",
"Explain how a CPU executes an instruction, covering fetch, decode, execute, and writeback.",
"Describe the life cycle of a star, from protostar to main sequence to red giant and beyond.",
"How does public key cryptography work? Explain RSA, including key generation, encryption, and signing.",
"Write a dialogue between two characters debating whether artificial intelligence can be conscious.",
"Explain the economic concepts of supply and demand, elasticity, and market equilibrium.",
"What is CRISPR gene editing and how does it work? Explain Cas9, guide RNA, and applications.",
"Describe the major causes and consequences of World War I.",
"How does a compiler work? Explain lexing, parsing, semantic analysis, optimization, and code generation."
)
function Get-Selections {
$u = Join-Path $Model ".coli_usage"
if (-not (Test-Path $u)) { return 0 }
$tot = 0
Get-Content $u | ForEach-Object {
$p = $_ -split '\s+'
if ($p.Count -eq 3) { $tot += [int]$p[2] }
}
return $tot
}
$start = Get-Date
$baseline = Get-Selections
$line = "=" * 72
"$line" | Tee-Object -FilePath $Log -Append
"colibri warmup - started $start" | Tee-Object -FilePath $Log -Append
" model: $Model" | Tee-Object -FilePath $Log -Append
" rounds: $Rounds x $($Prompts.Count) prompts" | Tee-Object -FilePath $Log -Append
" ngen: $Ngen tokens/prompt" | Tee-Object -FilePath $Log -Append
" baseline: $baseline selections" | Tee-Object -FilePath $Log -Append
"$line" | Tee-Object -FilePath $Log -Append
$iter = 0
$total = $Rounds * $Prompts.Count
for ($r = 1; $r -le $Rounds; $r++) {
for ($i = 0; $i -lt $Prompts.Count; $i++) {
$iter++
$prompt = $Prompts[$i]
$now = Get-Date -Format "HH:mm:ss"
$sel = Get-Selections
$header = "[$now] round $r/$Rounds prompt {0,2}/$($Prompts.Count) (iter $iter/$total) selections: $sel" -f ($i+1)
$header | Tee-Object -FilePath $Log -Append
" prompt: $($prompt.Substring(0, [Math]::Min(70, $prompt.Length)))..." | Tee-Object -FilePath $Log -Append
$t0 = Get-Date
# coli run writes status to stderr (normal) and may exit non-zero on
# EOS-early; neither is a real failure for our purpose. Relax the
# error preference and collect ALL output streams so stderr text
# doesn't abort the loop.
$prev = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
$output = & python $Coli run --model $Model --ngen $Ngen $prompt 2>&1 |
Select-Object -Last 4
} catch {
$output = @(" (engine run threw: $($_.Exception.Message))")
}
$ErrorActionPreference = $prev
$elapsed = ((Get-Date) - $t0).TotalSeconds
$after = Get-Selections
$delta = $after - $sel
$output | ForEach-Object { " $_" | Tee-Object -FilePath $Log -Append }
" -> {0:N0}s, +{1} selections (now {2})" -f $elapsed, $delta, $after | Tee-Object -FilePath $Log -Append
"" | Tee-Object -FilePath $Log -Append
}
}
$end = Get-Date
$final = Get-Selections
$gain = $final - $baseline
$duration = ($end - $start).ToString("hh\:mm\:ss")
"$line" | Tee-Object -FilePath $Log -Append
"colibri warmup - finished $end" | Tee-Object -FilePath $Log -Append
" duration: $duration" | Tee-Object -FilePath $Log -Append
" selections: $baseline -> $final (+$gain)" | Tee-Object -FilePath $Log -Append
" next: python coli chat --model $Model" | Tee-Object -FilePath $Log -Append
"$line" | Tee-Object -FilePath $Log -Append