Skip to content

Remove SafeTensors source weights after a converted GGUF loads - #225

Merged
ganisback merged 2 commits into
mainfrom
feat/drop-safetensors-after-convert
Oct 20, 2026
Merged

ganisback merged 2 commits into
mainfrom
feat/drop-safetensors-after-convert

Conversation

@ganisback

@ganisback ganisback commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • After a model converted from SafeTensors loads in llama-server, delete the model*.safetensors shards and model.safetensors.index.json the converter consumed, so the checkpoint is not stored twice (e.g. JEV-9B kept ~18 GB of shards next to its 9.5 GB GGUF).
  • Companion files such as head.safetensors and adapter directories are kept; a vision model keeps its weights until its mmproj GGUF exists.
  • The manifest is refreshed (files, size, format) so cluster sync ships the GGUF as a regular file instead of an optional derived extra.
  • Folded the two duplicated "start engine from a converted GGUF" blocks in loadEngineWithProgressMode into one closure.

Trade-off: converting the same model to a different dtype later needs a fresh download.

Testing

  • Unit tests for the cleanup rules and manifest refresh; make lint; go test ./internal/{convert,model,inference,server}.
  • End to end on a copy of laya-typed-decisions without its GGUF: conversion, load, 843 MB of SafeTensors removed, manifest switched to gguf, and a reload served the GGUF.

Review fixes (8663096)

  • Removed files are recorded in manifest.json as removed_source_weights before deletion. A dtype request with no matching GGUF then fails with ErrSourceWeightsRemoved instead of serving the GGUF that is left.
  • Leftover .safetensors companions no longer make the model look convertible. Removal waits for any conversion of the same directory, using a per-directory RWMutex.
  • go test ./... passes, and the new tests also pass under -race.

End to end: Qwen/Qwen3.5-4B (vision, 2 shards, 8.7 GB) on the :11435 preview

Step Result
POST /api/load with dtype=f16 Converted Qwen3.5-4B-f16.gguf (8.7 GB) and mmproj-Qwen3.5-4b-F16.gguf in ~31s, loaded ready. A chat completion answered correctly.
Source cleanup Ran only after the mmproj existed. Removed 2 shards + index and freed 9.3 GB. The manifest now has format=gguf, removed_source_weights lists the 3 files, and only the 2 GGUFs remain.
POST /api/load with dtype=q8_0 HTTP 400: model Qwen/Qwen3.5-4B has no q8_0 GGUF: SafeTensors source weights were removed after an earlier GGUF conversion; pull the model again to convert another dtype. No conversion ran and the f16 GGUF was not served.
PUT /api/models/Qwen/Qwen3.5-4B/config dtype=q8_0 was rejected with 400 (no q8_0 build and no weights to convert into one). dtype=f16 was accepted with 200. A default load afterwards was ready.

No panics and no data race reports in the server log.

🤖 Generated with Claude Code

Once a model converted from SafeTensors loads in llama-server, delete the
model*.safetensors shards and their index the converter consumed, so the
checkpoint is not stored twice. Companion files such as head.safetensors and
adapters stay, and a vision model keeps its weights until its mmproj exists.
The manifest is refreshed to list the GGUF so cluster sync ships it as a
regular file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@ganisback ganisback left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three blocking issues with deleting the SafeTensors source after a converted GGUF loads. See the inline comments.

Comment thread internal/inference/engine.go
Comment thread internal/inference/engine.go
Comment thread internal/convert/source_weights.go
Record the deleted source weights in the manifest before removing them, so
a dtype request the model can no longer convert fails with a clear error
instead of quietly serving the GGUF that is left. Leftover companions such
as head.safetensors no longer make the model look convertible, and removal
waits for conversions reading the same directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ganisback

Copy link
Copy Markdown
Collaborator Author

Addressed the three blocking review comments in 8663096:

  • dtype ignored after removal: the deleted files are now recorded in manifest.json (removed_source_weights) before they are removed. A dtype request with no matching GGUF now fails with ErrSourceWeightsRemoved instead of serving the GGUF that is left. A re-pull that restores every recorded file makes the model convertible again.
  • companion .safetensors: HasConvertibleHFWeights returns false once the recorded shards are missing, so head.safetensors no longer looks convertible. Convert also refuses to run in that case.
  • race: there is now a per-model-dir RWMutex. Convert holds the read lock and removal takes the write lock, so shards are never deleted while a conversion is reading them. Every load runs in the server process, so a lock inside that process is enough.

Tests: go test ./... passes, and the new tests also pass under -race.

@ganisback
ganisback merged commit 6317dc2 into main Oct 20, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant