Skip to content

registry: first-class support for split/multi-file GGUF (pull, scan, register, run) #1256

Description

@thinmintdev

Problem

hal0 assumes a single-file GGUF at every stage, so split GGUFs (model-00001-of-00003.gguf, produced by gguf-split and increasingly common for large HF uploads) can't be pulled, scanned, or registered through any supported path:

  • Pullrun_pull (src/hal0/registry/pull.py:935) builds a per-file manifest of exactly one main GGUF plus an optional mmproj sidecar (WS-11). There is no shard enumeration; pulling a sharded repo via the Add-by-HF modal downloads a single shard, which is unloadable alone.
  • Scan_SHARD_RE = ^.+-\d{5}-of-\d{5}$ in src/hal0/registry/discover.py:84 makes _is_skippable (:158) drop every shard, so hal0 model scan never surfaces split GGUFs. The same rule filters HF-cache candidates in src/hal0/api/routes/models.py:503. The comment says lone shards aren't loadable — true for a lone shard, but llama.cpp loads a complete shard set from the first shard automatically.
  • Metadatagguf_header.py / detect.py have no awareness of the GGUF split.count/split.no KVs, so even a hand-registered first shard reports the size/params of shard 1 only.
  • Run — the slot manager passes a single -m <path> to llama-server. This part already works for split models: llama.cpp auto-discovers -0000N-of- siblings in the same directory.

Current workarounds

  1. gguf-split --merge into one file, then pull/scan/register normally (the layout the repo assumes).
  2. Download all shards into one directory and hal0 model register <id> --path .../<name>-00001-of-0000N.gguf — POST /api/models does no shard check, and llama backends load it fine. Registry size/params metadata are wrong (first shard only), and FLM/NPU providers can't use it.

Proposed implementation

The pull manifest already supports multi-file jobs via PullFile(kind="model"|"mmproj"), so the plumbing mostly exists:

  1. Pull: when the selected HF file matches the shard pattern (or the repo file listing shows a -00001-of- set for the chosen quant), enqueue all shards as PullFile(kind="shard") entries in one job — aggregate bytes_total keeps the SSE wire shape unchanged. Register the row pointing at shard 1; store aggregate size.
  2. Scan/discover: exempt .gguf files matching -00001-of- from _is_skippable when their sibling set is complete (all N shards present in the directory); keep skipping shards 2..N and incomplete sets. Surface one candidate for the set.
  3. Metadata: read split.count from the GGUF header; when present, sum sibling file sizes for the registry size_bytes and mark the row (e.g. shards: N) so the dashboard can display it.
  4. Guardrails: model rm should remove the whole shard set (or warn); FLM/NPU capability routing should reject split models with a clear error instead of failing at load.

Ref: single-file assumption discussion while auditing the registry on 2026-07-11; llama.cpp shard auto-load verified as the existing behavior the run path already relies on.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions