đ¤ LLM SDK & Hugging Face â What's Running Under the Hood¶
Why this doc exists: You never write
import transformersorimport torchdirectly in yoursrc/code â but thellm_sdkpackage does, and understanding what it's doing makes debugging (and the first slow run!) much less mysterious. This doc explains Hugging Face, PyTorch basics, and walks through the actualllm_sdk/__init__.pyline by line.
Table of Contents¶
- What is Hugging Face?
- The Hugging Face Hub
- AutoModelForCausalLM & AutoTokenizer
- PyTorch â The Bare Minimum
- Walking Through llm_sdk/init.py
- Why the First Run Is Slow
- Where Files Get Cached
- What You're Forbidden From Using Directly
What is Hugging Face?¶
Hugging Face is a company and open-source ecosystem that hosts trained AI models, datasets, and the Python libraries used to load and run them. Think of it like GitHub, but specifically for AI models.
Three things Hugging Face provides that matter for this project:
| Thing | What it is |
|---|---|
| The Hub | A website + API where models are hosted (huggingface.co) |
transformers |
The Python library that loads and runs models like Qwen3 |
huggingface_hub |
The Python library that downloads files from the Hub |
You don't interact with any of these directly in your src/ code â they're forbidden by the subject. But llm_sdk uses all three internally.
The Hugging Face Hub¶
The Hub is where Qwen/Qwen3-0.6B actually lives: huggingface.co/Qwen/Qwen3-0.6B
What "Qwen/Qwen3-0.6B" means¶
This string is a repo ID â it identifies a specific model repository on the Hub, in the format organization/model-name:
Qwen/Qwen3-0.6B
â â
â âââ model name
âââ organization that published it (Alibaba's Qwen team)
What's inside a model repo¶
A typical Hugging Face model repo contains several files:
Qwen/Qwen3-0.6B/
âââ config.json â model architecture settings (layers, hidden size, etc.)
âââ model.safetensors â the actual trained weights (the "brain")
âââ tokenizer.json â fast tokenizer data
âââ tokenizer_config.json â tokenizer settings
âââ vocab.json â the vocabulary file you load with get_path_to_vocab_file()
âââ merges.txt â BPE merge rules (used during tokenizer training)
âââ special_tokens_map.json â maps special token names to their string forms
This is why get_path_to_vocab_file() and get_path_to_merges_file() in llm_sdk exist â they're downloading specific files out of this repo for you to inspect directly.
AutoModelForCausalLM & AutoTokenizer¶
These are the two main classes from the transformers library that llm_sdk uses to load the model. You'll see them in the SDK's __init__.py.
AutoTokenizer¶
Loads the tokenizer for a given model â the component that converts text â token IDs (covered in detail in TOKENIZATION.md).
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
The word "Auto" means it automatically figures out which kind of tokenizer to load based on the model's config â you don't need to know if it's BPE, SentencePiece, or something else.
AutoModelForCausalLM¶
Loads the model weights for causal language modelling â the "predict the next token" task that LLMs do.
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B")
"Causal" means the model only looks at previous tokens to predict the next one â it can't see into the future of the sequence. This is the standard setup for text-generation models like Qwen, GPT, and Llama.
Why "Auto"?¶
Both classes are smart wrappers â given just the repo ID, they read the config.json from the Hub and instantiate the correct underlying Python class automatically. This is why llm_sdk can support "other models as long as your project works with Qwen3-0.6B" (per the bonus part) â swapping the model_name string is often enough.
PyTorch â The Bare Minimum¶
You don't write PyTorch code in this project, but llm_sdk is built on it, and a few concepts will help you read its source and debug issues.
Tensors¶
A torch.Tensor is PyTorch's array type â similar to a numpy array, but capable of running on GPUs and tracking gradients (not relevant here since we're only doing inference).
This is why model.encode() doesn't return a plain Python list:
ids = model.encode("hello")
print(type(ids)) # <class 'torch.Tensor'>
print(ids.shape) # torch.Size([1, 2]) â 2D! batch dimension + sequence dimension
To get a plain Python list of ints, you convert it:
ids_list = ids[0].tolist() # [0] removes the batch dimension, tolist() converts
This is a common bug source. If you forget
[0].tolist()and try to use the tensor directly as a list of IDs, you'll get confusing errors or wrong behaviour. Always convert immediately after callingencode().
Device: cpu / cuda / mps¶
PyTorch can run computations on different hardware:
| Device | Meaning |
|---|---|
"cpu" |
Regular processor â always available, slower |
"cuda" |
NVIDIA GPU â much faster, requires NVIDIA drivers + CUDA installed |
"mps" |
Apple Silicon GPU (M1/M2/M3 Macs) â fast on Mac |
The llm_sdk auto-detects which is available:
if torch.backends.mps.is_available():
device = "mps"
elif torch.cuda.is_available():
device = "cuda"
else:
device = "cpu"
If you saw a warning like "CUDA initialization: The NVIDIA driver on your system is too old" â that's PyTorch trying cuda, failing gracefully, and falling back to cpu. It's just a warning, not an error; the model still runs, just slower.
torch.no_grad()¶
with torch.no_grad():
out = self._model(input_ids=input_tensor)
During training, PyTorch tracks every operation so it can compute gradients later (used to update the model's weights). Since this project only runs inference â using the model, not training it â gradient tracking is unnecessary overhead. torch.no_grad() turns it off, making inference faster and using less memory.
dtype: float16 vs float32¶
dtype = torch.float16 if self._device in ["cuda", "mps"] else torch.float32
This controls the numerical precision used for the model's weights:
float32â standard precision, more memory, used on CPU for compatibilityfloat16â half precision, less memory, faster on GPU, tiny accuracy trade-off
You don't need to change this â llm_sdk picks the right one automatically based on your device.
Walking Through llm_sdk/init.py¶
Here's what each part of the provided SDK actually does, mapped to the concepts above.
Initialization¶
def __init__(self, model_name: str = "Qwen/Qwen3-0.6B", ...):
self._tokenizer = AutoTokenizer.from_pretrained(model_name, ...)
self._model = AutoModelForCausalLM.from_pretrained(model_name, ...)
self._model.to(self._device)
self._model.eval()
- Downloads (or loads from cache) the tokenizer for
Qwen/Qwen3-0.6B - Downloads (or loads from cache) the model weights
- Moves the model onto the selected device (cpu/cuda/mps)
- Switches to
eval()mode â disables training-specific behaviours like dropout, ensuring consistent output
encode()¶
def encode(self, text: str) -> torch.Tensor:
ids = self._tokenizer.encode(text, add_special_tokens=False)
return torch.tensor([ids], device=self._device, dtype=torch.long)
add_special_tokens=Falseâ doesn't automatically insert things like<|im_start|>for you; you control the prompt format yourself[ids]â wraps the list in another list, creating the 2D shape(1, sequence_length)â the1is the batch size (you're only ever processing one prompt at a time, hence batch size 1)
get_logits_from_input_ids()¶
def get_logits_from_input_ids(self, input_ids: list[int]) -> list[float]:
input_tensor = torch.tensor([input_ids], device=self._device, dtype=torch.long)
with torch.no_grad():
out = self._model(input_ids=input_tensor)
logits = out.logits[0, -1].tolist()
return [float(x) for x in logits]
Breaking this down:
torch.tensor([input_ids], ...)â same batch-dimension wrapping asencode()self._model(input_ids=input_tensor)â runs a forward pass through the neural networkout.logitsâ shape is(batch, sequence_length, vocab_size)â logits for every position in the sequence, for every token in the vocabularyout.logits[0, -1]â[0]takes the first (only) batch item,[-1]takes the last position â i.e. the prediction for what comes next, after everything you fed in.tolist()â converts the tensor back to a plain Python list
This is the method you call at every step of your constrained decoding loop.
get_path_to_vocab_file()¶
def get_path_to_vocab_file(self) -> str:
vocab_file_name = self._tokenizer.vocab_files_names.get('vocab_file', "vocab.json")
vocab_path = hf_hub_download(repo_id=self._model_name, filename=vocab_file_name)
return vocab_path
hf_hub_download is a function from huggingface_hub that downloads a specific file from a specific repo on the Hub â and caches it locally, so subsequent calls don't re-download.
Why the First Run Is Slow¶
The first time you run anything that creates a Small_LLM_Model(), it needs to download:
- The model weights (
model.safetensors) â roughly 1.2GB for Qwen3-0.6B - The tokenizer files (
tokenizer.json,vocab.json,merges.txt) - The config files
This can take a few minutes depending on your connection. Subsequent runs are fast because everything is cached locally â the SDK won't re-download files it already has.
First run: uv run python3 test.py â âŗ downloading... (1-5 minutes)
Second run: uv run python3 test.py â ⥠instant (loads from cache)
If you see no output for a while on the first run, that's expected â it's downloading in the background.
Where Files Get Cached¶
By default, Hugging Face caches downloaded files in:
Linux/macOS: ~/.cache/huggingface/hub/
Windows: C:\Users\<you>\.cache\huggingface\hub\
Inside, you'll find a folder named after the repo:
~/.cache/huggingface/hub/models--Qwen--Qwen3-0.6B/
If something seems broken (corrupted download, wrong version), you can safely delete this folder and let it re-download on the next run. This is also useful to know for your .gitignore â these files should never be committed to your repository; they're large binary downloads, not your code.
What You're Forbidden From Using Directly¶
The subject is explicit about this â re-stating it here because it's directly tied to everything above:
"The use of dspy (or any similar package) is completely forbidden including pytorch, huggingface package, transformers, outlines, etc."
This means in your src/ code:
# â Forbidden in src/
import torch
import transformers
from transformers import AutoModelForCausalLM
# â
Allowed â going through the provided SDK
from llm_sdk import Small_LLM_Model
model = Small_LLM_Model()
model.encode(...)
model.get_logits_from_input_ids(...)
You only ever touch llm_sdk's public methods. Everything in this document is background knowledge to help you understand what's happening when you call those methods â not something you implement or import yourself.
Also forbidden: any private methods or attributes from
llm_sdk(anything starting with_, likeself._modelorself._tokenizer). Stick to the public interface:encode,decode,get_logits_from_input_ids,get_path_to_vocab_file,get_path_to_merges_file,get_path_to_tokenizer_file.
See also: LLM_GUIDE.md for the conceptual explanation of LLMs, tokens, and logits.
See also: TOKENIZATION.md for how the vocabulary file is structured and used.
Official docs: huggingface.co/docs/transformers, pytorch.org/docs