Add warmup peak memory estimation via --max-num-batched-tokens#67
Add warmup peak memory estimation via --max-num-batched-tokens#67vrdn-23 wants to merge 18 commits into
--max-num-batched-tokens#67Conversation
…peak Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… warning text Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
HuggingFace Transformers loads models in float32 by default when torch_dtype / dtype is absent from config.json. Older BERT-family and Sentence Transformers models don't declare the field at all, so resolve_activation_dtype was returning None and the warmup peak was silently skipped. Change the final fallback from None to "F32" so these models produce a warmup peak estimate. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix arch check for ForConditionalGeneration: capture original architectures list before swapping to text_config, so the experimental KV cache gate sees the outer config's architectures (was silently failing for VLMs like Qwen2-VL after the prior refactor). - Reuse _get_config_int from metadata.py for intermediate-size and MoE expert-count resolution instead of hand-rolled helpers. - Inline get_safetensors_dtype_bytes call (drop one-line wrapper). - Extract _resolve_vocab_size helper to dedupe causal/masked LM validation. - Rewrite narrative comment to explain the text_config swap's reason.
|
Still need to clean this up a bit more, but this is the general idea of the PR |
- resolve_activation_dtype reuses torch_dtype_to_safetensors_dtype instead of reimplementing the `torch.` prefix strip - Move _QUANTIZED_SAFETENSORS_DTYPES to module scope - Fold duplicate classifier warning into _resolve_num_labels - Replace verbose docstrings with `# NOTE:` comments matching repo style - Dedupe the kv_cache/warmup_peak label list in print_safetensors_report - Reorder warmup_peak import (ruff isort fix) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
alvarobartt
left a comment
There was a problem hiding this comment.
Thanks @vrdn-23, super useful! LGTM just left some nits, but I'll need to still review it in detail (might as well add Copilot for a quick review), thanks again 🤗
There was a problem hiding this comment.
Pull request overview
This PR adds an opt-in warmup forward-pass peak activation memory estimate for Safetensors models via --max-num-batched-tokens, reports it as a new warmup_peak component, and includes it in total_memory (independent of the existing --experimental KV-cache estimate).
Changes:
- Introduces a new warmup-peak estimator (
compute_safetensors_warmup_peak) based onconfig.jsonmetadata only. - Extends the result/reporting pipeline (CLI args, JSON output, table rendering) to include
warmup_peakand fold it intototal_memory. - Refactors
run.pyconfig fetching so the sameconfig.jsonload supports KV cache and/or warmup peak estimation (includingtext_configswapping forForConditionalGeneration).
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/hf_mem/safetensors/warmup_peak.py |
Implements architecture classification + closed-form warmup peak activation estimate. |
src/hf_mem/run.py |
Adds max_num_batched_tokens plumbing, computes/stores warmup_peak, updates total_memory. |
src/hf_mem/safetensors/print.py |
Prints the WARMUP PEAK block and includes it in the combined totals. |
src/hf_mem/cli.py |
Adds --max-num-batched-tokens CLI flag + warnings and passes it through to arun. |
src/hf_mem/_types.py |
Adds WarmupPeak dataclass and threads it through Result. |
README.md |
Documents the new warmup peak feature and usage. |
skills/hf-mem/SKILL.md |
Updates skill docs to mention activation/warmup memory support. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Alvaro Bartolome <36760800+alvarobartt@users.noreply.github.com>
Co-authored-by: Alvaro Bartolome <36760800+alvarobartt@users.noreply.github.com>
Co-authored-by: Alvaro Bartolome <36760800+alvarobartt@users.noreply.github.com>
- ForQuestionAnswering: split into a dedicated `question_answering` ModelClass with hardcoded num_labels=2 (start/end logits applied per token, not pooled — formerly mapped to seq_classification) - print.py: rename `batch-size=` to `max-num-seqs=` for the warmup block since `min(max_num_batched_tokens, batch_size)` may differ from the user-provided --batch-size Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
@alvarobartt Addressed the feedback and the Copilot comments seemed worth addressing. Let me know if there's anything else I can take care of. |
|
One thing I would like input on is whether you think we should have a separate input called |
…ak-memory # Conflicts: # src/hf_mem/run.py
|
@alvarobartt just wanted to bump this in case you had time to give it a review |
|
@alvarobartt Just wanted to bump this and see if there was anything more you want me to test |
Summary
--max-num-batched-tokensflag that estimates the peak activation memory during a single warmup forward pass for Safetensors models — analogous to what vLLM measures inprofile_runand TEI measures during its warmup.warmup_peakas a new memory component alongside the existing weights and KV cache, and includes it intotal_memory.--experimental— works on its own.Why
hf-memalready reports weights and (under--experimental) KV cache. A user trying to decide whether a model fits on a given GPU still has to guess the activation overhead, which can be a large fraction of total reserved memory — particularly for models with large vocab (LM head logits at the end of the forward pass) or wide FFNs. Inference engines (vLLM, TEI) measure this directly at startup; this PR produces a closed-form approximation fromconfig.jsonmetadata alone, no GPU required.What it computes
For each forward-pass phase, the formula models PyTorch's caching allocator (a persistent residual stream + the largest transient at any one point):
residual_stream = T × hidden_size × dattn_transient = T × (num_q_heads + 2·num_kv_heads) × head_dim × dffn_transient = 2 × T × intermediate_size × d(×num_experts_per_tokfor MoE)lm_head_spike: model-class-dependentS × vocab_size × dT × vocab_size × d(LM head applied to every token, not just the last)S × num_labels × dT × num_labels × dS × hidden_size × dpeak = residual_stream + max(attn_transient, ffn_transient, lm_head_spike)T = max_num_batched_tokens,S = min(T, batch_size),d = activation dtype bytes. The activation dtype is resolved fromconfig.json(torch_dtype/dtype, falling back to F32 for older configs like BERT that don't set it; bf16 for quantized models since vLLM/TEI dequantize for compute).Formula was derived by reading the actual vLLM V1 worker (
vllm/v1/worker/gpu_worker.py:388-416for the profile_run accounting,gpu_model_runner.py:5408-5431for the token splitting that givesS = min(T, max_num_seqs)), TEI's warmup pass, and llama.cpp (src/llama-context.cpp:551-612) to confirm GGUF doesn't have a closed-form equivalent.Sample output
JSON shape:
{"memory": ..., "kv_cache": ..., "warmup_peak": ..., "total_memory": ...}(withwarmup_peakomitted when the flag isn't set, so default behavior is unchanged).Out of scope
ggml_backend_sched_get_buffer_sizeafter building the actual ggml graph; there is no closed-form formula derivable from GGUF metadata. The flag is silently a no-op for GGUF files (emits a clear warning).non_torch_increase: NCCL buffers, FlashAttention kernel workspaces, CUDA staging — only knowable at runtime. The estimate modelstorch_peak_increaseonly; real-world peak is typically a bit higher, especially in multi-GPU deployments.--detailsJSON — deferred; the internalWarmupPeakdataclass is designed so adding it later doesn't break the public API.Implementation notes
src/hf_mem/safetensors/warmup_peak.py(pure functions; no I/O).WarmupPeakin_types.py(mirrorsKvCachestyle).Resultgainswarmup_peak: int | None(parallel tokv_cache) andwarmup_peak_metadata: WarmupPeak | None(parallel tokv_cache_metadata).run.py's experimental block was refactored to fetchconfig.jsononce whether one or both estimates are requested. TheForConditionalGenerationtext_configswap was lifted out of the experimental-only branch so warmup peak benefits from it too.--batch-sizeis reused asmax_num_seqs(the same physical concurrency that drives both KV cache sizing in vLLM and the LM head spike during warmup); no new flag for that.Test plan
HuggingFaceTB/SmolLM2-135M):--max-num-batched-tokens 8192producesWARMUP PEAKblock + positivewarmup_peakin JSON.--experimental+--max-num-batched-tokensrenders both KV CACHE and WARMUP PEAK blocks;total_memory = weights + kv_cache + warmup_peak.Qwen/Qwen2-VL-2B-Instruct) with--experimentalcorrectly reports both KV cache and warmup peak (text_config swap correctly preserved).bert-base-uncased):T × vocab_sizeterm dominates (~978 MiB at T=8192 in F32 fallback).sentence-transformers/all-MiniLM-L6-v2) and cross-encoder (cross-encoder/ms-marco-MiniLM-L-6-v2) detected as encoders; FFN transient dominates.Qwen/Qwen2.5-7B-Instruct, etc.) —num_experts_per_tokcorrectly scales the FFN transient.TheBloke/deepseek-llm-7B-chat-GGUF) emits a no-op warning and skips warmup peak; existing GGUF behavior unchanged.--max-num-batched-tokensraises a clearRuntimeError.warmup_peakfield in output (default behavior unchanged for existing users).profile_runlog on a reference model — left for follow-up; not blocking.Draft for early review. Will mark ready once the test plan's optional GPU comparison is done (or omitted, depending on feedback).
🤖 Generated with Claude Code