Skip to content

feat: add local file and directory support for safetensors and GGUF#43

Open
vrdn-23 wants to merge 8 commits into
alvarobartt:mainfrom
vrdn-23:vrdn-23/local-file-support
Open

feat: add local file and directory support for safetensors and GGUF#43
vrdn-23 wants to merge 8 commits into
alvarobartt:mainfrom
vrdn-23:vrdn-23/local-file-support

Conversation

@vrdn-23

@vrdn-23 vrdn-23 commented Mar 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #2
Add support for local file and directory paths in --model-id, so users can estimate memory for models already on disk without fetching from the Hub.

Local file support (4d25c71)

  • Auto-detection: local paths are detected when the value contains a path separator or starts with . and exists on disk. Use ./ prefix for relative paths.
  • Safetensors: supports all layouts locally — single file, sharded (model.safetensors.index.json), Diffusers (model_index.json), and Sentence Transformers (with Dense module detection via modules.json).
  • GGUF: supports single files, multi-file directories, and sharded models. Shard merging and --gguf-file filtering work identically to the remote path.
  • KV cache estimation: works with local config.json, including on-demand HTTP fetch for referenced configs when text_config._name_or_path points to a different Hub model (e.g. conditional generation models).
  • New module: src/hf_mem/_local.py with local I/O functions (read_safetensors_header, read_local_json, read_gguf_bytes, list_local_files) that return the same data shapes as the HTTP fetches, so all existing parsing and display code is reused unchanged.
  • CLI: --model-id help text updated to document local path usage.

Refactor: deduplicate local/remote paths (297aec9)

The initial local support duplicated ~340 lines of logic from the remote (Hub) code path. Extracted 5 shared private helpers in run.py to eliminate the duplication:

  • _resolve_hf_token: unifies 3 copies of HF token resolution (explicit param → HF_TOKEN env → $HF_HOME/token file). Also removes a dead-code guard (elif "Authorization" not in headers on a freshly-created dict).
  • _filter_gguf_paths: unifies 2 copies of --gguf-file path filtering (shard prefix matching, exact suffix matching, error messages). Error context parameterized via a label string.
  • _build_gguf_result: unifies 2 copies of Result construction from a gguf_files_dict (single-file and multi-file sub-paths).
  • _wrap_sentence_transformer_metadata: unifies 4 occurrences of the sentence-transformer component naming pattern (0_Transformer + dense modules vs plain Transformer).
  • _estimate_safetensors_kv_cache: unifies 2 copies (~60 lines each) of the KV cache estimation block. Uses an async Callable parameter to abstract the one difference between paths — how the referenced config is fetched (local creates a one-off httpx client, remote reuses the existing one). Also improves resource safety with try/finally on the local path's client cleanup.

Net result: ~47 fewer lines, single source of truth for all shared logic, both branches reduced to data-fetching + helper calls.

Allow `--model-id` to accept local paths (files or directories) in
addition to Hugging Face Hub model IDs. Local paths are detected when
the value contains a path separator and exists on disk.

Supports all safetensors layouts (single, sharded, diffusers, sentence
transformers) and GGUF files (single, multi-file, sharded) with full
parity to the Hub path. KV cache estimation works when config.json is
present locally, including on-demand HTTP fetch for referenced configs.

New module `_local.py` provides local I/O functions that return the
same data shapes as the HTTP fetch functions, so all existing parsing
and display code is reused unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vrdn-23
vrdn-23 marked this pull request as draft March 16, 2026 19:45
@vrdn-23
vrdn-23 marked this pull request as ready for review April 6, 2026 13:28
@vrdn-23

vrdn-23 commented Apr 6, 2026

Copy link
Copy Markdown
Contributor Author

@alvarobartt this is ready for a review now. I've tested this against most of scenarios and had to refactor some code into helper functions to avoid a lot of duplication. Apologies if this may be against the contributing guidelines. Please let me know if you have any thoughts/concerns/feedback

@vrdn-23

vrdn-23 commented Apr 20, 2026

Copy link
Copy Markdown
Contributor Author

@alvarobartt just wanted to ping and see if you got a chance to review this?

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds support for using local files/directories as --model-id, enabling memory estimation for on-disk Safetensors and GGUF models while reusing the existing parsing/printing pipeline.

Changes:

  • Added local path detection in arun() with support for local .safetensors, .gguf, and model directories (including sharded/layout variants).
  • Refactored shared logic into private helpers for token resolution, GGUF path filtering/result building, SentenceTransformer metadata wrapping, and KV-cache estimation.
  • Updated CLI help text to document local path usage.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
