@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 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. Loads the model once and keeps the Python backend alive for the whole chat session.
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("Commands: " + SLASH_COMMANDS.map(({ cmd }) => cmd).join(" ") + "\nType a prefix such as /r or /to, then press Tab for completion.");
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("Commands: " + SLASH_COMMANDS.map(({ cmd }) => cmd).join(" ") + "\nType a prefix such as /r or /to, then press Tab for completion.");
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
- ui.print("Press Ctrl+C to close.");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzhzzzzwill/cofos",
3
- "version": "1.3.6",
3
+ "version": "1.3.8",
4
4
  "description": "COFOS 9B + RAG chat CLI with optional local PDF ingestion.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 _select_dtype() -> torch.dtype:
229
- """Pick a stable dtype for the current runtime.
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
- CUDA can safely use bf16/fp16. On Mac/CPU, ``device_map=auto`` may place
232
- layers on CPU or disk; fp16 there can produce NaN probabilities or
233
- garbage tokens. Prefer float32 outside CUDA unless the user explicitly
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 torch.cuda.is_available():
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
- dtype = self._select_dtype()
281
- logging.info("Loading model weights with dtype=%s", dtype)
282
- model = AutoModelForCausalLM.from_pretrained(
283
- self.model_path_str,
284
- device_map="auto",
285
- torch_dtype=dtype,
286
- low_cpu_mem_usage=True,
287
- trust_remote_code=True,
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