Skip to content

Commit b39c0bf

Browse files
committed
Inferencing stuff and tests
1 parent b030283 commit b39c0bf

6 files changed

Lines changed: 289 additions & 19 deletions

File tree

backend_service/ddtree.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,12 @@ def generate_ddtree_mlx(
409409
cycles += 1
410410
else:
411411
# ── DDTree path ──────────────────────────────────
412-
draft_logits_np = np.array(draft_logits[0].tolist(), dtype=np.float32)
412+
# Evaluate and convert MLX tensor directly to NumPy via the
413+
# buffer protocol — avoids the very slow .tolist() intermediate
414+
# Python list, which is a major bottleneck for large vocabularies
415+
# (150K+ tokens for Qwen models).
416+
mx.eval(draft_logits)
417+
draft_logits_np = np.array(draft_logits[0], dtype=np.float32)
413418

414419
node_token_ids, node_depths, parents, child_maps, visibility = \
415420
build_ddtree_tree(draft_logits_np, effective_budget)

backend_service/mlx_worker.py

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -835,7 +835,14 @@ def _generate_dflash(self, request: dict[str, Any]) -> dict[str, Any]:
835835

836836
gen_tokens = [int(token_id) for token_id in summary.get("generated_token_ids", [])]
837837
text = self.tokenizer.decode(gen_tokens).strip() if gen_tokens else ""
838-
text = _strip_thinking_tokens(text) if text else ""
838+
# Respect thinkingMode: only strip raw reasoning patterns when thinking
839+
# is enabled. XML <think> tags are always processed regardless.
840+
thinking_mode = request.get("thinkingMode") or "off"
841+
if text:
842+
think_filter = ThinkingTokenFilter(detect_raw_reasoning=(thinking_mode != "off"))
843+
result = think_filter.feed(text)
844+
flushed = think_filter.flush()
845+
text = f"{result.text}{flushed.text}".strip()
839846
if not text:
840847
text = "Generation completed without decoded text."
841848

@@ -911,7 +918,14 @@ def _generate_ddtree(self, request: dict[str, Any]) -> dict[str, Any]:
911918
# Decode output tokens
912919
gen_tokens = result["generated_tokens"]
913920
text = self.tokenizer.decode(gen_tokens).strip()
914-
text = _strip_thinking_tokens(text) if text else ""
921+
# Respect thinkingMode: only strip raw reasoning patterns when thinking
922+
# is enabled. XML <think> tags are always processed regardless.
923+
thinking_mode = request.get("thinkingMode") or "off"
924+
if text:
925+
think_filter = ThinkingTokenFilter(detect_raw_reasoning=(thinking_mode != "off"))
926+
filter_result = think_filter.feed(text)
927+
flushed = think_filter.flush()
928+
text = f"{filter_result.text}{flushed.text}".strip()
915929
if not text:
916930
text = "Generation completed without decoded text."
917931

@@ -1055,7 +1069,12 @@ def _generate_standard(self, request: dict[str, Any]) -> dict[str, Any]:
10551069
)
10561070

10571071
raw_text = "".join(text_parts).strip()
1058-
text = _strip_thinking_tokens(raw_text)
1072+
# Respect thinkingMode: only strip raw reasoning when thinking is on.
1073+
thinking_mode = request.get("thinkingMode") or "off"
1074+
think_filter = ThinkingTokenFilter(detect_raw_reasoning=(thinking_mode != "off"))
1075+
filter_result = think_filter.feed(raw_text)
1076+
flushed = think_filter.flush()
1077+
text = f"{filter_result.text}{flushed.text}".strip()
10591078
if transcript_fallback:
10601079
text, transcript_trimmed = _trim_transcript_continuation(text)
10611080
if transcript_trimmed:
@@ -1085,8 +1104,42 @@ def stream_generate(self, request: dict[str, Any]) -> None:
10851104
raise RuntimeError("No MLX model is loaded.")
10861105