src/hf_mem/run.py Adds local model loading path + shared helper refactors for GGUF/Safetensors handling and KV-cache estimation.
src/hf_mem/cli.py Updates --model-id help to include local file/directory usage and detection rules.
src/hf_mem/_local.py Introduces local I/O utilities for listing files and reading Safetensors/GGUF/JSON data.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/hf_mem/_local.py
Comment on lines +29 to +35
with open(filepath, "rb") as f:
size_bytes = f.read(8)
if len(size_bytes) < 8:
raise RuntimeError(f"File too small to be a valid safetensors file: {filepath}")
metadata_size = struct.unpack("<Q", size_bytes)[0]
metadata_bytes = f.read(metadata_size)
if len(metadata_bytes) < metadata_size:
Comment thread src/hf_mem/_local.py Outdated
Comment on lines +40 to +42
# Remove __metadata__ key to match the shape returned by HTTP fetch
# (the HTTP path returns the full dict including __metadata__, and
# parse_safetensors_metadata skips it via the `if key in {"__metadata__"}` check)
Comment thread src/hf_mem/_local.py
Comment on lines +52 to +57
def read_gguf_bytes(filepath: str) -> bytes:
"""Read GGUF file header bytes (up to 100MB) for metadata parsing."""
file_size = os.path.getsize(filepath)
read_size = min(file_size, _MAX_GGUF_READ_SIZE)
with open(filepath, "rb") as f:
return f.read(read_size)
Comment thread src/hf_mem/run.py Outdated
"User-Agent": f"hf-mem/{__version__}; id={uuid4()}; model_id={model_id}; revision={revision}"
}
# --- Local path detection ---
is_local = (os.sep in model_id or model_id.startswith(".")) and os.path.exists(model_id)
Comment thread src/hf_mem/run.py
Comment on lines +177 to +186
if hf_token is not None:
return {"Authorization": f"Bearer {hf_token}"}
if token := os.getenv("HF_TOKEN"):
return {"Authorization": f"Bearer {token}"}
hf_home = os.getenv("HF_HOME", ".cache/huggingface")
token_path = (
os.path.join(os.path.expanduser("~"), hf_home, "token")
if not os.path.isabs(hf_home)
else os.path.join(hf_home, "token")
)
Comment thread src/hf_mem/run.py
if prefix_match := re.match(r"(.+)-\d+-of-\d+\.gguf$", gguf_file):
prefix = prefix_match.group(1)
gguf_paths = [
path for path in gguf_paths if re.match(rf"{re.escape(prefix)}-\d+-of-\d+\.gguf$", str(path))
Comment thread src/hf_mem/_local.py
Comment on lines +10 to +21
def list_local_files(directory: str) -> List[str]:
"""Walk directory and return relative file paths, following symlinks."""
file_paths = []
for root, _dirs, files in os.walk(directory, followlinks=True):
for f in files:
full_path = os.path.join(root, f)
# Skip broken symlinks
if not os.path.exists(full_path):
continue
rel_path = os.path.relpath(full_path, directory)
file_paths.append(rel_path)
return file_paths

@alvarobartt alvarobartt left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Hey @vrdn-23 thanks for the change, and apologies I get back to this just now, I tried to prioritize fixes (particularly around the KV cache estimations for certain quantization configurations).

I'd try to simplify it a bit so that it plays a bit nicer with the existing coding style and if possible reduce the lines-of-code it adds, as it feels a bit too much. Also some code-comments are "sloppy" and not required, so I'd remove those too e.g., the last code comment in read_safetensors_header feels unnecessary.

Thanks again, and happy to help out if required!

P.S. Could you also pull the latest changes from main? I plan to release v0.5.3 later today with those fixes, but would love to include this PR for v0.6.0 🎉

vrdn-23 added 3 commits May 4, 2026 09:47
Signed-off-by: Vinay Damodaran <vrdn@hey.com>
Signed-off-by: Vinay Damodaran <vrdn@hey.com>
Signed-off-by: Vinay Damodaran <vrdn@hey.com>
@vrdn-23

vrdn-23 commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

Hey @alvarobartt ,
Thanks for getting to this! I have cleaned it up a bit more and fixed some minor bugs I found along the way. If you could give this another pass, that would be great!

I really did try reduce the line count, but not sure if there is more I can do, without just duplicating a bunch of code all over the place to make it seem more simple. I've added you as a collaborator to my repo, so that you can take up the branches and work on the changes if there are any stylistic changes or other things you want to fix (will probably be faster than the back and forth).

But if you don't have bandwidth, please feel free to leave comments and I can get to them.

P.S - The comments from CoPilot are probably applicable to the remote case too for most of them. I haven't fixed any of them cause I would have to touch the remote calling part of the code too. Let me know if you would like me to address those with the existing behavior as well.

@vrdn-23
vrdn-23 requested a review from alvarobartt May 6, 2026 19:57
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.

[FEATURE] Estimate VRAM for local safetensors files

3 participants