Lesson 25 of 73
The Hugging Face Hub
The Hugging Face Hub is the website where most open-weight models are published. An open-weight model is one whose trained numbers (its weights) can be downloaded and run on your own machine. Every model on the Hub lives in a repository, and a repository is a git repository: a folder of files with a history of commits, plus branches and tags. If you know git, you already know the basic idea. If you do not, think of it as a folder that remembers every earlier version of itself and gives each version a name.
Running a model locally starts with downloading the right files from such a repository. This lesson goes through each file that a local-LLM tool reads and says what it is for. It then shows how to ask for one exact version of a repository, how to prove who you are for restricted models, how to download with the hf command and with llama.cpp, how to work without a network, and which files can run code on your machine when loaded. After it, you can open any model repository and say which files an engine reads and why.
| Topic | Files or settings |
|---|---|
| Model description | config.json, generation_config.json |
| Tokenizer | tokenizer.json, tokenizer_config.json, vocab.json, merges.txt |
| Weights | *.safetensors, *.gguf, legacy *.bin |
| Versions | branches, tags, 40-character commit hashes |
| Access | gated repositories, HF_TOKEN, hf auth login |
| Tools | hf download, llama-cli -hf |
| Environment | HF_HOME, HF_HUB_CACHE, HF_HUB_OFFLINE, HF_ENDPOINT, LLAMA_CACHE |
You do not need network access for the examples. Each Java program parses a JSON document written inline in its source, in the shape that the Hub and the huggingface_hub client (the official Python library for the Hub, version 2.1.1, read for this lesson) use. The numbers in the example files are illustrative unless the text names their source. The Hub HTTP API and Cache Layout in Java makes the real HTTP requests.
Example 1: the files of a model repository and what each is for
A model repository holds many files, and an inference engine (a program that runs a model) needs only some of them. This program takes the file list of a typical small instruct model published in safetensors format and prints what each file is for. Read the right-hand column of the output as a short glossary for the rest of the lesson.
import java.util.List;
public class RepoFiles {
static String purpose(String name) {
if (name.equals("config.json")) return "architecture and hyperparameters";
if (name.equals("generation_config.json")) return "default sampling settings and stop tokens";
if (name.equals("tokenizer.json")) return "full tokenizer: vocabulary, merges, pre-tokenizer, special tokens";
if (name.equals("tokenizer_config.json")) return "tokenizer settings and the chat template";
if (name.equals("vocab.json") || name.equals("merges.txt")) return "BPE vocabulary and merges (older format)";
if (name.endsWith(".safetensors")) return "weights";
if (name.endsWith(".safetensors.index.json")) return "map from tensor name to shard file";
if (name.endsWith(".gguf")) return "weights, tokenizer, and metadata in one llama.cpp file";
if (name.endsWith(".bin") || name.endsWith(".pt") || name.endsWith(".pth")) return "weights in PyTorch pickle format";
if (name.endsWith(".py")) return "custom model code (runs only with trust_remote_code)";
if (name.equals("README.md")) return "model card: license, usage, YAML metadata";
if (name.equals(".gitattributes")) return "which files git stores with LFS";
if (name.startsWith("LICENSE")) return "license text";
return "other";
}
public static void main(String[] args) {
List<String> files = List.of(".gitattributes", "LICENSE", "README.md", "config.json",
"generation_config.json", "merges.txt", "model.safetensors", "tokenizer.json",
"tokenizer_config.json", "vocab.json");
for (String f : files) System.out.printf("%-24s %s%n", f, purpose(f));
}
}.gitattributes which files git stores with LFS
LICENSE license text
README.md model card: license, usage, YAML metadata
config.json architecture and hyperparameters
generation_config.json default sampling settings and stop tokens
merges.txt BPE vocabulary and merges (older format)
model.safetensors weights
tokenizer.json full tokenizer: vocabulary, merges, pre-tokenizer, special tokens
tokenizer_config.json tokenizer settings and the chat template
vocab.json BPE vocabulary and merges (older format)Three groups of files appear in almost every model repository:
- Model description:
config.jsonsays which architecture to build and with which sizes.generation_config.jsongives default generation settings. - Tokenizer: the tokenizer turns text into numbers (token ids) that the model reads.
tokenizer.jsonis the complete tokenizer in the format of the Hugging Facetokenizerslibrary.vocab.jsonandmerges.txthold the same BPE (byte-pair encoding) data in the older GPT-2 format, and repositories often ship both.tokenizer_config.jsonadds settings and, in most current models, the chat template. - Weights: one
model.safetensorsfile, or several shards (model-00001-of-00004.safetensorsand so on) withmodel.safetensors.index.jsonmapping each tensor name to its shard. A shard is one piece of a file that was cut into parts because it was too large.
The other files are for git and for people. .gitattributes tells git which file patterns are stored with Git LFS (Large File Storage), the git extension that keeps large binary files outside the normal git objects. The repository itself then holds a small pointer file for each of them (the next lesson parses one). README.md is the model card: a YAML header (structured settings at the top of the file) with the license and tags, followed by documentation.
An engine such as vLLM reads the first two groups. llama.cpp reads none of them directly. Making, Quantizing, and Testing Your Own GGUF showed that convert_hf_to_gguf.py copies everything it needs into one .gguf file.
Example 2: config.json field by field
config.json describes the shape of the model: how many layers it has, how wide they are, how many attention heads it uses. An engine reads it before loading any weights, because it must know what to build. This program parses a config in the format of Qwen2-architecture models and calculates the numbers that The Transformer Block and the Full Model and Memory Math use. Look at the parameter count and the file size at the end of the output.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Locale;
public class ConfigJson {
public static void main(String[] args) throws Exception {
String json = """
{
"architectures": ["Qwen2ForCausalLM"],
"model_type": "qwen2",
"hidden_size": 896,
"intermediate_size": 4864,
"num_hidden_layers": 24,
"num_attention_heads": 14,
"num_key_value_heads": 2,
"max_position_embeddings": 32768,
"rope_theta": 1000000.0,
"rms_norm_eps": 1e-06,
"hidden_act": "silu",
"vocab_size": 151936,
"tie_word_embeddings": true,
"torch_dtype": "bfloat16",
"bos_token_id": 151643,
"eos_token_id": 151645
}
""";
JsonNode c = new ObjectMapper().readTree(json);
long d = c.get("hidden_size").asLong(), ff = c.get("intermediate_size").asLong();
long layers = c.get("num_hidden_layers").asLong(), vocab = c.get("vocab_size").asLong();
int heads = c.get("num_attention_heads").asInt(), kvHeads = c.get("num_key_value_heads").asInt();
long headDim = d / heads;
System.out.println("architecture: " + c.get("architectures").get(0).asText() + " (" + c.get("model_type").asText() + ")");
System.out.println("head_dim = " + d + " / " + heads + " = " + headDim + ", query heads per KV head = " + heads / kvHeads);
long attn = 2 * d * d + 2 * d * kvHeads * headDim; // q, o and k, v projections
long mlp = 3 * d * ff; // gate, up, down
long embed = vocab * d * (c.get("tie_word_embeddings").asBoolean() ? 1 : 2);
long total = layers * (attn + mlp) + embed;
System.out.println(String.format(Locale.ROOT, "parameters (no biases or norms): %.3f billion", total / 1e9));
System.out.println(String.format(Locale.ROOT, "weights at %s: %.2f GiB", c.get("torch_dtype").asText(), total * 2 / Math.pow(2, 30)));
System.out.println("context " + c.get("max_position_embeddings").asInt() + ", rope_theta " + c.get("rope_theta").asDouble()
+ ", eos " + c.get("eos_token_id").asInt());
}
}architecture: Qwen2ForCausalLM (qwen2)
head_dim = 896 / 14 = 64, query heads per KV head = 7
parameters (no biases or norms): 0.494 billion
weights at bfloat16: 0.92 GiB
context 32768, rope_theta 1000000.0, eos 151645The fields of the config, in the order they appear:
| Field | Meaning |
|---|---|
architectures |
The Transformers class that builds the model. Engines map this name to their own implementation, and convert_hf_to_gguf.py uses it to choose a converter |
model_type |
Short family name |
hidden_size |
Width of the residual stream (d_model), the vector that carries a token's information through the layers |
intermediate_size |
Width of the feed-forward layer |
num_hidden_layers |
Number of transformer blocks |
num_attention_heads, num_key_value_heads |
Query heads and KV heads. Fewer KV heads than query heads means grouped-query attention |
max_position_embeddings |
Longest context the model was configured for |
rope_theta |
RoPE (rotary position encoding) base frequency |
rms_norm_eps |
Small constant in RMSNorm (a normalization step), there to avoid dividing by zero |
hidden_act |
Activation function in the feed-forward layer (silu for SwiGLU models) |
vocab_size |
Rows of the embedding matrix, which is the number of tokens the model knows |
tie_word_embeddings |
true when the output layer reuses the embedding matrix |
torch_dtype |
Data type of the stored weights (newer Transformers versions write dtype) |
bos_token_id, eos_token_id |
Ids of the special tokens that mark the beginning and the end of a sequence |
The derived values in the output follow from these fields, and you can check each one by hand.
- The head dimension is
896 / 14 = 64. The hidden size is split evenly across the 14 query heads. - 14 query heads share 2 KV heads, so each KV head serves
14 / 2 = 7query heads. - The parameter estimate adds up the matrices of one layer and the shared embedding. One layer has two
d x dprojections, twod x (kv_heads * head_dim)projections, and three feed-forward matrices. The embedding is counted once because the weights are tied (tie_word_embeddingsistrue). The total is 0.494 billion, which is why such a model is called "0.5B". - At 2 bytes per BF16 weight, the file is about 0.92 GiB.
Some configs nest these values. Multimodal models (models that also read images) often put the language model's fields under text_config, and models with RoPE scaling add a rope_scaling object. A program that reads config.json must handle missing fields with defaults rather than assume every key exists.
Example 3: generation_config.json: default sampling and EOS id lists
When a model generates text, it must pick each next token from a list of probabilities. The rules for picking are called sampling settings, and the model's publisher recommends values for them. They live in generation_config.json, along with the tokens that tell the engine to stop. This program reads such a file and handles eos_token_id (the id of the end-of-sequence token) either as one number or as a list.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.List;
public class GenerationConfig {
static List<Integer> ids(JsonNode n) { // an int or a list of ints
List<Integer> out = new ArrayList<>();
if (n == null || n.isNull()) return out;
if (n.isArray()) n.forEach(v -> out.add(v.asInt())); else out.add(n.asInt());
return out;
}
public static void main(String[] args) throws Exception {
String json = """
{
"bos_token_id": 151643,
"pad_token_id": 151643,
"do_sample": true,
"eos_token_id": [151645, 151643],
"repetition_penalty": 1.1,
"temperature": 0.7,
"top_p": 0.8,
"top_k": 20
}
""";
JsonNode g = new ObjectMapper().readTree(json);
System.out.println("stop on any of: " + ids(g.get("eos_token_id")));
System.out.println("sampling: do_sample=" + g.path("do_sample").asBoolean(false)
+ " temperature=" + g.path("temperature").asDouble(1.0)
+ " top_p=" + g.path("top_p").asDouble(1.0)
+ " top_k=" + g.path("top_k").asInt(50)
+ " repetition_penalty=" + g.path("repetition_penalty").asDouble(1.0));
System.out.println("min_p present: " + g.has("min_p"));
}
}stop on any of: [151645, 151643]
sampling: do_sample=true temperature=0.7 top_p=0.8 top_k=20 repetition_penalty=1.1
min_p present: falsegeneration_config.json holds the defaults that the Transformers generate method uses when a caller does not override them: do_sample, temperature, top_p, top_k, and repetition_penalty. These are the samplers of Sampling and Penalties, Logit Bias, and Sampler Chains. Model publishers set them to the values they recommend for the model. An engine that does not read this file uses its own defaults instead.
eos_token_id is the field to read with care. In config.json it is often a single id. Here it is a list, [151645, 151643], which means generation stops at either token. An instruct model usually ends a turn with an end-of-turn token (<|im_end|> here) and also has a document-end token (<|endoftext|>). A client that stops only on the id from config.json can keep generating past the end of the answer. The helper ids() accepts both forms, which is what every reader of this file needs.
Missing keys get defaults. path("top_k").asInt(50) returns 50 when the key is absent, which is the Transformers default. The last output line shows a key the file does not set (min_p), so the engine's own default applies.
Example 4: tokenizer.json: model type, vocabulary, merges, pre-tokenizer, added tokens
A tokenizer needs several pieces: a vocabulary, rules for combining characters, and special tokens. tokenizer.json stores all of them in one file. This program parses a shortened tokenizer.json with an 8-entry vocabulary. It then applies the merges to the word "hello", as Building a BPE Tokenizer did in lesson 7. A merge is a rule that joins two neighboring pieces into one larger piece. Watch how the list of pieces shrinks after each merge.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.List;
public class TokenizerJson {
public static void main(String[] args) throws Exception {
String json = """
{
"version": "1.0",
"added_tokens": [
{"id": 8, "content": "<|endoftext|>", "special": true},
{"id": 9, "content": "<|im_start|>", "special": true}
],
"pre_tokenizer": {"type": "ByteLevel", "add_prefix_space": false},
"model": {
"type": "BPE",
"vocab": {"h": 0, "e": 1, "l": 2, "o": 3, "he": 4, "ll": 5, "hell": 6, "hello": 7},
"merges": [["h", "e"], ["l", "l"], ["he", "ll"], ["hell", "o"]]
}
}
""";
JsonNode t = new ObjectMapper().readTree(json);
JsonNode model = t.get("model");
System.out.println("model type: " + model.get("type").asText() + ", vocab size: " + model.get("vocab").size()
+ ", merges: " + model.get("merges").size());
System.out.println("pre-tokenizer: " + t.get("pre_tokenizer").get("type").asText());
for (JsonNode a : t.get("added_tokens")) System.out.println("added token " + a.get("id") + " " + a.get("content").asText());
List<String> parts = new ArrayList<>(List.of("h", "e", "l", "l", "o"));
for (JsonNode m : model.get("merges")) { // "a b" strings or ["a", "b"] pairs
String left = m.isArray() ? m.get(0).asText() : m.asText().split(" ")[0];
String right = m.isArray() ? m.get(1).asText() : m.asText().split(" ")[1];
for (int i = 0; i + 1 < parts.size(); i++)
if (parts.get(i).equals(left) && parts.get(i + 1).equals(right)) { parts.set(i, left + right); parts.remove(i + 1); i--; }
System.out.println("after merge " + left + " + " + right + ": " + parts);
}
System.out.println("token id: " + model.get("vocab").get(parts.get(0)).asInt());
}
}model type: BPE, vocab size: 8, merges: 4
pre-tokenizer: ByteLevel
added token 8 <|endoftext|>
added token 9 <|im_start|>
after merge h + e: [he, l, l, o]
after merge l + l: [he, ll, o]
after merge he + ll: [hell, o]
after merge hell + o: [hello]
token id: 7The top-level keys of a tokenizer.json are:
modelholds the algorithm ("type": "BPE", or"WordPiece","Unigram"), thevocabobject that maps token text to an id, and for BPE the orderedmergeslist.pre_tokenizerdescribes how text is split before BPE runs. Byte-level BPE models useByteLevel, often inside aSequencewith a regexSplit.added_tokenslists tokens added on top of the vocabulary, with their ids and aspecialflag. Chat markers such as<|im_start|>live here.normalizer,post_processor, anddecoderdescribe Unicode normalization, the tokens added around a sequence, and how ids turn back into text.
Merges come in two spellings. Older files store each merge as one string with a space ("h e"). Files written by recent versions of the tokenizers library store a two-element array (["h", "e"]), which also allows tokens that contain spaces. The program accepts both.
In the output, the 4 merges are applied in order. h e l l o becomes he l l o, then he ll o, then hell o, and finally one token, hello, whose id in the vocabulary is 7. The order matters: the list is sorted so that the merge learned first is applied first. A real vocabulary has 32,000 to over 200,000 entries, and its merge list is almost as long.
Example 5: tokenizer_config.json: chat template, BOS, and EOS
A chat model expects its input in a fixed text layout, with markers that show who is speaking. The recipe for that layout is the chat template, and it is stored in tokenizer_config.json together with the special-token settings. This program reads the file, finds the id of the EOS token (end of sequence), and inspects the template.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Iterator;
import java.util.Map;
public class TokenizerConfig {
public static void main(String[] args) throws Exception {
String json = """
{
"add_bos_token": false,
"bos_token": null,
"eos_token": "<|im_end|>",
"pad_token": "<|endoftext|>",
"model_max_length": 131072,
"added_tokens_decoder": {
"151643": {"content": "<|endoftext|>", "special": true},
"151644": {"content": "<|im_start|>", "special": true},
"151645": {"content": "<|im_end|>", "special": true}
},
"chat_template": "{% for message in messages %}{{'<|im_start|>' + message['role'] + '\\n' + message['content'] + '<|im_end|>' + '\\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\\n' }}{% endif %}"
}
""";
JsonNode c = new ObjectMapper().readTree(json);
String eos = c.get("eos_token").asText();
int eosId = -1;
for (Iterator<Map.Entry<String, JsonNode>> it = c.get("added_tokens_decoder").fields(); it.hasNext(); ) {
Map.Entry<String, JsonNode> e = it.next();
if (e.getValue().get("content").asText().equals(eos)) eosId = Integer.parseInt(e.getKey());
}
System.out.println("eos_token " + eos + " = id " + eosId);
System.out.println("bos_token " + c.get("bos_token") + ", add_bos_token " + c.get("add_bos_token").asBoolean());
String template = c.get("chat_template").asText();
System.out.println("chat_template: " + template.length() + " characters, uses add_generation_prompt: "
+ template.contains("add_generation_prompt"));
System.out.println(template.substring(0, 47));
}
}eos_token <|im_end|> = id 151645
bos_token null, add_bos_token false
chat_template: 196 characters, uses add_generation_prompt: true
{% for message in messages %}{{'<|im_start|>' +tokenizer_config.json names special tokens by their text, not their id: "eos_token": "<|im_end|>". The id comes from added_tokens_decoder, a map from id (as a string key) to token. The program searches that map and finds 151645.
bos_token (beginning of sequence) is null and add_bos_token is false. This tokenizer adds no BOS token, while Llama-family tokenizers do. An engine that adds a BOS token where the model was trained without one feeds the model an input it never saw.
chat_template is a Jinja template, a short program in a text template language. The shortened template here loops over the messages and wraps each one in <|im_start|>role ... <|im_end|>. When add_generation_prompt is true, it appends <|im_start|>assistant, which tells the model that its turn begins. Chat Templates, Roles, and Special Tokens renders such templates in Java. Some repositories store the template in a separate chat_template.jinja file instead of this field. A tool that reads only tokenizer_config.json then finds no template.
model_max_length (131072 here) is a tokenizer setting and can differ from max_position_embeddings in config.json. The model config is the one that describes the model.
Example 6: revisions: branches, tags, commit hashes, and pinning
A repository changes over time. A publisher can fix a chat template or replace weights, and a download that worked last week may give different files today. To get exactly the same files every time, you need to refer to one exact version. This program parses a response in the shape of GET /api/models/{repo}/refs (the Hub request that lists a repository's branches and tags). It then builds a download URL for a branch and for a commit. The repository name and the commit hashes are illustrative.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Revisions {
public static void main(String[] args) throws Exception {
// Shape of GET /api/models/{repo}/refs (fields read by huggingface_hub's list_repo_refs).
String json = """
{
"branches": [
{"name": "main", "ref": "refs/heads/main", "targetCommit": "7ae557604adf67be50417f59c2c2f167def9a775"},
{"name": "dev", "ref": "refs/heads/dev", "targetCommit": "1d2c4b9f0e8a7d6c5b4a39281706f5e4d3c2b1a0"}
],
"converts": [],
"tags": [
{"name": "v1.0", "ref": "refs/tags/v1.0", "targetCommit": "0f9e8d7c6b5a49382716051f2e3d4c5b6a798867"}
]
}
""";
JsonNode refs = new ObjectMapper().readTree(json);
for (String kind : new String[] {"branches", "tags"})
for (JsonNode r : refs.get(kind))
System.out.printf("%-8s %-5s -> %s%n", kind, r.get("name").asText(), r.get("targetCommit").asText());
String repo = "example-org/example-model", file = "config.json";
String main = refs.get("branches").get(0).get("targetCommit").asText();
System.out.println("moving: https://huggingface.co/" + repo + "/resolve/main/" + file);
System.out.println("pinned: https://huggingface.co/" + repo + "/resolve/" + main + "/" + file);
System.out.println("full 40-hex commit: " + main.matches("[0-9a-f]{40}"));
}
}branches main -> 7ae557604adf67be50417f59c2c2f167def9a775
branches dev -> 1d2c4b9f0e8a7d6c5b4a39281706f5e4d3c2b1a0
tags v1.0 -> 0f9e8d7c6b5a49382716051f2e3d4c5b6a798867
moving: https://huggingface.co/example-org/example-model/resolve/main/config.json
pinned: https://huggingface.co/example-org/example-model/resolve/7ae557604adf67be50417f59c2c2f167def9a775/config.json
full 40-hex commit: trueA revision is anything git can resolve to a commit. There are three kinds:
- A branch (
main,dev) is a movable name.resolve/main/config.jsonreturns whatevermainpoints to today. - A tag (
v1.0) is a name that usually stays on one commit. - A commit hash is 40 hexadecimal characters and never changes.
The output shows each branch and tag with the commit it points to. The two URLs differ only in one path segment: main in the first, the 40-character hash in the second. The first can change its answer tomorrow. The second cannot.
The JSON has branches, tags, and converts arrays (and pullRequests when requested). Each entry has name, ref, and targetCommit, which are the fields list_repo_refs reads in huggingface_hub. llama.cpp reads the same endpoint. get_repo_commit in common/hf-cache.cpp takes the targetCommit of the main branch, checks that it is 40 hex characters (is_valid_commit), and writes it to refs/main in its cache.
Pinning means downloading with the commit hash instead of the branch name, as in the second URL. A pinned download gives the same bytes every time. This makes a setup reproducible and protects it from an unreviewed change upstream. The hf download --revision option and the revision argument of every huggingface_hub download function take any of the three forms.
Example 7: GGUF repositories: one file per quant, naming, and split files
GGUF repositories look different from safetensors repositories. They hold many weight files, one for each quantization (a way of storing the weights with fewer bits), and they have no config.json. A tool that downloads from such a repository must therefore choose one file. This program re-implements find_best_model and gguf_filename_is_model from common/download.cpp in llama.cpp and applies them to a file list. Compare the four requests in main with the four lines of output.
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GgufRepoNames {
static final Pattern SPLIT = Pattern.compile("-(\\d{5})-of-(\\d{5})\\.gguf$");
static boolean isModel(String f) { // gguf_filename_is_model in llama.cpp
return f.endsWith(".gguf") && !f.contains("mmproj") && !f.contains("imatrix");
}
static boolean firstShardOrWhole(String f) {
Matcher m = SPLIT.matcher(f);
return !m.find() || Integer.parseInt(m.group(1)) == 1;
}
static String pick(List<String> files, String tag) { // find_best_model in llama.cpp
List<String> tags = tag.isEmpty() ? List.of("Q4_K_M", "Q8_0") : List.of(tag);
for (String t : tags) {
Pattern p = Pattern.compile(Pattern.quote(t) + "[.-]", Pattern.CASE_INSENSITIVE);
for (String f : files) if (isModel(f) && p.matcher(f).find() && firstShardOrWhole(f)) return f;
}
if (tag.isEmpty()) for (String f : files) if (isModel(f) && firstShardOrWhole(f)) return f;
return null;
}
public static void main(String[] args) {
List<String> files = List.of("README.md", "imatrix.gguf", "mmproj-model-f16.gguf",
"Model-7B-Instruct-IQ4_XS.gguf", "Model-7B-Instruct-Q4_K_M.gguf", "Model-7B-Instruct-Q6_K.gguf",
"Model-7B-Instruct-Q8_0-00001-of-00002.gguf", "Model-7B-Instruct-Q8_0-00002-of-00002.gguf");
for (String tag : new String[] {"", "q6_k", "Q8_0", "Q2_K"})
System.out.printf("tag %-6s -> %s%n", tag.isEmpty() ? "(none)" : tag, pick(files, tag));
Matcher m = SPLIT.matcher(files.get(7));
if (m.find()) System.out.println("shard " + Integer.parseInt(m.group(1)) + " of " + Integer.parseInt(m.group(2)));
}
}tag (none) -> Model-7B-Instruct-Q4_K_M.gguf
tag q6_k -> Model-7B-Instruct-Q6_K.gguf
tag Q8_0 -> Model-7B-Instruct-Q8_0-00001-of-00002.gguf
tag Q2_K -> null
shard 2 of 2The rules, as written in the pinned llama.cpp source, are:
- Only
.gguffiles count, and files whose names containmmproj(the vision projector of multimodal models) orimatrix(an importance matrix, not a model) are skipped. The source also skips draft-model files (mtp-,eagle3-,dflash-,dspark-). The Java version leaves those out for brevity. - With a tag such as
q6_k, the file name must match the regular expressionTAG[.-], case-insensitively. The[.-]after the tag preventsQ4_Kfrom matchingQ4_K_M. - Without a tag, the tags
Q4_K_Mand thenQ8_0are tried, and then the first model file of any kind. - For split files, only shard 1 qualifies (
-00001-of-00002.gguf). The loader opens the other shards itself.
The output follows the rules:
- No tag selects
Q4_K_M, the first default. q6_kmatchesQ6_Kdespite the different case.Q8_0selects the first shard of a two-part file.Q2_Kmatches nothing and returnsnull. llama.cpp reports this as an error with a list of the available files (Example 10).
The last line prints the shard number and count parsed from the split name. The split pattern -NNNNN-of-NNNNN.gguf is the one llama-gguf-split writes.
Example 8: gated models, access tokens, and HF_TOKEN
Some models are gated. Their files are available only to accounts that have accepted the publisher's conditions on the model page. A program that downloads them has to prove that it acts for such an account. It does this with an access token, a secret string created in the Hub account settings. This program implements the token lookup order of get_token() in huggingface_hub and llama.cpp's token format check. It runs the lookup three times with different environment variables.
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
public class TokenLookup {
// huggingface_hub get_token(): HF_TOKEN, then HUGGING_FACE_HUB_TOKEN, then the token file.
static String token(Map<String, String> env, Path home) throws Exception {
for (String key : new String[] {"HF_TOKEN", "HUGGING_FACE_HUB_TOKEN"})
if (env.containsKey(key) && !env.get(key).isBlank()) return env.get(key).trim();
Path hfHome = env.containsKey("HF_HOME") ? Path.of(env.get("HF_HOME"))
: Path.of(env.getOrDefault("XDG_CACHE_HOME", home.resolve(".cache").toString()), "huggingface");
Path file = env.containsKey("HF_TOKEN_PATH") ? Path.of(env.get("HF_TOKEN_PATH")) : hfHome.resolve("token");
return Files.exists(file) ? Files.readString(file).trim() : null;
}
static boolean llamaCppAccepts(String t) { // is_valid_hf_token in llama.cpp
return t != null && t.length() >= 37 && t.length() <= 256 && t.startsWith("hf_") && t.substring(3).matches("[A-Za-z0-9]+");
}
static String masked(String t) { return t == null ? "none" : t.substring(0, 6) + "..." + t.substring(t.length() - 2); }
public static void main(String[] args) throws Exception {
Path home = Files.createTempDirectory("home");
Files.createDirectories(home.resolve(".cache/huggingface"));
Files.writeString(home.resolve(".cache/huggingface/token"), "hf_FileTokenAbcdefghijklmnopqrstuvwxyz12\n");
String envToken = "hf_EnvTokenAbcdefghijklmnopqrstuvwxyz123";
for (Map<String, String> env : new Map[] {Map.of(), Map.of("HF_TOKEN", envToken), Map.of("HF_HOME", "/nonexistent")}) {
String t = token(env, home);
System.out.println(env.keySet() + " -> " + masked(t) + ", llama.cpp accepts: " + llamaCppAccepts(t));
}
System.out.println("header: Authorization: Bearer " + masked(envToken));
}
}[] -> hf_Fil...12, llama.cpp accepts: true
[HF_TOKEN] -> hf_Env...23, llama.cpp accepts: true
[HF_HOME] -> none, llama.cpp accepts: false
header: Authorization: Bearer hf_Env...23Requests send the token as Authorization: Bearer <token>. For gated or private repositories, the token must belong to an account with access.
huggingface_hub looks for a token in this order (from get_token and the constants module):
- An OIDC exchange when
HF_OIDC_RESOURCEis set (used by CI systems, which are automated build servers). - The environment variable
HF_TOKEN, then the olderHUGGING_FACE_HUB_TOKEN. - The token file at
HF_TOKEN_PATH, which defaults to$HF_HOME/token.HF_HOMEdefaults to$XDG_CACHE_HOME/huggingfaceor~/.cache/huggingface. The commandhf auth loginwrites this file.
The output shows the order working. With no variables, the file token is used. With HF_TOKEN set, it overrides the file. When HF_HOME points to an empty directory, nothing is found.
llama.cpp reads HF_TOKEN too, and -hft / --hf-token overrides it. It sends the token only if it passes is_valid_hf_token: the token must start with hf_, be 37 to 256 characters long, and contain only letters and digits after the prefix. Otherwise it logs "invalid token, authentication disabled".
Without a valid token, a request for a gated repository fails. huggingface_hub turns a response with the header X-Error-Code: GatedRepo into a GatedRepoError ("Cannot access gated repo"). A 401 on a repository API URL without that header is reported as "Repository Not Found". A comment in the client's _http.py explains that the Hub returns 401 both for missing repositories and for private or gated ones when the request is not authenticated. These status codes are described from the client's error handling in huggingface_hub 2.1.1, not from a published specification of the Hub API.
Tokens are secrets. The program masks them before printing, and tools should never write them to logs.
Example 9: the hf CLI: login and download
The hf command-line tool comes with the huggingface_hub package. It covers the two jobs you do most often: signing in and downloading files. The commands below go from the simplest case, one whole repository, to a filtered download of one quantization into a chosen directory and a download pinned to a commit. Text after # is a comment.
pip install huggingface_hub
hf auth login # browser login or a pasted token; stores it in $HF_HOME/token
hf auth whoami # shows the account the token belongs to
hf download Qwen/Qwen2.5-Coder-0.5B-Instruct # whole repository into the cache
hf download Qwen/Qwen2.5-Coder-0.5B-Instruct config.json tokenizer.json
hf download Qwen/Qwen2.5-Coder-7B-Instruct-GGUF --include "*q4_k_m*.gguf" --local-dir ./models
hf download Qwen/Qwen2.5-Coder-0.5B-Instruct --revision <40-character-commit>The subcommands and options come from hf --help, hf auth --help, and hf download --help of huggingface_hub 2.1.1:
| Command or option | Effect |
|---|---|
hf auth login, logout, whoami, list, switch, token |
Store, remove, check, list, switch between, and print access tokens |
hf download REPO_ID [FILENAMES]... |
Download the named files, or the whole repository when none are named |
--include, --exclude |
Glob patterns that select files |
--revision |
A branch, tag, or commit hash |
--cache-dir |
Use a different cache directory |
--local-dir |
Write plain files to a directory instead of the cache |
--max-workers |
Parallel downloads (default 8) |
--dry-run |
Show what would be downloaded |
Without --local-dir, files go into the shared cache (~/.cache/huggingface/hub by default). The cache has a fixed layout with blobs/ (the file contents), snapshots/<commit>/ (one folder per version, with links to the blobs), and refs/ (which commit each branch name points to). Every tool that uses huggingface_hub shares this cache, so a second download of the same file is skipped. The Hub HTTP API and Cache Layout in Java builds this layout in Java.
The pattern *q4_k_m*.gguf in the GGUF example depends on how the publisher names files. Glob matching is case-sensitive, so the pattern must match the actual names.
Example 10: llama-cli -hf repo:quant direct download
llama.cpp can download GGUF files itself, without the hf command. The -hf option takes a repository name and an optional quantization. The first command below shows the normal online use. The next two commands run against a hand-built cache with --offline, which shows the same file-selection logic without needing a network.
# online (repository name from the pinned llama.cpp README): picks a file, downloads it into the cache, runs it
llama-cli -hf ggml-org/Qwen3.5-0.8B-GGUF
# offline replay: a cache built by hand with one file, tiny-Q8_0.gguf, for the repo "demo/tiny-GGUF"
LLAMA_CACHE=$PWD/fakecache llama-completion -hf demo/tiny-GGUF:Q8_0 --offline -no-cnv -p "Hello" -n 4 -v
LLAMA_CACHE=$PWD/fakecache llama-completion -hf demo/tiny-GGUF:Q4_K_M --offline -no-cnv -p "Hello" -n 4The hand-built cache has the layout llama.cpp expects. The file models--demo--tiny-GGUF/refs/main holds a 40-character commit, and snapshots/<commit>/tiny-Q8_0.gguf is a symbolic link (a file that points to another file) to blobs/<sha256 of the file>. The first block of output below is representative output from the pinned build with the random-weight tiny model. Timestamps are removed, the path is shortened, and the first run is abridged to the line that names the loaded file, which -v (verbose) makes visible.
llama_model_loader: loaded meta data with 22 key-value pairs and 39 tensors from /tmp/fakecache/models--demo--tiny-GGUF/snapshots/0123456789abcdef0123456789abcdef01234567/tiny-Q8_0.gguf (version GGUF V3 (latest))The first run found tiny-Q8_0.gguf through the snapshot path and loaded it. The second run asks for a quantization that is not in the cache:
E common_download_get_hf_plan: no GGUF files found in repository demo/tiny-GGUF
I Available GGUF files:
I - tiny-Q8_0.gguf
error: --model is requiredThe error lists the files that are available, so you can pick a name that exists.
The -hf help text at the pinned commit describes the argument as <user>/<model>[:quant]. The quant "is optional, case-insensitive, default to Q4_K_M, or falls back to the first file in the repo if Q4_K_M doesn't exist". An mmproj file is downloaded too when the repository has one (--no-mmproj turns that off). The source tries Q8_0 after Q4_K_M before falling back, as Example 7 showed. -hff / --hf-file names an exact file instead.
Online, llama.cpp asks the API for the main commit (/api/models/{repo}/refs) and for the file list (/api/models/{repo}/tree/{commit}?recursive=true). It then downloads from {endpoint}{repo}/resolve/{commit}/{file}. The cache directory uses the Hugging Face layout, so a file downloaded by hf download is found by llama-cli -hf, and the other way around. --offline skips the API and uses only the cache, which is how the replay above worked without a network.
Example 11: offline mode and alternative endpoints
Sometimes a machine has no network access, or a company runs its own copy of the Hub (a mirror). Both tools need to be told. This program works out the endpoint (the base URL of the Hub), the cache directory, and the offline flag the way huggingface_hub and llama.cpp do. It does this for four sets of environment variables, so you can compare the two tools side by side.
import java.util.Map;
import java.util.Set;
public class HubEnvironment {
static boolean isTrue(String v) { return v != null && Set.of("1", "ON", "YES", "TRUE").contains(v.toUpperCase()); }
static String hubEndpoint(Map<String, String> env) { // huggingface_hub constants.ENDPOINT
return env.getOrDefault("HF_ENDPOINT", "https://huggingface.co").replaceAll("/+$", "");
}
static String llamaEndpoint(Map<String, String> env) { // common_get_model_endpoint in llama.cpp
String e = env.getOrDefault("MODEL_ENDPOINT", env.getOrDefault("HF_ENDPOINT", "https://huggingface.co/"));
return e.endsWith("/") ? e : e + "/";
}
static String hubCache(Map<String, String> env, String home) {
if (env.containsKey("HF_HUB_CACHE")) return env.get("HF_HUB_CACHE");
String hfHome = env.getOrDefault("HF_HOME", env.getOrDefault("XDG_CACHE_HOME", home + "/.cache") + "/huggingface");
return hfHome + "/hub";
}
static String llamaCache(Map<String, String> env, String home) { // hf-cache.cpp get_cache_directory
if (env.containsKey("LLAMA_CACHE")) return env.get("LLAMA_CACHE");
if (env.containsKey("HF_HUB_CACHE")) return env.get("HF_HUB_CACHE");
if (env.containsKey("HUGGINGFACE_HUB_CACHE")) return env.get("HUGGINGFACE_HUB_CACHE");
if (env.containsKey("HF_HOME")) return env.get("HF_HOME") + "/hub";
if (env.containsKey("XDG_CACHE_HOME")) return env.get("XDG_CACHE_HOME") + "/huggingface/hub";
return home + "/.cache/huggingface/hub";
}
public static void main(String[] args) {
String home = "/home/dev";
Map<String, String>[] envs = new Map[] {
Map.of(),
Map.of("HF_HOME", "/data/hf", "HF_HUB_OFFLINE", "1"),
Map.of("HF_ENDPOINT", "https://hub.internal.example/", "TRANSFORMERS_OFFLINE", "yes"),
Map.of("LLAMA_CACHE", "/models", "HF_HUB_OFFLINE", "0")};
for (Map<String, String> env : envs) {
boolean offline = isTrue(env.get("HF_HUB_OFFLINE")) || (!env.containsKey("HF_HUB_OFFLINE") && isTrue(env.get("TRANSFORMERS_OFFLINE")));
System.out.println(new java.util.TreeMap<>(env));
System.out.println(" huggingface_hub: endpoint " + hubEndpoint(env) + ", cache " + hubCache(env, home) + ", offline " + offline);
System.out.println(" llama.cpp: endpoint " + llamaEndpoint(env) + ", cache " + llamaCache(env, home));
}
}
}{}
huggingface_hub: endpoint https://huggingface.co, cache /home/dev/.cache/huggingface/hub, offline false
llama.cpp: endpoint https://huggingface.co/, cache /home/dev/.cache/huggingface/hub
{HF_HOME=/data/hf, HF_HUB_OFFLINE=1}
huggingface_hub: endpoint https://huggingface.co, cache /data/hf/hub, offline true
llama.cpp: endpoint https://huggingface.co/, cache /data/hf/hub
{HF_ENDPOINT=https://hub.internal.example/, TRANSFORMERS_OFFLINE=yes}
huggingface_hub: endpoint https://hub.internal.example, cache /home/dev/.cache/huggingface/hub, offline true
llama.cpp: endpoint https://hub.internal.example/, cache /home/dev/.cache/huggingface/hub
{HF_HUB_OFFLINE=0, LLAMA_CACHE=/models}
huggingface_hub: endpoint https://huggingface.co, cache /home/dev/.cache/huggingface/hub, offline false
llama.cpp: endpoint https://huggingface.co/, cache /modelsThe variables and their effects are:
| Variable | huggingface_hub |
llama.cpp |
|---|---|---|
HF_ENDPOINT |
Base URL of the Hub (default https://huggingface.co, trailing / removed) |
Used when MODEL_ENDPOINT is not set; a trailing / is added |
MODEL_ENDPOINT |
Not used | Base URL, checked first |
HF_HOME |
Root for the token and caches (default ~/.cache/huggingface) |
Cache at $HF_HOME/hub |
HF_HUB_CACHE |
Model cache directory (default $HF_HOME/hub) |
Cache directory, after LLAMA_CACHE |
LLAMA_CACHE |
Not used | Cache directory, checked first |
HF_HUB_OFFLINE |
Values 1, ON, YES, TRUE (any case) disable network calls; TRANSFORMERS_OFFLINE is read when it is unset |
Not used; llama.cpp has the --offline flag |
The four runs in the output show the resolution:
- With no variables, both tools use the default endpoint and the default cache.
- Setting
HF_HOMEmoves both caches.HF_HUB_OFFLINE=1switcheshuggingface_hubto offline mode. HF_ENDPOINTchanges both tools' base URL.TRANSFORMERS_OFFLINE=yesswitcheshuggingface_hubto offline mode becauseHF_HUB_OFFLINEis absent.LLAMA_CACHEmoves only llama.cpp's cache, andHF_HUB_OFFLINE=0keepshuggingface_hubonline.
Offline mode is useful on machines without internet access, in CI, and to guarantee that a run uses exactly the cached revision. An alternative endpoint is used for internal mirrors and proxies that serve the same API. A client must then send the same paths (/api/models/..., /{repo}/resolve/...) to the new base URL.
Example 12: safety: trust_remote_code, pickle files, and the publisher
A model repository is code and data written by someone else, and loading it runs it on your machine. Most files only hold data, but a few kinds can run code. This program scans a file list and a config.json for the two risky patterns. The output has one line for each risky file, so check which file names appear.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
public class RepoSafetyScan {
public static void main(String[] args) throws Exception {
List<String> files = List.of("config.json", "configuration_custom.py", "modeling_custom.py",
"pytorch_model.bin", "model.safetensors", "tokenizer.json", "training_args.bin");
String config = """
{"architectures": ["CustomForCausalLM"],
"auto_map": {"AutoConfig": "configuration_custom.CustomConfig",
"AutoModelForCausalLM": "modeling_custom.CustomForCausalLM"}}
""";
JsonNode c = new ObjectMapper().readTree(config);
for (String f : files) {
if (f.endsWith(".bin") || f.endsWith(".pt") || f.endsWith(".pth") || f.endsWith(".pkl") || f.endsWith(".ckpt"))
System.out.println("pickle file: " + f + " (loading it with torch.load can run code)");
else if (f.endsWith(".py"))
System.out.println("python file: " + f + " (runs only with trust_remote_code=True)");
}
if (c.has("auto_map"))
c.get("auto_map").fields().forEachRemaining(e ->
System.out.println("auto_map " + e.getKey() + " -> " + e.getValue().asText()));
boolean hasSafetensors = files.stream().anyMatch(f -> f.endsWith(".safetensors"));
System.out.println("safetensors weights available: " + hasSafetensors);
}
}python file: configuration_custom.py (runs only with trust_remote_code=True)
python file: modeling_custom.py (runs only with trust_remote_code=True)
pickle file: pytorch_model.bin (loading it with torch.load can run code)
pickle file: training_args.bin (loading it with torch.load can run code)
auto_map AutoConfig -> configuration_custom.CustomConfig
auto_map AutoModelForCausalLM -> modeling_custom.CustomForCausalLM
safetensors weights available: trueTwo kinds of files can run code:
- Pickle files (
.bin,.pt,.pth,.pkl,.ckpt). PyTorch's traditional checkpoint format is Python'spickle, and unpickling can call any Python function. A crafted checkpoint can therefore run commands when it is loaded. The safetensors Format shows why. PyTorch'storch.loadrestricts what it unpickles whenweights_only=True, which is the default since PyTorch 2.6 according to its release notes. Older versions, and code that passesweights_only=False, load the pickle in full.training_args.bin, a file many fine-tuning repositories contain, is also a pickle. - Python files with
auto_map. Whenconfig.jsonhas anauto_mapthat points to modules in the repository (modeling_custom.CustomForCausalLM), Transformers loads that code only if the caller passestrust_remote_code=True. The code then runs with the caller's permissions.
safetensors and GGUF files contain only tensors and metadata, and their readers do not execute anything from the file. A repository that offers them can be used without loading any pickle. llama.cpp never runs Python from a repository.
Run these checks before downloading from an unfamiliar repository:
| Check | Reason |
|---|---|
| Weights in safetensors or GGUF | Avoids pickle deserialization |
No auto_map, or the code was read |
trust_remote_code=True runs that code |
| Publisher is the model's original organization, or a known quantizer whose card names the base model | The base model link (base_model in the card's YAML) and the commit history show where the weights came from |
| Pinned commit | A later commit cannot change what was reviewed |
| File hashes | The Hub lists a SHA-256 for each LFS file; the next lesson reads it |
Interview points
- A Hub model is a git repository with LFS-stored weights. Engines read
config.json, the tokenizer files, and safetensors weights, while llama.cpp reads one GGUF file with all of them inside. config.jsongives the architecture and sizes, and the head dimension, the GQA ratio (query heads per KV head), and the parameter count follow from it.eos_token_idcan be a list ingeneration_config.json, and stopping on only one id runs past the end of turns.tokenizer.jsonholds the model type, vocabulary, merges (as strings or pairs), pre-tokenizer, and added tokens.tokenizer_config.jsonholds the chat template and the special-token names.- Branches move, tags rarely move, and commit hashes never move, so pinning a commit makes downloads reproducible.
- llama.cpp's
-hf repo:quantmatchesQUANT[.-]case-insensitively, defaults toQ4_K_Mand thenQ8_0, skipsmmprojandimatrixfiles, and uses only shard 1 of split files. - Tokens come from
HF_TOKENfirst and then from the token file inHF_HOME, and llama.cpp sends only tokens that start withhf_. - A 401 from the Hub can mean missing, private, or gated without authentication, and the header
X-Error-Code: GatedRepoidentifies gated access. HF_HUB_OFFLINE=1stops network calls inhuggingface_hub, llama.cpp uses--offline, and both share themodels--org--namecache layout.- Pickle checkpoints and
trust_remote_codeare the two ways a repository can run code on load.
Summary
- Weights, tokenizer, and configuration are separate files in a Transformers repository and one file in a GGUF repository.
.gitattributeslists the patterns stored with Git LFS.- A Qwen2-format config with 896 hidden size, 24 layers, and tied embeddings gives about 0.494 billion parameters.
generation_config.jsoncarries the publisher's default sampling settings.- Merges in
tokenizer.jsonare either"a b"strings or["a", "b"]pairs. added_tokens_decodermaps special token ids to their text.bos_token: nullwithadd_bos_token: falsemeans no BOS token is added.- The refs endpoint returns
branches,tags, andconvertswithtargetCommithashes. - A
resolveURL with a commit hash always returns the same bytes. - GGUF split files are named
-NNNNN-of-NNNNN.gguf, and tools select shard 1. hf downloadtakes--include,--exclude,--revision,--cache-dir, and--local-dir.LLAMA_CACHEandMODEL_ENDPOINTare llama.cpp-specific, whileHF_HOME,HF_HUB_CACHE, andHF_ENDPOINTare shared.- safetensors and GGUF files cannot execute code when read, and pickle files can.