10871106
speculative_stream_fallback_note = None
1088-
# DFLASH doesn't support token-level streaming natively, so emit
1089-
# the full result as a single chunk in the streaming protocol.
1107+
# DFLASH/DDTree don't support token-level streaming natively, so
1108+
# emit the full result as a single chunk in the streaming protocol.
1109+
# Prefer DDTree (tree-based) when tree_budget > 0, else linear DFlash.
1110+
if self.speculative_decoding and self.tree_budget > 0 and self._ddtree_draft is not None:
1111+
try:
1112+
result = self._generate_ddtree(request)
1113+
if result.get("text"):
1114+
_emit({"ok": True, "chunk": {"text": result["text"]}})
1115+
_emit({
1116+
"ok": True,
1117+
"done": True,
1118+
"result": {
1119+
"finishReason": result.get("finishReason", "stop"),
1120+
"promptTokens": result.get("promptTokens", 0),
1121+
"completionTokens": result.get("completionTokens", 0),
1122+
"totalTokens": result.get("totalTokens", 0),
1123+
"tokS": result.get("tokS", 0.0),
1124+
"promptTokS": result.get("promptTokS", 0.0),
1125+
"peakMemoryGb": result.get("peakMemoryGb", 0.0),
1126+
"runtimeNote": result.get("runtimeNote"),
1127+
"dflashAcceptanceRate": result.get("dflashAcceptanceRate"),
1128+
"cacheStrategy": result.get("cacheStrategy"),
1129+
"cacheBits": result.get("cacheBits"),
1130+
"fp16Layers": result.get("fp16Layers"),
1131+
"speculativeDecoding": result.get("speculativeDecoding"),
1132+
"treeBudget": result.get("treeBudget"),
1133+
},
1134+
})
1135+
return
1136+
except Exception as exc:
1137+
speculative_stream_fallback_note = (
1138+
f"DDTree stream path failed ({exc}). "
1139+
"Falling back to linear DFLASH."
1140+
)
1141+
# Fall through to linear DFLASH below
1142+
10901143
if self.speculative_decoding and self._dflash_generator is not None:
10911144
try:
10921145
result = self._generate_dflash(request)

scripts/build-llama-turbo.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ else
5858
fi
5959

6060
# Platform-specific CMake flags
61-
CMAKE_FLAGS=(-DCMAKE_BUILD_TYPE=Release)
61+
# -DBUILD_SHARED_LIBS=OFF produces a self-contained static binary that
62+
# doesn't depend on .dylib/.so files at a build-time rpath. Without this
63+
# the installed binary crashes on dyld load because the shared libraries
64+
# are left behind in the build directory.
65+
CMAKE_FLAGS=(-DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF)
6266
case "$(uname -s)" in
6367
Darwin)
6468
CMAKE_FLAGS+=(-DGGML_METAL=ON)

scripts/stage-runtime.mjs

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ function main() {
6969
}
7070

7171
const chaosEngineBundle = stageVendoredChaosEngine(pythonInfo.executable);
72+
const bundledOptionalPackages = stageOptionalRuntimePackages(pythonInfo.executable);
7273
validateBundledProjectImports(pythonInfo.executable);
7374
const llamaWarnings = stageLlamaBinaries();
7475
maybeSignEmbeddedRuntime();
@@ -88,6 +89,7 @@ function main() {
8889
llamaCli: fs.existsSync(path.join(binDest, binaryName("llama-cli"))) ? `bin/${binaryName("llama-cli")}` : null,
8990
pythonVersion: pythonInfo.versionTag,
9091
bundledCacheStrategies: chaosEngineBundle ? ["chaosengine"] : [],
92+
bundledOptionalPackages: bundledOptionalPackages,
9193
warnings: llamaWarnings,
9294
};
9395

