Local LLMs skill for Claude Code

Size it, quantize it, benchmark it, then serve. Claude sizes memory before anything downloads, using a bundled estimator that reads the model's config to compute weights plus KV cache for your context and concurrency. It then picks a runtime and quantization, sets context length and GPU offload explicitly, benchmarks prefill and decode speed, checks quality on your own tasks and keeps the endpoint on localhost or behind auth.

  • namelocal-llms
  • size209 lines · ≈2,000 tokens
  • files2
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Runs open-weight LLMs locally and on self-hosted servers with Ollama, llama.cpp, vLLM, and MLX - sizing memory for weights and KV cache, choosing GGUF quantization levels, setting context length, enabling GPU offload, exposing OpenAI-compatible endpoints, and benchmarking speed and quality. Use when someone wants to run a model offline or on-prem, picks hardware for local inference, gets out-of-memory errors or slow tokens per second, or needs a local drop-in endpoint for an app.

Try asking

  • What is the biggest model I can run on a 24 GB GPU with 32K context?
  • Ollama is painfully slow on my machine. Figure out why
  • Set up a local OpenAI-compatible endpoint so our app can run offline

Use it when

  • You need to run a model offline, on-prem, or without sending data out
  • You are choosing hardware or a quantization level for local inference
  • Local inference is slow, runs out of memory, or ignores long prompts
  • An app needs a local drop-in endpoint for development or privacy

Skip it when

How to check the result

The model loads with every layer on GPU, the observed memory is close to the estimate, decode speed meets the target, and quality on your own tasks holds against a reference.

The common mistake it prevents

Leaving the default context length in place, so long prompts are silently truncated and the model appears to ignore the system prompt.

~/.claude/skills/local-llms/SKILL.md

--- name: local-llms description: Runs open-weight LLMs locally and on self-hosted servers with Ollama, llama.cpp, vLLM, and MLX - sizing memory for weights and KV cache,… license: MIT ---

A local deployment is right when the model fits entirely in fast memory with the needed context, the measured tokens per second meets the use case, quality was checked on your own tasks at the chosen quantization, and the endpoint is not exposed without authentication. “It runs” with half the layers on CPU at 3 tokens per second is not done.

Ground rules

  • Size memory before downloading: weights + KV cache for the target context + overhead.
  • Prefer a larger model at Q4 over a smaller model at Q8 for the same memory, but verify on your tasks.
  • Set context length explicitly. Defaults are often small and prompts are silently truncated.
  • Keep the server on localhost unless it sits behind a reverse proxy with auth and TLS.
  • Benchmark on your own prompts. Leaderboard scores do not survive quantization equally.

Workflow

- [ ] 1. Define the job: model quality needed, context length, users/concurrency
- [ ] 2. Estimate memory and pick model size + quantization
- [ ] 3. Pick the runtime
- [ ] 4. Run with explicit context, offload, and KV settings
- [ ] 5. Benchmark speed and quality
- [ ] 6. Expose an OpenAI-compatible endpoint safely

2. Size memory

python3 scripts/vram_estimate.py --params 8 --quant Q4_K_M --ctx 16384 --config config.json --vram 12

The script reads layer and KV-head counts from the model’s Hugging Face config.json (or --layers --kv-heads --head-dim), prints weights, KV cache, and total, and exits 1 if it will not fit in --vram with 10% headroom.

Rules of thumb:

  • Weights ~ params x bits / 8. 8B at Q4_K_M ~ 4.9 GB; 70B at Q4_K_M ~ 42 GB.
  • KV cache grows linearly with context and concurrent sequences; at 32K+ context it can rival the weights.
  • Apple Silicon: unified memory, but macOS reserves part of it for the system by default (roughly a quarter to a third); plan on ~65-75% of RAM for the model.
  • Decode speed upper bound ~ memory bandwidth / model bytes. 400 GB/s and a 5 GB model: at most ~80 tokens/s. Prompt processing (prefill) is compute-bound and benefits from GPU.

Quantization choice (GGUF)

QuantSize vs F16Use when
Q8_0~53%Memory is plentiful; near-lossless reference
Q6_K / Q5_K_M~40% / ~36%Quality-sensitive (code, math) and it fits
Q4_K_M~30%Default balance for most models
IQ4_XS / Q3_K_M~25-27%Squeezing a bigger model in; check quality
Q2_K / IQ2< 22%Last resort; small models degrade badly

Mixture-of-experts models need memory for all experts even though only a few run per token; they are fast for their size but not small.

3. Pick the runtime

SituationRuntime
Developer laptop, quick setup, model managementOllama
Maximum control, CPU+GPU split, any GGUF, embeddedllama.cpp (llama-server)
Apple Silicon, best native speed, Python workflowsMLX (mlx-lm)
Multi-user GPU server, high throughput, batchingvLLM (or SGLang) with safetensors / AWQ / FP8 weights
GUI for non-developersLM Studio (also serves an OpenAI-compatible API)

4. Run

Ollama:

ollama pull llama3.1:8b
ollama run llama3.1:8b "Summarize: ..."
ollama ps                              # shows loaded models and the CPU/GPU split
ollama show llama3.1:8b                # context length, parameters, template

Server settings (environment variables for ollama serve):

export OLLAMA_CONTEXT_LENGTH=16384    # default context for all models
export OLLAMA_FLASH_ATTENTION=1
export OLLAMA_KV_CACHE_TYPE=q8_0      # halves KV memory; needs flash attention
export OLLAMA_NUM_PARALLEL=2          # concurrent requests per model (multiplies KV memory)
export OLLAMA_KEEP_ALIVE=30m          # keep the model loaded between requests

Per-model defaults via a Modelfile:

FROM llama3.1:8b
PARAMETER num_ctx 16384
PARAMETER temperature 0.2
SYSTEM You are a concise assistant for internal docs.
ollama create docs-assistant -f Modelfile

llama.cpp:

brew install llama.cpp                  # or build: cmake -B build -DGGML_CUDA=ON && cmake --build build -j
llama-server -hf <user>/<repo>-GGUF:Q4_K_M \
  -c 16384 -ngl 99 --jinja \
  --cache-type-k q8_0 --cache-type-v q8_0 -fa on \
  --host 127.0.0.1 --port 8080
  • -ngl 99 offloads all layers to GPU; lower it only when the model does not fit (each layer left on CPU costs speed).
  • --jinja uses the model’s embedded chat template, needed for correct tool calling.
  • Flash-attention flag syntax changed across builds (-fa vs. -fa on); check llama-server --help for your version. A quantized V cache requires flash attention.
  • -np N serves N parallel slots; the context budget is shared or per-slot depending on version, so confirm the per-slot context in the startup log.

MLX (Apple Silicon):

pip install mlx-lm
mlx_lm.generate --model mlx-community/<model>-4bit --prompt "Hello" --max-tokens 200
mlx_lm.server --model mlx-community/<model>-4bit --port 8080

vLLM (Linux, NVIDIA/AMD GPU server):

vllm serve <org>/<model> --max-model-len 16384 --gpu-memory-utilization 0.90 \
  --api-key "$LOCAL_LLM_KEY" --port 8000

5. Benchmark

Speed:

llama-bench -m model.gguf -p 512 -n 128      # prefill and decode tokens/s
ollama run llama3.1:8b --verbose "..."       # prints eval rate after the response

Targets: interactive chat >= 15-20 tokens/s decode; coding assistants want 30+. Time to first token for a long prompt is dominated by prefill; measure it with your real prompt length.

Quality: run 30-50 of your own tasks through the local model and through a reference (a hosted model or a higher-precision quant), and grade with llm-evals. For comparing quants of the same model, llama-perplexity on representative text gives a fast signal.

6. Expose an endpoint

All runtimes above speak the OpenAI Chat Completions format, so apps can point any OpenAI-compatible client at them:

RuntimeBase URL
Ollamahttp://localhost:11434/v1 (native API at /api/chat)
llama-serverhttp://localhost:8080/v1
vLLMhttp://localhost:8000/v1

Keep model choice behind your app’s client module so you can swap between a local model and a hosted API (for example the Anthropic SDK for Claude) by configuration.

Security:

  • Ollama and llama-server have no authentication by default. Never bind 0.0.0.0 on an untrusted network; put nginx or Caddy with auth and TLS in front.
  • Download weights from the original publisher or well-known quantizers; check licenses.
  • Hugging Face models that need trust_remote_code=True execute arbitrary Python; read the code first.

Gotchas

  • Silent context truncation: the prompt exceeds the configured context and the runtime drops the beginning. Symptoms: model ignores the system prompt or early documents.
  • Partial offload cliff: speed drops several-fold once layers spill to CPU. Check ollama ps or the llama.cpp load log for layers on GPU.
  • Wrong chat template produces rambling, role confusion, or broken tool calls. Use the model’s own template (--jinja, Ollama’s library models).
  • Concurrency multiplies KV memory: OLLAMA_NUM_PARALLEL=4 at 16K context needs four 16K caches.
  • Small quantized models fail at tool calling and strict JSON more often than their benchmarks suggest; use grammar/JSON-schema constrained output where the runtime supports it, and validate.
  • Thermal throttling on laptops halves sustained throughput; benchmark for minutes, not seconds.

Output contract

**Model:** <name, params, license>, quant <type>, context <n>
**Runtime:** <Ollama | llama.cpp | MLX | vLLM> on <hardware, memory>
**Memory:** estimated <GB> (weights <a>, KV <b>), observed <GB>, all layers on GPU: <yes/no>
**Speed:** prefill <t/s>, decode <t/s>, TTFT <s> at <prompt length>
**Quality:** <score> vs. reference <score> on <n> tasks
**Endpoint:** <URL>, auth <method>, bound to <interface>

Bundled files

Loaded only when a step in the skill needs them. Review them here before installing.

