Skip to content

Pinning a Gemma Checkpoint Instead of Tracking Main

9 min read · updated August 11, 2026

from_pretrained("google/gemma-3-4b-it") resolves to whatever the main branch of that repository points at right now. That is a moving target, and on Gemma it has moved for reasons that change model behaviour: template corrections, tokenizer fixes, re-uploaded weights. This is how to stop it moving under you.

What moves when you track main

A Hugging Face model repository is a git repository, and main is a branch like any other. When Google pushes a commit, every deployment that pulls without a revision picks it up on its next cold start. The files that change are not decorative:

  • tokenizer_config.json holds the chat template. A template correction changes the exact text your model receives, which changes its output.
  • generation_config.json holds the stop-token set. A change here changes when generation ends, which is the subject of the duplicated stop-token page.
  • config.json holds architectural values including the sliding window and the position limit.
  • The safetensors shards themselves have been re-uploaded after conversion fixes, on this family and on others.

None of that announces itself. Your evaluation results shift, your golden outputs stop matching, and the diff is in someone else’s repository. Pinning turns that into a decision you make.

The failure has a characteristic shape that is worth recognising, because it wastes days when you do not. Nothing changes for weeks; then one instance restarts, pulls a new revision into a cold cache, and begins behaving differently from its siblings. You now have a fleet where some nodes are on one model and some on another, with identical code, identical configuration and identical container images. Every debugging instinct points at your own deployment, and none of them finds anything, because the difference is in a directory the container downloaded at start-up.

Find the commit you are on

Before pinning to a good revision, establish which one you are currently running, so the pin preserves behaviour instead of changing it. Gemma repositories are gated, so authenticate first: accept the licence on the model page and log in.

  1. Authenticate, once per machine or CI runner.
    hf auth login          # older CLIs: huggingface-cli login
  2. Ask the hub what main currently resolves to.
    from huggingface_hub import model_info
    
    info = model_info("google/gemma-3-4b-it")
    print(info.sha)          # full commit hash of main, right now
    print(info.lastModified)
  3. If the model is already in your local cache, read the hash you actually have rather than the one upstream has now. They differ precisely when it matters.
    from huggingface_hub import scan_cache_dir
    
    for repo in scan_cache_dir().repos:
        if repo.repo_id == "google/gemma-3-4b-it":
            for rev in repo.revisions:
                print(rev.commit_hash, sorted(r for r in rev.refs))

Pin every loader, not just the model

This is the step that is usually done halfway. The weights get a revision and the tokenizer does not, which produces the worst combination available: pinned parameters with a floating chat template. Pass revision to every from_pretrained call that touches the repository.

  1. Put the hash in one place, not in each call site.
    MODEL_ID = "google/gemma-3-4b-it"
    REVISION = "0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6"   # full 40-char sha
  2. Load the tokenizer, the processor if the checkpoint is multimodal, and the model at the same revision.
    from transformers import AutoTokenizer, AutoProcessor, AutoModelForCausalLM
    
    tok   = AutoTokenizer.from_pretrained(MODEL_ID, revision=REVISION)
    proc  = AutoProcessor.from_pretrained(MODEL_ID, revision=REVISION)   # vision sizes only
    model = AutoModelForCausalLM.from_pretrained(MODEL_ID, revision=REVISION)
  3. For a container image or an air-gapped deployment, download the revision as a unit at build time and point the runtime at the resulting directory.
    from huggingface_hub import snapshot_download
    
    path = snapshot_download(MODEL_ID, revision=REVISION)
    print(path)   # a directory whose contents cannot change under you
  4. Make a floating load impossible rather than merely discouraged. In CI, fail the build if any from_pretrained lacks a revision; a grep is enough, and it catches the call site somebody adds in six months.

Use the full commit hash rather than a tag. Tags are movable references and inherit the problem you are solving. A 40-character sha is content-addressed and cannot be repointed.

Two adjacent pins matter as much as the model one, and both are easy to forget. The first is the library: Gemma 3 requires a transformers version new enough to know the architecture, and a stack that is too old fails at load rather than silently, which is the good case. The bad case is the reverse, where a newer library changes a default in the processor or the attention implementation and your pinned weights run differently anyway. Pin the library alongside the revision.

The second is the download itself. In a container build, doing the download at image-build time rather than at container start means the artefact is baked in, start-up does no network I/O, and an outage at the hub cannot stop you scaling. It also means the licence-gated authentication happens once, in a controlled place, rather than on every node.

Verify and record the pin

  1. Assert the pin at start-up so a misconfigured environment fails loudly instead of quietly serving a different model.
    import hashlib, json
    
    tmpl = tok.chat_template or ""
    print("template sha256:", hashlib.sha256(tmpl.encode()).hexdigest()[:16])
    print("eos ids:", model.generation_config.eos_token_id)
    print("window:", model.config.max_position_embeddings)
  2. Log those three values with every evaluation run. When a score moves you will want to know whether the model moved, and a hash in a log answers that in seconds.
  3. Record the revision next to your licence records too. The Gemma terms attach to the artefact you shipped, so knowing exactly which artefact that was is part of being able to answer a licence question later.

Moving the pin on purpose

A pin is not a decision to never upgrade. It is a decision that upgrading is an event with a diff attached. The upgrade loop is short:

  1. Compare your pinned hash with current main.
    from huggingface_hub import model_info
    print(model_info(MODEL_ID).sha == REVISION)
  2. Read the repository’s commit history on the hub to see what changed. A template or generation-config change deserves a full evaluation; a README edit does not.
  3. Run your evaluation set against both revisions before switching, and re-measure token counts if the tokenizer changed at all. The version history lists which properties are most likely to have moved.
  4. Update the constant, redeploy, and keep the previous hash in the commit message so rolling back is one edit rather than an investigation.

A reasonable cadence is to check for upstream movement on a schedule rather than to react to it. A weekly job that compares your pinned hash against main and opens an issue when they differ turns an invisible risk into a small piece of routine maintenance, and it means you find out about a template correction because a job told you rather than because a user complained.

The same discipline applies to derived artefacts. Quantised conversions, ONNX exports and fine-tuned adapters all carry an implicit dependency on the base revision they were produced from, and none of them records it unless you do. Write the base hash into the artefact’s own metadata or filename at build time. When an adapter behaves strangely on a newer base a year later, that one string is the difference between a five-minute answer and an afternoon.

None of this is Gemma-specific, and that is rather the point. Every open-weight family is served from a mutable repository, so the same discipline applies to all of them; Gemma is simply a family whose template and generation config have both been corrected upstream, which makes the cost of not pinning concrete rather than theoretical. If you take one habit from this page, make it the constant at the top of the module: a model id and a full commit hash, in one place, that every loader in the codebase reads.

Hub CLI commands and helper names have changed over time; the revision argument to from_pretrained and snapshot_download is the stable part. Check the current huggingface_hub documentation for the CLI spelling your version uses.