@@ -169,6 +171,7 @@ function validateBundledPythonPackages(pythonBinary) {
169171
"requirements = {",
170172
" 'desktop': [('fastapi', 'fastapi'), ('huggingface_hub', 'huggingface_hub'), ('psutil', 'psutil'), ('uvicorn', 'uvicorn')],",
171173
" 'images': [('accelerate', 'accelerate'), ('diffusers', 'diffusers'), ('huggingface_hub', 'huggingface_hub'), ('PIL', 'pillow'), ('safetensors', 'safetensors'), ('torch', 'torch')],",
174+
" 'inference': [('dflash_mlx', 'dflash-mlx'), ('turboquant', 'turboquant'), ('turboquant_mlx', 'turboquant-mlx-full')],",
172175
"}",
173176
"missing = {",
174177
" group: [label for module, label in modules if importlib.util.find_spec(module) is None]",
@@ -182,14 +185,28 @@ function validateBundledPythonPackages(pythonBinary) {
182185
encoding: "utf8",
183186
}).trim();
184187
const missing = JSON.parse(payload);
185-
const flatMissing = Object.entries(missing)
188+
189+
// Inference packages are optional — warn but never block the build.
190+
const optionalGroups = new Set(["inference"]);
191+
const requiredMissing = Object.entries(missing)
192+
.filter(([group]) => !optionalGroups.has(group))
193+
.flatMap(([group, values]) => values.length ? values.map((value) => `${group}:${value}`) : []);
194+
const optionalMissing = Object.entries(missing)
195+
.filter(([group]) => optionalGroups.has(group))
186196
.flatMap(([group, values]) => values.length ? values.map((value) => `${group}:${value}`) : []);
187-
if (flatMissing.length === 0) {
197+
198+
if (optionalMissing.length) {
199+
console.warn(
200+
`[stage-runtime] info: optional inference packages not in build venv (${optionalMissing.join(", ")}). ` +
201+
`DFlash/TurboQuant will require user install via the Setup page.`,
202+
);
203+
}
204+
if (requiredMissing.length === 0) {
188205
return;
189206
}
190207

191208
const message =
192-
`Embedded Python is missing required runtime packages (${flatMissing.join(", ")}). ` +
209+
`Embedded Python is missing required runtime packages (${requiredMissing.join(", ")}). ` +
193210
`Install them into the build venv with: ${pythonBinary} -m pip install -e ".[desktop,images]"`;
194211
if (strict) {
195212
throw new Error(message);
@@ -278,6 +295,82 @@ function resolveChaosEngineVendor() {
278295
};
279296
}
280297

298+
function stageOptionalRuntimePackages(pythonBinary) {
299+
// Pre-install optional runtime packages into the staged site-packages
300+
// so that DFlash, TurboQuant, and RotorQuant work out of the box for
301+
// new users without requiring manual pip installs via the Setup page.
302+
//
303+
// Each entry: [pip package name, import name used for verification]
304+
const optionalPackages = [
305+
["dflash-mlx", "dflash_mlx"],
306+
["turboquant", "turboquant"],
307+
["turboquant-mlx-full", "turboquant_mlx"],
308+
];
309+
310+
const installed = [];
311+
const skipped = [];
312+
313+
for (const [pipName, importName] of optionalPackages) {
314+
// Check if already available in the build venv
315+
const checkScript = `import importlib.util; exit(0 if importlib.util.find_spec("${importName}") else 1)`;
316+
let available = false;
317+
try {
318+
execFileSync(pythonBinary, ["-c", checkScript], {
319+
cwd: workspaceRoot,
320+
stdio: "ignore",
321+
});
322+
available = true;
323+
} catch {
324+
available = false;
325+
}
326+
327+
if (!available) {
328+
const message = `Optional package "${pipName}" not found in build venv — skipping bundle`;
329+
if (strict) {
330+
console.warn(`[stage-runtime] warning: ${message}. Install with: ${pythonBinary} -m pip install ${pipName}`);
331+
}
332+
skipped.push(pipName);
333+
continue;
334+
}
335+
336+
try {
337+
console.log(`[stage-runtime] bundling optional package: ${pipName}`);
338+
execFileSync(
339+
pythonBinary,
340+
[
341+
"-m", "pip", "install",
342+
"--disable-pip-version-check",
343+
"--no-deps",
344+
"--no-compile",
345+
"--upgrade",
346+
"--target", sitePackagesDest,
347+
pipName,
348+
],
349+
{
350+
cwd: workspaceRoot,
351+
stdio: "inherit",
352+
},
353+
);
354+
installed.push(pipName);
355+
} catch (err) {
356+
const message = `Failed to bundle optional package "${pipName}": ${err.message}`;
357+
if (strict) {
358+
throw new Error(message);
359+
}
360+
console.warn(`[stage-runtime] warning: ${message}`);
361+
skipped.push(pipName);
362+
}
363+
}
364+
365+
if (installed.length) {
366+
console.log(`[stage-runtime] bundled optional packages: ${installed.join(", ")}`);
367+
}
368+
if (skipped.length) {
369+
console.log(`[stage-runtime] skipped optional packages (not in build venv): ${skipped.join(", ")}`);
370+
}
371+
return installed;
372+
}
373+
281374
function stageLlamaBinaries() {
282375
const warnings = [];
283376
const sourceDir = process.env.CHAOSENGINE_LLAMA_BIN_DIR || defaultLlamaBinDir();
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
[
2+
{
3+
"_label": "Qwen3.5-9B MLX — native, NO DFlash (baseline)",
4+
"modelRef": "mlx-community/Qwen3.5-9B-4bit",
5+
"modelName": "Qwen3.5-9B-MLX-4bit",
6+
"path": "/Users/dan/AI_Models/mlx-community/Qwen3.5-9B-MLX-4bit",
7+
"backend": "mlx",
8+
"cacheStrategy": "native",
9+
"cacheBits": 0,
10+
"fp16Layers": 0,
11+
"contextTokens": 8192,
12+
"maxTokens": 512,
13+
"temperature": 0.7,
14+
"fusedAttention": false,
15+
"fitModelInMemory": true,
16+
"speculativeDecoding": false,
17+
"treeBudget": 0,
18+
"thinkingMode": "off",
19+
"prompt": "Explain the difference between TCP and UDP. Cover reliability, ordering, speed, and typical use cases."
20+
},
21+
{
22+
"_label": "Qwen3.5-9B MLX — native, DFlash ON (tree=16)",
23+
"modelRef": "mlx-community/Qwen3.5-9B-4bit",
24+
"modelName": "Qwen3.5-9B-MLX-4bit",
25+
"path": "/Users/dan/AI_Models/mlx-community/Qwen3.5-9B-MLX-4bit",
26+
"backend": "mlx",
27+
"cacheStrategy": "native",
28+
"cacheBits": 0,
29+
"fp16Layers": 0,
30+
"contextTokens": 8192,
31+
"maxTokens": 512,
32+
"temperature": 0.7,
33+
"fusedAttention": false,
34+
"fitModelInMemory": true,
35+
"speculativeDecoding": true,
36+
"treeBudget": 16,
37+
"thinkingMode": "off",
38+
"prompt": "Explain the difference between TCP and UDP. Cover reliability, ordering, speed, and typical use cases."
39+
},
40+
{
41+
"_label": "Qwen3.5-35B-A3B MLX — native, NO DFlash (baseline)",
42+
"modelRef": "mlx-community/Qwen3.5-35B-A3B-4bit",
43+
"modelName": "Qwen3.5-35B-A3B-4bit",
44+
"path": "/Users/dan/AI_Models/mlx-community/Qwen3.5-35B-A3B-4bit",
45+
"backend": "mlx",
46+
"cacheStrategy": "native",
47+
"cacheBits": 0,
48+
"fp16Layers": 0,
49+
"contextTokens": 8192,
50+
"maxTokens": 512,
51+
"temperature": 0.7,
52+
"fusedAttention": false,
53+
"fitModelInMemory": true,
54+
"speculativeDecoding": false,
55+
"treeBudget": 0,
56+
"thinkingMode": "off",
57+
"prompt": "Explain the difference between TCP and UDP. Cover reliability, ordering, speed, and typical use cases."
58+
},
59+
{
60+
"_label": "Qwen3.5-35B-A3B MLX — native, DFlash ON (tree=16)",
61+
"modelRef": "mlx-community/Qwen3.5-35B-A3B-4bit",
62+
"modelName": "Qwen3.5-35B-A3B-4bit",
63+
"path": "/Users/dan/AI_Models/mlx-community/Qwen3.5-35B-A3B-4bit",
64+
"backend": "mlx",
65+
"cacheStrategy": "native",
66+
"cacheBits": 0,
67+
"fp16Layers": 0,
68+
"contextTokens": 8192,
69+
"maxTokens": 512,
70+
"temperature": 0.7,
71+
"fusedAttention": false,
72+
"fitModelInMemory": true,
73+
"speculativeDecoding": true,
74+
"treeBudget": 16,
75+
"thinkingMode": "off",
76+
"prompt": "Explain the difference between TCP and UDP. Cover reliability, ordering, speed, and typical use cases."
77+
},
78+
{
79+
"_label": "Qwen3.5-9B GGUF — TurboQuant 4-bit",
80+
"modelRef": "lmstudio-community/Qwen3.5-9B-GGUF",
81+
"modelName": "Qwen3.5-9B-GGUF",
82+
"path": "/Users/dan/.cache/huggingface/hub/models--lmstudio-community--Qwen3.5-9B-GGUF",
83+
"backend": "gguf",
84+
"cacheStrategy": "turboquant",
85+
"cacheBits": 4,
86+
"fp16Layers": 4,
87+
"contextTokens": 4096,
88+
"maxTokens": 256,
89+
"temperature": 0.7,
90+
"fusedAttention": false,
91+
"fitModelInMemory": true,
92+
"speculativeDecoding": false,
93+
"treeBudget": 0,
94+
"thinkingMode": "off",
95+
"prompt": "What is the capital of France? Answer in one sentence."
96+
},
97+
{
98+
"_label": "Qwen3.5-9B GGUF — RotorQuant 4-bit",
99+
"modelRef": "lmstudio-community/Qwen3.5-9B-GGUF",
100+
"modelName": "Qwen3.5-9B-GGUF",
101+
"path": "/Users/dan/.cache/huggingface/hub/models--lmstudio-community--Qwen3.5-9B-GGUF",
102+
"backend": "gguf",
103+
"cacheStrategy": "rotorquant",
104+
"cacheBits": 4,
105+
"fp16Layers": 4,
106+
"contextTokens": 4096,
107+
"maxTokens": 256,
108+
"temperature": 0.7,
109+
"fusedAttention": false,
110+
"fitModelInMemory": true,
111+
"speculativeDecoding": false,
112+
"treeBudget": 0,
113+
"thinkingMode": "off",
114+
"prompt": "What is a neural network? Answer in two sentences."
115+
}
116+
]

0 commit comments

Comments
 (0)