@lzhzzzzwill/cofos 1.3.6 → 1.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/bin/cofos.js +41 -7
- package/package.json +1 -1
- package/runtime/src/inference/student_adapter_inference.py +52 -17
package/README.md
CHANGED
|
@@ -77,6 +77,14 @@ Show model reasoning scaffold when emitted:
|
|
|
77
77
|
npx @lzhzzzzwill/cofos --show-reasoning
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
Select the inference device explicitly:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npx @lzhzzzzwill/cofos --device cuda # CUDA GPU machine
|
|
84
|
+
npx @lzhzzzzwill/cofos --device mps # Apple Silicon Mac
|
|
85
|
+
npx @lzhzzzzwill/cofos --device cpu # CPU fallback, slow
|
|
86
|
+
```
|
|
87
|
+
|
|
80
88
|
Ask once and exit, useful for scripts and smoke tests:
|
|
81
89
|
|
|
82
90
|
```bash
|
|
@@ -95,7 +103,8 @@ On startup, the CLI does the following:
|
|
|
95
103
|
5. If `--pdf-dir` is provided, scans PDF names, sizes, and mtimes.
|
|
96
104
|
6. Re-parses PDFs only when the folder contents changed or `--rebuild-pdf-index` is passed.
|
|
97
105
|
7. Merges ROS-related PDF chunks into BM25 retrieval.
|
|
98
|
-
8.
|
|
106
|
+
8. Selects the inference backend: CUDA first, then Apple Silicon MPS, then CPU.
|
|
107
|
+
9. Loads the model once and keeps the Python backend alive for the whole chat session.
|
|
99
108
|
|
|
100
109
|
PDF files are read locally and are not uploaded anywhere. Set `COFOS_CACHE_DIR` to override the COFOS cache root, or `HF_HOME` to override only the Hugging Face model cache.
|
|
101
110
|
|
|
@@ -133,6 +142,8 @@ cofos \
|
|
|
133
142
|
| `--cache-dir` | Folder for model, KG/BM25, and parsed PDF cache; defaults to `./.cofos` |
|
|
134
143
|
| `--rebuild-pdf-index` | Force PDF parsing and BM25 rebuild |
|
|
135
144
|
| `--top-k` | Retrieval top-k |
|
|
145
|
+
| `--device` | `auto`, `cuda`, `mps`, or `cpu`; defaults to `auto` |
|
|
146
|
+
| `--dtype` | `auto`, `float32`, `float16`, or `bfloat16`; defaults to `auto` |
|
|
136
147
|
| `-p`, `--prompt` | Ask one question and exit |
|
|
137
148
|
| `--show-reasoning` | Show emitted reasoning scaffold in dim text before the final answer |
|
|
138
149
|
| `--max-new-tokens` | Override generation length |
|
package/bin/cofos.js
CHANGED
|
@@ -216,7 +216,7 @@ function createPlainUi() {
|
|
|
216
216
|
async function main() {
|
|
217
217
|
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
218
218
|
console.log("\n COFOS CLI — 9B + RAG chat (Willlzh/COFOS)\n");
|
|
219
|
-
console.log(" Usage: cofos [-p QUESTION] [--show-reasoning] [--model PATH] [--config PATH] [--pdf-dir DIR] [--cache-dir DIR] [--top-k N] [--no-rag]\n");
|
|
219
|
+
console.log(" Usage: cofos [-p QUESTION] [--show-reasoning] [--device DEVICE] [--dtype DTYPE] [--model PATH] [--config PATH] [--pdf-dir DIR] [--cache-dir DIR] [--top-k N] [--no-rag]\n");
|
|
220
220
|
console.log(" Commands in chat:");
|
|
221
221
|
console.log(" /help Show this help");
|
|
222
222
|
console.log(" /exit Quit");
|
|
@@ -229,6 +229,8 @@ async function main() {
|
|
|
229
229
|
console.log(" --pdf-dir DIR Add PDFs from DIR as extra BM25 evidence");
|
|
230
230
|
console.log(" --cache-dir DIR Store model/data cache here (default: ./.cofos)");
|
|
231
231
|
console.log(" --max-new-tokens N Override generation length");
|
|
232
|
+
console.log(" --device DEVICE auto|cuda|mps|cpu (default: auto; CUDA first, then Mac MPS)");
|
|
233
|
+
console.log(" --dtype DTYPE auto|float32|float16|bfloat16 (default: auto)");
|
|
232
234
|
console.log(" --show-reasoning Show model reasoning scaffold in dim text when emitted");
|
|
233
235
|
console.log(" -p, --prompt TEXT Ask once and exit");
|
|
234
236
|
console.log(" --rebuild-pdf-index Force reparsing PDFs and rebuilding BM25\n");
|
|
@@ -247,6 +249,8 @@ async function main() {
|
|
|
247
249
|
let cacheDir = process.env.COFOS_CACHE_DIR ? resolve(process.env.COFOS_CACHE_DIR) : resolve(process.cwd(), ".cofos");
|
|
248
250
|
let rebuildPdfIndex = false;
|
|
249
251
|
let showReasoning = false;
|
|
252
|
+
let devicePreference = process.env.COFOS_DEVICE || "auto";
|
|
253
|
+
let dtypePreference = process.env.COFOS_DTYPE || "auto";
|
|
250
254
|
let singlePrompt = null;
|
|
251
255
|
const backendArgs = [chatScript];
|
|
252
256
|
|
|
@@ -280,6 +284,12 @@ async function main() {
|
|
|
280
284
|
rebuildPdfIndex = true;
|
|
281
285
|
} else if (arg === "--show-reasoning") {
|
|
282
286
|
showReasoning = true;
|
|
287
|
+
} else if (arg === "--device") {
|
|
288
|
+
devicePreference = value(++i, arg).toLowerCase();
|
|
289
|
+
if (!["auto", "cuda", "mps", "cpu", "gpu", "metal"].includes(devicePreference)) die(`--device must be one of auto, cuda, mps, cpu; got "${devicePreference}"`);
|
|
290
|
+
} else if (arg === "--dtype") {
|
|
291
|
+
dtypePreference = value(++i, arg).toLowerCase();
|
|
292
|
+
if (!["auto", "fp32", "float32", "fp16", "float16", "bf16", "bfloat16"].includes(dtypePreference)) die(`--dtype must be one of auto, float32, float16, bfloat16; got "${dtypePreference}"`);
|
|
283
293
|
} else if (arg === "-p" || arg === "--prompt") {
|
|
284
294
|
singlePrompt = value(++i, arg);
|
|
285
295
|
} else if (arg === "--no-rag") {
|
|
@@ -318,6 +328,8 @@ async function main() {
|
|
|
318
328
|
PYTHONUNBUFFERED: process.env.PYTHONUNBUFFERED || "1",
|
|
319
329
|
PYTHONIOENCODING: process.env.PYTHONIOENCODING || "utf-8",
|
|
320
330
|
COFOS_SHOW_REASONING: showReasoning ? "1" : (process.env.COFOS_SHOW_REASONING || "0"),
|
|
331
|
+
COFOS_DEVICE: devicePreference,
|
|
332
|
+
COFOS_DTYPE: dtypePreference,
|
|
321
333
|
};
|
|
322
334
|
const proc = spawn(py, backendArgs, {
|
|
323
335
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -327,7 +339,7 @@ async function main() {
|
|
|
327
339
|
proc.stdout.setEncoding("utf-8");
|
|
328
340
|
proc.stderr.setEncoding("utf-8");
|
|
329
341
|
|
|
330
|
-
let buf = "", ready = false, generating = false, responseOpen = false, commandOpen = false, closed = false;
|
|
342
|
+
let buf = "", ready = false, generating = false, responseOpen = false, commandOpen = false, closed = false, backendExited = false;
|
|
331
343
|
let currentAnswer = "", assistantStart = -1, plainAssistantMode = "undecided", plainAssistantPending = "";
|
|
332
344
|
let plainReasonBuffer = "", plainReasonPrinted = 0;
|
|
333
345
|
const inputQ = [];
|
|
@@ -356,8 +368,12 @@ async function main() {
|
|
|
356
368
|
function handleLocalOnly(rawInput) {
|
|
357
369
|
const input = String(rawInput || "").trim();
|
|
358
370
|
if (!input) return true;
|
|
371
|
+
if (backendExited && !/^\/(exit|quit)$/.test(input)) {
|
|
372
|
+
ui.print("• COFOS backend is not running. Restart `cofos` after fixing the error above, or run /exit.");
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
359
375
|
if (input === "/") {
|
|
360
|
-
ui.print("
|
|
376
|
+
ui.print("• Type a command prefix such as /r or /to, then press Tab. Run /help for all commands.");
|
|
361
377
|
return true;
|
|
362
378
|
}
|
|
363
379
|
if (/^\/(exit|quit)$/.test(input)) { shutdown(); return true; }
|
|
@@ -370,7 +386,7 @@ async function main() {
|
|
|
370
386
|
return true;
|
|
371
387
|
}
|
|
372
388
|
if (/^\/(info)$/.test(input)) {
|
|
373
|
-
ui.print(`\n COFOS CLI — v${pkg.version}\n Model: ${modelPath} (Hugging Face)\n RAG: Willlzh/COFOS_data/runtime (KG + BM25) + optional --pdf-dir BM25 evidence\n Cache: ${cacheDir}\n HF: ${backendEnv.HF_HOME}\n Engine: StudentAdapterInference + transformers + PyTorch\n Status: ${ready ? "ready" : "loading"}\n Hist: ~/.cofos_history.jsonl\n`);
|
|
389
|
+
ui.print(`\n COFOS CLI — v${pkg.version}\n Model: ${modelPath} (Hugging Face)\n RAG: Willlzh/COFOS_data/runtime (KG + BM25) + optional --pdf-dir BM25 evidence\n Cache: ${cacheDir}\n HF: ${backendEnv.HF_HOME}\n Engine: StudentAdapterInference + transformers + PyTorch\n Device: ${devicePreference}\n Dtype: ${dtypePreference}\n Status: ${ready ? "ready" : "loading"}\n Hist: ~/.cofos_history.jsonl\n`);
|
|
374
390
|
return true;
|
|
375
391
|
}
|
|
376
392
|
if (input.startsWith("/") && !/^\/(clear|save|topk)(?:\s|$)/.test(input) && !/^\/rag\s+(?:on|off)$/.test(input)) {
|
|
@@ -397,8 +413,12 @@ async function main() {
|
|
|
397
413
|
input = input.trim();
|
|
398
414
|
if (!input) return false;
|
|
399
415
|
|
|
416
|
+
if (backendExited && !/^\/(exit|quit)$/.test(input)) {
|
|
417
|
+
ui.print("• COFOS backend is not running. Restart `cofos` after fixing the error above, or run /exit.");
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
400
420
|
if (input === "/") {
|
|
401
|
-
ui.print("
|
|
421
|
+
ui.print("• Type a command prefix such as /r or /to, then press Tab. Run /help for all commands.");
|
|
402
422
|
return false;
|
|
403
423
|
}
|
|
404
424
|
if (/^\/(exit|quit)$/.test(input)) { shutdown(); return true; }
|
|
@@ -439,7 +459,7 @@ async function main() {
|
|
|
439
459
|
return true;
|
|
440
460
|
}
|
|
441
461
|
if (/^\/(info)$/.test(input)) {
|
|
442
|
-
ui.print(`\n COFOS CLI — v${pkg.version}\n Model: ${modelPath} (Hugging Face)\n RAG: Willlzh/COFOS_data/runtime (KG + BM25) + optional --pdf-dir BM25 evidence\n Cache: ${cacheDir}\n HF: ${backendEnv.HF_HOME}\n Engine: StudentAdapterInference + transformers + PyTorch\n Hist: ~/.cofos_history.jsonl\n`);
|
|
462
|
+
ui.print(`\n COFOS CLI — v${pkg.version}\n Model: ${modelPath} (Hugging Face)\n RAG: Willlzh/COFOS_data/runtime (KG + BM25) + optional --pdf-dir BM25 evidence\n Cache: ${cacheDir}\n HF: ${backendEnv.HF_HOME}\n Engine: StudentAdapterInference + transformers + PyTorch\n Device: ${devicePreference}\n Dtype: ${dtypePreference}\n Hist: ~/.cofos_history.jsonl\n`);
|
|
443
463
|
return false;
|
|
444
464
|
}
|
|
445
465
|
if (input.startsWith("/")) {
|
|
@@ -465,6 +485,11 @@ async function main() {
|
|
|
465
485
|
if (!closed) ui.focus();
|
|
466
486
|
return;
|
|
467
487
|
}
|
|
488
|
+
if (backendExited) {
|
|
489
|
+
ui.print("• COFOS backend is not running. Restart `cofos` after fixing the error above, or run /exit.");
|
|
490
|
+
if (!closed) ui.focus();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
468
493
|
if (!ready || generating) {
|
|
469
494
|
inputQ.push(line);
|
|
470
495
|
ui.print(ready ? "• Queued until the current answer finishes." : "• Queued until the model is ready. Use /exit to quit while loading.");
|
|
@@ -658,8 +683,17 @@ async function main() {
|
|
|
658
683
|
proc.on("error", (e) => { ui.print("[cofos] Python error: " + e.message); shutdown(); });
|
|
659
684
|
proc.on("exit", (c) => {
|
|
660
685
|
if (closed) return;
|
|
686
|
+
backendExited = true;
|
|
687
|
+
ready = false;
|
|
688
|
+
generating = false;
|
|
689
|
+
inputQ.length = 0;
|
|
661
690
|
ui.print(c && c !== 0 ? "[cofos] Python exited with code " + c : "[cofos] Python backend exited.");
|
|
662
|
-
|
|
691
|
+
if (c && c !== 0) {
|
|
692
|
+
ui.print("[cofos] Backend failed before the model became ready. On Mac this usually means the 9B model could not fit in available memory and Transformers tried to offload the whole model to disk.");
|
|
693
|
+
ui.print("[cofos] Try a machine with more unified/GPU memory, force CPU with --device cpu for a slower attempt, or use a smaller/quantized model. Use /exit to close this session.");
|
|
694
|
+
} else {
|
|
695
|
+
ui.print("Press Ctrl+C to close.");
|
|
696
|
+
}
|
|
663
697
|
ui.focus();
|
|
664
698
|
process.exitCode = c ?? 0;
|
|
665
699
|
});
|
package/package.json
CHANGED
|
@@ -225,14 +225,32 @@ class StudentAdapterInference:
|
|
|
225
225
|
raise FileNotFoundError(f"Base model path not found: {self.model_path}")
|
|
226
226
|
|
|
227
227
|
@staticmethod
|
|
228
|
-
def
|
|
229
|
-
"""
|
|
228
|
+
def _select_device() -> str:
|
|
229
|
+
requested = os.environ.get("COFOS_DEVICE", "auto").strip().lower()
|
|
230
|
+
aliases = {"gpu": "cuda", "metal": "mps"}
|
|
231
|
+
requested = aliases.get(requested, requested)
|
|
232
|
+
if requested not in {"", "auto", "cuda", "mps", "cpu"}:
|
|
233
|
+
logging.warning("Ignoring unsupported COFOS_DEVICE=%s; using auto", requested)
|
|
234
|
+
requested = "auto"
|
|
235
|
+
if requested == "cuda":
|
|
236
|
+
if not torch.cuda.is_available():
|
|
237
|
+
raise RuntimeError("COFOS_DEVICE=cuda was requested, but CUDA is not available.")
|
|
238
|
+
return "cuda"
|
|
239
|
+
if requested == "mps":
|
|
240
|
+
if not getattr(torch.backends, "mps", None) or not torch.backends.mps.is_available():
|
|
241
|
+
raise RuntimeError("COFOS_DEVICE=mps was requested, but PyTorch MPS is not available.")
|
|
242
|
+
return "mps"
|
|
243
|
+
if requested == "cpu":
|
|
244
|
+
return "cpu"
|
|
245
|
+
if torch.cuda.is_available():
|
|
246
|
+
return "cuda"
|
|
247
|
+
if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
|
|
248
|
+
return "mps"
|
|
249
|
+
return "cpu"
|
|
230
250
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
opts into another dtype with COFOS_DTYPE.
|
|
235
|
-
"""
|
|
251
|
+
@staticmethod
|
|
252
|
+
def _select_dtype(device: str) -> torch.dtype:
|
|
253
|
+
"""Pick a stable dtype for the selected runtime device."""
|
|
236
254
|
requested = os.environ.get("COFOS_DTYPE", "auto").strip().lower()
|
|
237
255
|
aliases = {
|
|
238
256
|
"fp32": torch.float32,
|
|
@@ -247,10 +265,12 @@ class StudentAdapterInference:
|
|
|
247
265
|
if requested not in {"", "auto"}:
|
|
248
266
|
logging.warning("Ignoring unsupported COFOS_DTYPE=%s; using auto", requested)
|
|
249
267
|
|
|
250
|
-
if
|
|
268
|
+
if device == "cuda":
|
|
251
269
|
if torch.cuda.is_bf16_supported():
|
|
252
270
|
return torch.bfloat16
|
|
253
271
|
return torch.float16
|
|
272
|
+
if device == "mps":
|
|
273
|
+
return torch.float16
|
|
254
274
|
return torch.float32
|
|
255
275
|
|
|
256
276
|
def load_model(self) -> None:
|
|
@@ -277,15 +297,30 @@ class StudentAdapterInference:
|
|
|
277
297
|
# Decoder-only models must be left-padded for correct generation.
|
|
278
298
|
self.tokenizer.padding_side = "left"
|
|
279
299
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
model
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
300
|
+
device = self._select_device()
|
|
301
|
+
dtype = self._select_dtype(device)
|
|
302
|
+
logging.info("Loading model weights with dtype=%s on device=%s", dtype, device)
|
|
303
|
+
load_kwargs = {
|
|
304
|
+
"torch_dtype": dtype,
|
|
305
|
+
"low_cpu_mem_usage": True,
|
|
306
|
+
"trust_remote_code": True,
|
|
307
|
+
}
|
|
308
|
+
if device == "cuda":
|
|
309
|
+
load_kwargs["device_map"] = "auto"
|
|
310
|
+
try:
|
|
311
|
+
model = AutoModelForCausalLM.from_pretrained(self.model_path_str, **load_kwargs)
|
|
312
|
+
if device in {"mps", "cpu"}:
|
|
313
|
+
model = model.to(torch.device(device))
|
|
314
|
+
except ValueError as exc:
|
|
315
|
+
if "offload the whole model to the disk" in str(exc):
|
|
316
|
+
raise RuntimeError(
|
|
317
|
+
"COFOS could not fit the 9B model in available memory. "
|
|
318
|
+
"CUDA machines use automatic GPU placement; on Mac, COFOS avoids "
|
|
319
|
+
"Transformers disk offload and tries direct MPS loading instead. "
|
|
320
|
+
"Use a machine with more unified memory/GPU memory, or set "
|
|
321
|
+
"COFOS_DEVICE=cpu for a slower CPU-only attempt."
|
|
322
|
+
) from exc
|
|
323
|
+
raise
|
|
289
324
|
logging.info("Using merged model without a separate LoRA adapter")
|
|
290
325
|
|
|
291
326
|
model.generation_config.use_cache = True
|