scripts/vram_estimate.py 89 lines · 4.4 KB
#!/usr/bin/env python3
"""Estimate memory needed to run a local LLM: weights + KV cache + runtime overhead.

Usage:
    python3 vram_estimate.py --params 8 --quant Q4_K_M --ctx 8192 --config config.json [--vram 12]
    python3 vram_estimate.py --params 70 --quant Q4_K_M --ctx 16384 --layers 80 --kv-heads 8 --head-dim 128 --vram 48
    python3 vram_estimate.py --params 8 --quant Q4_K_M --ctx 32768 --config config.json --kv-type q8_0 --parallel 4

--params      model size in billions of parameters
--config      a Hugging Face config.json (reads num_hidden_layers, num_key_value_heads, head_dim)
--layers/--kv-heads/--head-dim   architecture values if no config.json is available
--kv-type     KV cache precision: f16 (default), q8_0, q4_0
--parallel    concurrent sequences (each gets its own --ctx worth of KV cache)
--vram        available GPU / unified memory in GB; exit 1 if the estimate does not fit

Estimates are approximate (typically within 10-15%). Mixture-of-experts models need memory
for ALL experts' weights even though only some are active per token.
"""
import argparse
import json
import sys

BITS_PER_WEIGHT = {  # effective bits incl. scales, approximate for llama.cpp GGUF types
    "F32": 32.0, "F16": 16.0, "BF16": 16.0, "Q8_0": 8.5, "Q6_K": 6.6, "Q5_K_M": 5.7,
    "Q5_K_S": 5.5, "Q4_K_M": 4.85, "Q4_K_S": 4.6, "Q4_0": 4.55, "IQ4_XS": 4.25,
    "Q3_K_M": 3.9, "IQ3_M": 3.7, "Q2_K": 3.35, "AWQ4": 4.25, "GPTQ4": 4.25, "FP8": 8.0,
}
KV_BYTES = {"f16": 2.0, "q8_0": 1.0625, "q4_0": 0.5625}


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--params", type=float, required=True)
    ap.add_argument("--quant", default="Q4_K_M", type=str.upper)
    ap.add_argument("--ctx", type=int, default=8192)
    ap.add_argument("--config")
    ap.add_argument("--layers", type=int)
    ap.add_argument("--kv-heads", type=int)
    ap.add_argument("--head-dim", type=int)
    ap.add_argument("--kv-type", default="f16", choices=sorted(KV_BYTES))
    ap.add_argument("--parallel", type=int, default=1)
    ap.add_argument("--overhead-gb", type=float, default=0.8, help="runtime + compute buffers")
    ap.add_argument("--vram", type=float)
    a = ap.parse_args()

    if a.quant not in BITS_PER_WEIGHT:
        print(f"error: unknown quant {a.quant}; known: {', '.join(BITS_PER_WEIGHT)}", file=sys.stderr)
        sys.exit(2)

    layers, kv_heads, head_dim = a.layers, a.kv_heads, a.head_dim
    if a.config:
        try:
            cfg = json.load(open(a.config, encoding="utf-8"))
        except (OSError, json.JSONDecodeError) as exc:
            print(f"error: cannot read {a.config}: {exc}", file=sys.stderr)
            sys.exit(2)
        cfg = cfg.get("text_config", cfg)  # multimodal configs nest the LLM
        layers = layers or cfg.get("num_hidden_layers")
        heads = cfg.get("num_attention_heads")
        kv_heads = kv_heads or cfg.get("num_key_value_heads", heads)
        head_dim = head_dim or cfg.get("head_dim") or (cfg["hidden_size"] // heads if heads and cfg.get("hidden_size") else None)
    if not (layers and kv_heads and head_dim):
        print("error: need --config or all of --layers --kv-heads --head-dim for the KV cache estimate", file=sys.stderr)
        sys.exit(2)

    weights_gb = a.params * 1e9 * BITS_PER_WEIGHT[a.quant] / 8 / 1e9
    kv_per_token = 2 * layers * kv_heads * head_dim * KV_BYTES[a.kv_type]  # K and V
    kv_gb = kv_per_token * a.ctx * a.parallel / 1e9
    total = weights_gb + kv_gb + a.overhead_gb

    print(f"weights   {a.params:g}B @ {a.quant:<7} {weights_gb:7.2f} GB")
    print(f"KV cache  {a.ctx} ctx x {a.parallel} seq @ {a.kv_type:<5} {kv_gb:7.2f} GB"
          f"   ({kv_per_token / 1024:.0f} KiB/token; layers={layers} kv_heads={kv_heads} head_dim={head_dim})")
    print(f"overhead                       {a.overhead_gb:7.2f} GB")
    print(f"total                          {total:7.2f} GB")

    if a.vram is not None:
        usable = a.vram * 0.9  # leave headroom for the OS/display/other processes
        if total <= usable:
            print(f"FITS in {a.vram:g} GB (using ~90% = {usable:.1f} GB as the usable budget)")
        else:
            short = total - usable
            print(f"DOES NOT FIT in {a.vram:g} GB (short by {short:.1f} GB). Options: smaller quant, "
                  f"shorter --ctx, --kv-type q8_0, fewer --parallel, or partial GPU offload (slower).")
            sys.exit(1)


if __name__ == "__main__":
    main()

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc