Deep Learning Lab
Guide
Module 7 · Pretrained Models and the Hugging Face Ecosystem Lesson 7.3

Finding and Selecting the Right Model

Classroom material

39 min · 17 code cells · 0 labs

In this lesson
Time
~39 min
Sections
8
Code cells
17
Figures
4

Before you start

Given a problem, how do we find a pretrained model that fits it, and how do we tell whether its advertised performance will hold for us?

You will be able to
  • turn a problem into the questions that narrow down a model search;
  • name the main task types on the Hub and recognise which one a problem belongs to;
  • estimate a model's memory needs and input limit from its metadata, without downloading it;
  • explain how input length drives compute, and when attention's quadratic cost starts to matter;
  • check a model card's claims against your own measurements, and explain why a benchmark score may not transfer;
  • weigh hosted APIs against running models locally.

The Hub holds an enormous number of models, and the number grows every day. That is both the opportunity and the problem. Somewhere there is probably a model close to what you need, but there are also many that look right and aren't: trained for a slightly different task, too big for your hardware, unable to read inputs as long as yours, or advertised with numbers that won't hold on your data.

In Lesson 7.2 we learned what a model repository contains and how to read a model card. This lesson turns that knowledge into a method for choosing. We start from the problem rather than the model, map problems to the kinds of model that solve them, estimate whether a model will fit on your hardware before downloading it, and then read a model card critically, testing its claims rather than trusting them. Lesson 7.7 puts the whole method into practice on a real selection.

Key question: Given a problem, how do we find a pretrained model that fits it, and how do we tell whether its advertised performance will hold for us?

Prerequisites

read Lessons 7.1 and 7.2. You'll use the pipeline API (7.1), model cards and metadata (7.2), parameters × bytes (7.1, Section 6), and attention's cost growing with sequence length (Lesson 6.3).

Setup — imports, versions and device detection used by the notebook (1 cells)

The usual imports, seeds and device check. The same harmless warnings as in the previous notes will appear (TqdmWarning, the unauthenticated-request message and, on a Mac, a few W… lines). This lesson downloads two more models than earlier lessons, a small text generator and a sentence-embedding model, so its first run takes longer.

Environment and reference results. Use Python 3.12 and requirements.txt (also supplied at the root of the Module 7/8 notebook bundle). Install packages into a separate environment, restart the kernel, and run from the beginning. The saved outputs are the author's CPU reference run, with approved explanatory corrections. Seeds and artifact revisions reduce variation; numbers and timings can differ across hardware. No account or paid API is required for the public teaching models. Initial model/data downloads require internet access.

Setup 35 lines
import time, hashlibimport numpy as npimport torchimport torch.nn.functional as Fimport matplotlib.pyplot as pltfrom datasets import load_datasetfrom huggingface_hub import HfApi, ModelCard, get_safetensors_metadatafrom transformers import pipeline, AutoConfig, AutoTokenizer, AutoModelForSequenceClassificationtorch.manual_seed(0)np.random.seed(0)device = "cuda" if torch.cuda.is_available() else "cpu"print("PyTorch", torch.__version__, "| device:", device)# Freeze the public artifacts used by the recorded experiments.MODEL_REVISIONS = {'openai/clip-vit-base-patch32': '3d74acf9a28c67741b2f4f2ea7635f0aaf6f0268', 'distilbert/distilbert-base-uncased-finetuned-sst-2-english': '714eb0fa89d2f80546fda750413ed43d93601a13', 'sentence-transformers/all-MiniLM-L6-v2': '1110a243fdf4706b3f48f1d95db1a4f5529b4d41', 'HuggingFaceTB/SmolLM2-135M': '93efa2f097d58c2a74874c7e644dbc9b0cee75a2', 'distilbert/distilgpt2': '2290a62682d06624634c1f46a6ad5be0f47f38aa', 'HuggingFaceTB/SmolLM2-135M-Instruct': '12fd25f77366fa6b3b4b768ec3050bf629380bac', 'HuggingFaceTB/SmolLM2-360M-Instruct': 'a10cc1512eabd3dde888204e902eca88bddb4951', 'Qwen/Qwen2.5-0.5B': '060db6499f32faf8b98477b0a26969ef7d8b9987', 'Qwen/Qwen2.5-7B': 'd149729398750b98c0af14eb82c78cfe92750796', 'google-bert/bert-large-uncased': '6da4b6a26a1877e173fca3225479512db81a5e5b', 'distilbert/distilbert-base-uncased': '12040accade4e8a0f71eabdb258fecc2e7e948be', 'google-bert/bert-base-uncased': '86b5e0934494bd15c9632b12f734a8a67f723594', 'cardiffnlp/twitter-roberta-base-sentiment-latest': '3216a57f2a0d9c45a2e6c20157c20c49fb4bf9c7', 'distilbert/distilbert-base-cased-distilled-squad': '564e9b582944a57a3e586bbb98fd6f0a4118db7f', 'google/vit-base-patch16-224': '3f49326eb077187dfe1c2a2bb15fbd74e6ab91e3', 'sshleifer/distilbart-cnn-12-6': 'a4f8f3ea906ed274767e9906dbaede7531d660ff', 'textattack/distilbert-base-uncased-imdb': '5b0f46c2fc4b86bf21f0ec0409bed77ee142b332', 'google/gemma-2-2b': 'c5ebcd40d208330abc697524c919956e692655cf', 'meta-llama/Llama-3.2-1B': '4e20de362430cd3b72f300e6b0f18e50e7166e08'}DATASET_REVISIONS = {'stanfordnlp/imdb': 'e6281661ce1c48d982bc483cf8a173c1bbeb5d31', 'stanfordnlp/sst2': '8d51e7e4887a4caaa95b3fbebbf53c0490b58bbb'}def benchmark_clock():    """Read elapsed time after synchronising any queued CUDA work."""    if torch.cuda.is_available():        torch.cuda.synchronize()    import time    return time.perf_counter()def ordered_fingerprint(texts, labels):    """Hash ordered text/label pairs to verify alignment for paired comparisons.    Arguments:        texts: Text strings in the order expected by the caller.        labels: Integer targets aligned with the input examples.    """    import json, hashlib    rows = [[str(t), int(y)] for t, y in zip(texts, labels)]    return hashlib.sha256(json.dumps(rows, ensure_ascii=False, separators=(',', ':')).encode()).hexdigest()
Warnings / log
/Users/solomonpromise/Documents/Simplilearn Classes/Deep Learning/_work78/.venv/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
W1017 13:03:14.797000 73645 site-packages/torch/distributed/elastic/multiprocessing/redirects.py:35] NOTE: Redirects are currently not supported in MacOs.
W1017 13:03:14.889000 73645 site-packages/torch/utils/_pytree.py:630] <enum 'KernelPreference'> is an Enum subclass and is now natively supported by torch.compile as an opaque value type. Calling register_constant() on Enum subclasses is deprecated and will be an error in a future release.
W1017 13:03:14.911000 73645 site-packages/torch/utils/_pytree.py:630] <enum 'ScaleCalculationMode'> is an Enum subclass and is now natively supported by torch.compile as an opaque value type. Calling register_constant() on Enum subclasses is deprecated and will be an error in a future release.
Output
PyTorch 2.14.1 | device: cpu
1

The Big Picture: Matching Models to Problems

A common way to pick a model is to search for something popular and try it. Sometimes it works. More often it wastes an afternoon on a model that was never right for the job. A better habit is to describe the problem first, in a few concrete questions, and let the answers filter the search:

  1. What is the task? What goes in and what must come out: a label for a whole text, a label for each word, a span copied from a passage, new text, a vector, a label for an image? This decides the kind of model (Section 2).
  2. What do the inputs look like? Language, domain (tweets, legal contracts, film reviews), and above all length. A model that reads 512 tokens can't read a 20-page document in one go (Section 3).
  3. What hardware will run it, and how fast must it answer? This sets an upper limit on model size (Section 3).
  4. How good must it be, measured how? And on whose data? A benchmark number is evidence, not a guarantee (Section 4).
  5. Where will it run, and what are you allowed to do with it? On your own machine or through someone else's service (Section 5), and under what licence (Lesson 7.2).

Only after these questions does it make sense to look at specific models. The questions turn "find a good model" into "find a model that does this task, on inputs like these, within this budget, this well, under these terms", which is far easier to answer.

From Problem to Shortlist. Requirements narrow a shortlist; documentation and representative evaluation determine whether a candidate actually fits. Popularity and parameter count alone do not decide.

Figure 7.3.1. Requirements narrow a shortlist; documentation and representative evaluation determine whether a candidate actually fits. Popularity and parameter count alone do not decide.

The first filter is the task itself, so that is where we start.

Checkpoint Before you move on from §1

Where should model selection start?

2

A Map of Common Tasks

Every model on the Hub is tagged with the task it was built for, its pipeline tag, and the Hub's search can filter by it. Here are seven well-known models, one per common task, with the tag the Hub gives each:

Python 8 lines
SST2 = "distilbert/distilbert-base-uncased-finetuned-sst-2-english"MINILM = "sentence-transformers/all-MiniLM-L6-v2"GPT = "distilbert/distilgpt2"task_examples = [SST2, "distilbert/distilbert-base-cased-distilled-squad", "sshleifer/distilbart-cnn-12-6",                 GPT, MINILM, "google/vit-base-patch16-224", "openai/clip-vit-base-patch32"]api = HfApi()for r in task_examples:    print(f"{api.model_info(r, revision=MODEL_REVISIONS.get(r)).pipeline_tag:<31} {r}")       # the task tag from the Hub, no download
Output
text-classification             distilbert/distilbert-base-uncased-finetuned-sst-2-english
question-answering              distilbert/distilbert-base-cased-distilled-squad
summarization                   sshleifer/distilbart-cnn-12-6
text-generation                 distilbert/distilgpt2
sentence-similarity             sentence-transformers/all-MiniLM-L6-v2
image-classification            google/vit-base-patch16-224
zero-shot-image-classification  openai/clip-vit-base-patch32

These tags are the vocabulary of model search. What each means in practice:

Task (pipeline tag) Input → output Typical use
text-classification text → one label for the whole text sentiment, spam, topic, intent
question-answering question + passage → the span of the passage that answers it searching manuals, FAQs, documents
summarization long text → shorter text news digests, meeting notes
text-generation prompt → continuation drafting, chat, code completion
sentence-similarity (also feature-extraction) text → a vector of numbers semantic search, clustering, duplicate detection
image-classification image → one label from a fixed set product photos, medical scans
zero-shot-image-classification image + candidate labels in words → the best label classifying into categories chosen at run time

Notice how the same architecture serves different tasks. The first two models are both DistilBERT, one fine-tuned for classification and one for question answering. What makes them different is the head and the fine-tuning (Lesson 7.1, Section 4). Let's run three of these tasks with small models. Question answering and summarisation stay on the map only, to keep downloads down, and the image tasks get their turn in Lesson 7.5.

Text classification, which you already know:

Python 2 lines
classifier = pipeline("text-classification", model=SST2, device=device, revision=MODEL_REVISIONS.get(SST2))print(classifier("The soundtrack alone is worth the price of the ticket."))
Warnings / log
Loading weights:   0%|          | 0/104 [00:00<?, ?it/s]Loading weights: 100%|██████████| 104/104 [00:00<00:00, 17060.69it/s]
Output
[{'label': 'POSITIVE', 'score': 0.9995796084403992}]

Text generation with distilgpt2, a small version of GPT-2. With do_sample=False, the pipeline always picks the most likely next token: greedy decoding, from Lesson 6.6.

Python 3 lines
generator = pipeline("text-generation", model=GPT, device=device, revision=MODEL_REVISIONS.get(GPT))    # downloads about 353 MB the first timeout = generator("The best thing about this movie is", max_new_tokens=20, do_sample=False)print(out[0]["generated_text"])
Warnings / log
Loading weights:   0%|          | 0/76 [00:00<?, ?it/s]Loading weights: 100%|██████████| 76/76 [00:00<00:00, 8878.56it/s]
Output
The best thing about this movie is that it's a very good movie. It's a very good movie. It's a very good

The model continues the prompt fluently but falls into a loop: "It's a very good movie" twice in 20 tokens. Greedy decoding often repeats like this, exactly as you saw with your own model in Lesson 6.6. Lesson 7.5 tries the sampling strategies that fix it.

Embeddings with all-MiniLM-L6-v2, a small model trained so that sentences with similar meanings get similar vectors. The pipeline returns one vector per token; averaging them gives one vector per sentence.

Python 9 lines
embedder = pipeline("feature-extraction", model=MINILM, device=device, revision=MODEL_REVISIONS.get(MINILM))sentences = ["A thrilling, beautifully acted film.",             "An exciting movie with superb performances.",             "The parcel arrived on Tuesday."]vectors = [torch.tensor(embedder(s)[0]).mean(dim=0) for s in sentences]   # (tokens, 384) -> (384,) per sentenceprint("vector length:", vectors[0].shape[0])for i, j in [(0, 1), (0, 2), (1, 2)]:    print(f"cosine {F.cosine_similarity(vectors[i], vectors[j], dim=0):.3f}  {sentences[i]!r} vs {sentences[j]!r}")del generator, embedder
Warnings / log
Loading weights:   0%|          | 0/103 [00:00<?, ?it/s]Loading weights: 100%|██████████| 103/103 [00:00<00:00, 10536.91it/s]
Output
vector length: 384
cosine 0.734  'A thrilling, beautifully acted film.' vs 'An exciting movie with superb performances.'
cosine -0.007  'A thrilling, beautifully acted film.' vs 'The parcel arrived on Tuesday.'
cosine 0.008  'An exciting movie with superb performances.' vs 'The parcel arrived on Tuesday.'

Each sentence becomes a vector of 384 numbers. The two film sentences share almost no words ("thrilling, beautifully acted film" against "exciting movie with superb performances"), yet their cosine similarity is 0.734, while each scores about zero against the sentence about a parcel (−0.007 and 0.008). The model compares meanings, not words, which is what makes semantic search possible. Lesson 7.5 builds a search engine on it.

Key terms

  • Pipeline tag (task tag): the Hub's label for the task a model was built for, such as text-classification; it is also the name you pass to pipeline(...).
  • Greedy decoding: generating text by always choosing the single most likely next token.
  • Sentence embedding: one vector representing a whole sentence, built so that similar meanings give nearby vectors.
Check your understandingCheck yourself
  1. A support team wants to find past tickets that describe the same problem as a new ticket, even when worded differently. Which task tag would you search for, and why not text-classification?

Knowing the task narrows the search to a few kinds of model. Within a kind, the next filter is whether a model fits your hardware and your inputs.

Checkpoint Before you move on from §2

Which output supports semantic search directly?

3

Model Size, Context Length and Compute Requirements

3.1 Size and memory, before you download

You can find out how big a model is without downloading it. A .safetensors file begins with a short header that lists every tensor's name, shape and number type, and huggingface_hub can read just that header. Combined with each model's configuration (a few kilobytes), it tells us how many numbers a model stores and how long an input it accepts.

Python 12 lines
BYTES = {"F32": 4, "BF16": 2, "F16": 2, "I64": 8}               # bytes per stored number, by data typeladder = [MINILM, "distilbert/distilbert-base-uncased", "google-bert/bert-base-uncased",          "google-bert/bert-large-uncased", "Qwen/Qwen2.5-0.5B", "Qwen/Qwen2.5-7B"]sizes = {}for r in ladder:    meta = get_safetensors_metadata(r)                         # reads only the files' headers    counts = meta.parameter_count                              # {data type: how many numbers}    n = sum(counts.values())    gb = sum(k * BYTES[d] for d, k in counts.items()) / 1e9    ctx = AutoConfig.from_pretrained(r, revision=MODEL_REVISIONS.get(r)).max_position_embeddings   # longest input, in tokens    sizes[r] = n    print(f"{r:<40} {n:>14,} numbers | stored as {'+'.join(counts):<7} | {gb:7.2f} GB | context {ctx:>7,} tokens")
Output
sentence-transformers/all-MiniLM-L6-v2       22,713,728 numbers | stored as I64+F32 |    0.09 GB | context     512 tokens
Warnings / log
Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
Output
distilbert/distilbert-base-uncased           66,985,530 numbers | stored as F32     |    0.27 GB | context     512 tokens
google-bert/bert-base-uncased               110,106,428 numbers | stored as F32     |    0.44 GB | context     512 tokens
google-bert/bert-large-uncased              336,226,108 numbers | stored as F32     |    1.34 GB | context     512 tokens
Qwen/Qwen2.5-0.5B                           494,032,768 numbers | stored as BF16    |    0.99 GB | context  32,768 tokens
Warnings / log
Parse safetensors files:   0%|          | 0/4 [00:00<?, ?it/s]Parse safetensors files:  25%|██▌       | 1/4 [00:00<00:02,  1.08it/s]Parse safetensors files:  50%|█████     | 2/4 [00:01<00:01,  1.12it/s]Parse safetensors files:  75%|███████▌  | 3/4 [00:01<00:00,  1.79it/s]Parse safetensors files: 100%|██████████| 4/4 [00:02<00:00,  2.52it/s]Parse safetensors files: 100%|██████████| 4/4 [00:02<00:00,  1.90it/s]
Output
Qwen/Qwen2.5-7B                           7,615,616,512 numbers | stored as BF16    |   15.23 GB | context 131,072 tokens

That's a size ladder spanning several hundred times, from 22,713,728 stored numbers (MiniLM) to 7,615,616,512 (Qwen2.5-7B, a general-purpose language model).

Three things to read from this table:

  • "Stored as" matters. The BERT models store 32-bit floats (F32, 4 bytes each). The Qwen models store BF16 (bfloat16), a 16-bit format, 2 bytes each. So Qwen2.5-0.5B, with more numbers than BERT-large, takes less disk: 0.99 GB against 1.34 GB. MiniLM also stores a few integers (I64), which are position indexes rather than learned weights. (The header counts everything stored, so its total can differ slightly from the number of trainable parameters.)
  • Context length varies enormously. All four BERT-style encoders accept at most 512 tokens. The Qwen models accept 32,768 and 131,072. For long documents, that alone can decide the choice.
  • Disk size predicts memory. To run a model you must at least hold its weights in memory, so the file size is a lower bound on the memory you need. Running it needs more: activations, and for generation, the cache of past keys and values (Lesson 7.6).

Using the rule from Lesson 7.1, parameters × bytes per parameter, we can estimate the weight memory in different number formats:

Python 2 lines
for r in ladder:    print(f"{r:<40} float32: {sizes[r] * 4 / 1e9:6.2f} GB | float16: {sizes[r] * 2 / 1e9:6.2f} GB")
Output
sentence-transformers/all-MiniLM-L6-v2   float32:   0.09 GB | float16:   0.05 GB
distilbert/distilbert-base-uncased       float32:   0.27 GB | float16:   0.13 GB
google-bert/bert-base-uncased            float32:   0.44 GB | float16:   0.22 GB
google-bert/bert-large-uncased           float32:   1.34 GB | float16:   0.67 GB
Qwen/Qwen2.5-0.5B                        float32:   1.98 GB | float16:   0.99 GB
Qwen/Qwen2.5-7B                          float32:  30.46 GB | float16:  15.23 GB

Qwen2.5-7B needs 30.46 GB just for its weights in float32, or 15.23 GB in a 16-bit format. On a laptop with 16 GB of RAM, even the 16-bit version leaves almost nothing for the operating system and for the computation itself. The small models on this ladder take well under a gigabyte. This kind of arithmetic takes seconds and saves you from downloading 15.23 GB only to watch your machine run out of memory. Lesson 7.6 covers ways to shrink these numbers, such as quantisation.

3.2 Input length and compute

How does input length affect cost? In Lesson 6.3 you saw that attention compares every token with every other token, so its cost grows with the square of the length. But a Transformer layer also contains linear layers, the Q, K, V and output projections and the feed-forward network, whose cost grows only in proportion to the length. Which part dominates depends on the length. We can count the multiplications per token in one DistilBERT layer:

  • the linear parts need about 4·d² + 2·d·d_ff, with d = 768 and d_ff = 3072, the same for every token;
  • attention needs about 2·L·d per token: one dot product with each of the L positions to get the scores, and one weighted sum over L value vectors.
Python 9 lines
cfg = AutoConfig.from_pretrained(SST2, revision=MODEL_REVISIONS.get(SST2))d, d_ff = cfg.dim, cfg.hidden_dim                              # 768 and 3072 for DistilBERTlinear_per_token = 4 * d * d + 2 * d * d_ff                    # Q, K, V, output projections + feed-forwardprint("d =", d, "| d_ff =", d_ff)for L in (128, 512, 2048, 8192):    attention_per_token = 2 * L * d                            # scores (Q·K) and weighted sum over L positions    share = attention_per_token / (attention_per_token + linear_per_token)    print(f"{L:>5} tokens: attention is {share:.1%} of the multiplications in each layer")print("attention equals the linear parts at", linear_per_token // (2 * d), "tokens")
Output
d = 768 | d_ff = 3072
  128 tokens: attention is 2.7% of the multiplications in each layer
  512 tokens: attention is 10.0% of the multiplications in each layer
 2048 tokens: attention is 30.8% of the multiplications in each layer
 8192 tokens: attention is 64.0% of the multiplications in each layer
attention equals the linear parts at 4608 tokens

At 128 tokens, attention is only 2.7% of the work; at 512 tokens, the most DistilBERT accepts, it's 10.0%. The two parts become equal only at 4608 tokens, and attention dominates beyond that (64.0% at 8192). So for short-context models like DistilBERT, cost grows roughly in proportion to length. For long-context models like the Qwen family, which accept inputs of tens of thousands of tokens, the quadratic part takes over. That is why very long inputs are so expensive, and why so much engineering goes into making attention cheaper.

Let's check the prediction by timing the SST-2 model on the same long text, truncated to different lengths, in batches of 16:

Interpret the evidence carefully. The 4,608-token crossover is from this simplified multiplication count for DistilBERT's width and feed-forward expansion. It is not a universal Transformer boundary or a measured runtime crossover. Configured position limits also do not guarantee equal quality at every length.

Python 14 lines
tokenizer = AutoTokenizer.from_pretrained(SST2, revision=MODEL_REVISIONS.get(SST2))model = AutoModelForSequenceClassification.from_pretrained(SST2, revision=MODEL_REVISIONS.get(SST2)).to(device).eval()long_text = "This film was a quiet, patient study of grief. " * 120           # far longer than 512 tokenslengths = [32, 64, 128, 256, 512]per_review = []with torch.inference_mode():    for L in lengths:        enc = tokenizer([long_text] * 16, truncation=True, max_length=L, return_tensors="pt").to(device)  # (16, L)        model(**enc)                                           # warm-up run, not timed        t0 = benchmark_clock()        for _ in range(3):            model(**enc)        per_review.append((benchmark_clock() - t0) / 3 / 16)        print(f"{L:>4} tokens: {per_review[-1] * 1000:.2f} ms per review")
Warnings / log
Loading weights:   0%|          | 0/104 [00:00<?, ?it/s]Loading weights: 100%|██████████| 104/104 [00:00<00:00, 12005.49it/s]
Output
  32 tokens: 2.72 ms per review
  64 tokens: 4.90 ms per review
 128 tokens: 9.95 ms per review
 256 tokens: 19.05 ms per review
 512 tokens: 40.98 ms per review
Python 4 lines
plt.figure(figsize=(6.2, 3.6))plt.plot(lengths, [t * 1000 for t in per_review], "o-")plt.xlabel("tokens per review (after truncation)"); plt.ylabel("milliseconds per review")plt.title("DistilBERT on this CPU: cost grows with input length"); plt.grid(alpha=0.3); plt.show()
Plot output
Plot produced by the code above

The measured time per review rises in a near-straight line with length: each doubling of the token count roughly doubles the time. That's what the multiplication count predicted for this range, where the linear parts dominate. (Your timings will differ from the ones printed here, since they depend on your processor, but the shape of the curve should be the same.) Two practical consequences follow. Truncating reviews to 256 tokens instead of 512 roughly halves inference cost. And if your inputs are long, context length and compute have to be judged together: a model that can read 32,768 tokens will also charge you for reading them.

Key terms

  • Context length (context window): the maximum number of tokens a model can take as input at once.
  • bfloat16 (BF16): a 16-bit number format, 2 bytes per number, widely used for storing and running large models.
  • Weight memory: the memory needed just to hold a model's parameters: parameters × bytes per parameter.
  • Truncation: cutting an input down to a maximum number of tokens.

Common mistake: choosing a model by parameter count alone. Two models with similar parameter counts can differ in storage format (float32 or bfloat16), context length and speed. Check the file sizes, the number types and the context length too.

Check your understandingCheck yourself
  1. A model has 3 billion parameters stored in bfloat16. Estimate its weight memory. Would you expect it to run on a laptop with 8 GB of RAM?
Check your understandingCheck yourself
  1. Attention's cost grows with the square of the input length. Why, then, did doubling the length from 256 to 512 tokens only roughly double DistilBERT's time per review?

A model that fits your task and your hardware still has to be good enough. That is what model cards and benchmarks claim to tell you.

Checkpoint Before you move on from §3

Does parameter storage equal total runtime memory?

4

Reading Model Cards, Benchmarks and Limitations Critically

4.1 Check the claim on its own terms

A benchmark is a fixed, public dataset with an agreed way of scoring, so that different models can be compared on equal terms. SST-2 is one such benchmark, and it is part of a larger collection called GLUE. Our SST-2 model's card makes its claim in two places:

Python 6 lines
card = ModelCard.load(SST2)print("card text:   ", [l.strip() for l in card.text.splitlines() if "accuracy" in l.lower()])for result in card.data.to_dict()["model-index"][0]["results"]:    for m in result["metrics"]:        if m["type"] == "accuracy":            print("card metadata:", result["dataset"]["name"], result["dataset"]["split"], f"accuracy {m['value']:.4f}")
Output
card text:    ['This model reaches an accuracy of 91.3 on the dev set (for comparison, Bert bert-base-uncased version reaches an accuracy of 92.7).']
card metadata: glue validation accuracy 0.9106
card metadata: sst2 train accuracy 0.9886

The card's text says "an accuracy of 91.3 on the dev set" (dev set is another name for validation set). Its metadata says 0.9106 on the GLUE validation split. These don't quite agree: 91.3 in the text against 0.9106 in the metadata. Small inconsistencies like this are common, because card text and metadata are often written at different times or from different runs. They are a first hint to verify rather than copy.

Let's verify it ourselves. The SST-2 validation set is small and public:

Python 4 lines
sst2 = load_dataset("stanfordnlp/sst2", cache_dir="data/sst2",                    data_files={"validation": "data/validation-*.parquet"}, revision=DATASET_REVISIONS.get("stanfordnlp/sst2"))["validation"]print(sst2)print(sst2[0])
Output
Dataset({
    features: ['idx', 'sentence', 'label'],
    num_rows: 872
})
{'idx': 0, 'sentence': "it 's a charming and often affecting journey . ", 'label': 1}

The validation set has 872 sentences. Look at the first one: it is lowercase, and there are spaces before the punctuation (it 's, journey .). Benchmark text has often been preprocessed, and may look quite different from what your users will type. Now we run the model on all 872:

Python 4 lines
out = classifier(list(sst2["sentence"]), batch_size=32)pred = np.array([o["label"] == "POSITIVE" for o in out])sst2_acc = (pred == np.array(sst2["label"]).astype(bool)).mean()print(f"SST-2 validation, {len(sst2)} sentences: accuracy {sst2_acc:.4f}")
Output
SST-2 validation, 872 sentences: accuracy 0.9106

We get 0.9106, exactly the figure in the card's metadata. So the claim is honest, for SST-2. That's the first step of critical reading: confirm that a claim can be reproduced on its own terms. The next step is to ask whether it holds on yours.

4.2 Check it on your data

Our data is IMDb, with the fixed split from Lesson 7.1. The function is copied here so this notebook stands alone, and the fingerprints confirm it gives exactly the same reviews.

Python 49 lines
def load_imdb_split(seed=0, n_val=2500, n_eval=1000, verbose=True):    """Load the declared IMDb revision, remove exact duplicates and construct the seeded course splits.    Arguments:        seed: Seed controlling the local split or sampling generator.        n_val: Number of training-half reviews reserved for validation.        n_eval: Size of the reproducible evaluation subset.        verbose: Whether to print dataset audit counts.    """    ds = load_dataset("stanfordnlp/imdb", cache_dir="data/imdb",                      data_files={"train": "plain_text/train-*.parquet",                                  "test":  "plain_text/test-*.parquet"}, revision=DATASET_REVISIONS.get("stanfordnlp/imdb"))    def dedup(texts, labels, seen):        """Keep the first occurrence of each exact text not already in the supplied seen set.        Arguments:            texts: Text strings in the order expected by the caller.            labels: Integer targets aligned with the input examples.            seen: Set of exact texts already retained.        """        keep = []        for i, t in enumerate(texts):            if t not in seen:                              # keep only the first copy of each review                seen.add(t); keep.append(i)        return [texts[i] for i in keep], np.array([labels[i] for i in keep])    tr_x, tr_y = dedup(list(ds["train"]["text"]), list(ds["train"]["label"]), set())    te_x, te_y = dedup(list(ds["test"]["text"]), list(ds["test"]["label"]), set(tr_x))  # also drops test reviews already in train    if verbose:        print(f"official split: {len(ds['train'])} train, {len(ds['test'])} test reviews")        print(f"duplicates removed: {len(ds['train']) - len(tr_x)} from train, "              f"{len(ds['test']) - len(te_x)} from test (repeats, or reviews that are also in train)")    clean = lambda xs: [x.replace("<br />", " ") for x in xs]                         # remove HTML line breaks    rng = np.random.default_rng(seed)    p = rng.permutation(len(tr_x)); v, t = p[:n_val], p[n_val:]                       # validation is carved from train    q = rng.permutation(len(te_x))[:n_eval]                                           # a fixed sample of the test set    split = {"train": (clean([tr_x[i] for i in t]), tr_y[t]),             "val":   (clean([tr_x[i] for i in v]), tr_y[v]),             "test":  (clean(te_x), te_y)}    split["val_eval"] = (split["val"][0][:n_eval], split["val"][1][:n_eval])    split["test_eval"] = (clean([te_x[i] for i in q]), te_y[q])    return splitdef fingerprint(texts):    """Hash sorted texts to identify membership; this does not check row order or labels.    Arguments:        texts: Text strings in the order expected by the caller.    """    return hashlib.sha256("\n".join(sorted(texts)).encode()).hexdigest()[:12]
Python 3 lines
imdb = load_imdb_split()for name, (x, y) in imdb.items():    print(f"{name:<9} {len(x):>6} reviews | positive share {y.mean():.3f} | fingerprint {fingerprint(x)}")
Output
official split: 25000 train, 25000 test reviews
duplicates removed: 96 from train, 322 from test (repeats, or reviews that are also in train)
train      22404 reviews | positive share 0.502 | fingerprint fb3a1229f951
val         2500 reviews | positive share 0.492 | fingerprint 07758bc36c9b
test       24678 reviews | positive share 0.503 | fingerprint 429c5557b6d4
val_eval    1000 reviews | positive share 0.495 | fingerprint 9934ffeaac91
test_eval   1000 reviews | positive share 0.515 | fingerprint fc9fa98701bc

The fingerprints match Lesson 7.1's. Now the same model on the 1,000 val_eval reviews, truncated to 256 tokens:

Python 5 lines
val_x, val_y = imdb["val_eval"]out = classifier(val_x, truncation=True, max_length=256, batch_size=32)   # reviews cut to 256 tokensimdb_pred = np.array([o["label"] == "POSITIVE" for o in out])imdb_right = imdb_pred == val_y.astype(bool)print(f"IMDb val_eval, {len(val_x)} reviews: accuracy {imdb_right.mean():.4f}")
Output
IMDb val_eval, 1000 reviews: accuracy 0.8820

Accuracy drops from 0.9106 on SST-2 to 0.8820 on IMDb validation. Same task (positive or negative), same domain (films), a few points lower. Why? The obvious difference is length:

Python 6 lines
sst2_tokens = np.array([len(ids) for ids in tokenizer(list(sst2["sentence"]))["input_ids"]])imdb_tokens = np.array([len(ids) for ids in tokenizer(val_x)["input_ids"]])print(f"median tokens: SST-2 sentences {np.median(sst2_tokens):.0f} | IMDb reviews {np.median(imdb_tokens):.0f}")short = imdb_tokens <= 256print(f"IMDb reviews that fit in 256 tokens:  {short.sum():>4} | accuracy {imdb_right[short].mean():.4f}")print(f"IMDb reviews cut off at 256 tokens:   {(~short).sum():>4} | accuracy {imdb_right[~short].mean():.4f}")
Warnings / log
[transformers] Token indices sequence length is longer than the specified maximum sequence length for this model (756 > 512). Running this sequence through the model will result in indexing errors
Output
median tokens: SST-2 sentences 24 | IMDb reviews 214
IMDb reviews that fit in 256 tokens:   601 | accuracy 0.9068
IMDb reviews cut off at 256 tokens:    399 | accuracy 0.8446

The median SST-2 sentence is 24 tokens long; the median IMDb review is 214. The model was fine-tuned on single sentences and is now reading whole reviews, often with mixed opinions: a reviewer may praise the acting and still hate the film. On the 601 reviews that fit within 256 tokens, accuracy is 0.9068, close to the card's figure. On the 399 that were cut off, it is 0.8446.

Be careful about what this does and doesn't show. It's tempting to say "truncation costs about six points", but the two groups differ in more than truncation: long reviews may also be more nuanced, more sarcastic or simply harder. This comparison shows that the model does worse on long reviews; it doesn't separate why. Lesson 7.7 measures the effect of the truncation length directly, by running the same reviews at different lengths.

4.3 What the model has seen: contamination

There's one more question to ask of any benchmark number: could the model have seen the test data during training? If so, its score measures memory, not ability. This is called contamination, and it can happen at either training stage.

Python 2 lines
print("DistilBERT base was pretrained on:", ModelCard.load("distilbert/distilbert-base-uncased").data.get("datasets"))print("this checkpoint was fine-tuned on:", card.data.get("datasets"))
Output
DistilBERT base was pretrained on: ['bookcorpus', 'wikipedia']
this checkpoint was fine-tuned on: ['sst2', 'glue']

DistilBERT was pretrained on bookcorpus (a collection of books) and wikipedia, so IMDb reviews are unlikely to be in its pretraining data, and it was fine-tuned on SST-2's training split, not on its validation split. For this model, both our measurements are clean. But the question needs asking every time:

  • Models pretrained on large web crawls may have read IMDb pages, and popular benchmarks themselves often end up in crawled data. A suspiciously high score on a famous dataset is a warning sign.
  • Models fine-tuned on a dataset will score well on any split they were trained on. If you carve your validation set from IMDb's training half, as we did, a model fine-tuned on IMDb's training half has seen it. Lesson 7.7 meets exactly this case.

Read the card's training-data section, and if it doesn't say what the model was trained on, treat its benchmark scores with suspicion.

Three Readings of One Model. The card text, structured metadata, and reproduced SST-2 score refer to a different evaluation context from the IMDb run. All displayed numbers come from the source reference results.

Figure 7.3.2. The card text, structured metadata, and reproduced SST-2 score refer to a different evaluation context from the IMDb run. All displayed numbers come from the source reference results.

Key terms

  • Benchmark: a fixed public dataset and scoring rule used to compare models.
  • GLUE: a widely used collection of English language-understanding benchmarks, including SST-2.
  • Dev set: another name for a validation set.
  • Domain shift (distribution shift): a difference between the data a model was trained or evaluated on and the data it meets in use.
  • Contamination: test or benchmark data appearing in a model's training data, which inflates its scores.

Common mistake: treating a lower score on your data as a sign that you made a mistake. A drop from a card's benchmark to your data is normal; it measures the distance between the two datasets. Verify your pipeline on the benchmark itself, as we did, and then believe your own number.

Check your understandingCheck yourself
  1. The card's text says 91.3 and its metadata says 0.9106. You measured 0.9106 on SST-2 and 0.8820 on IMDb. Which number belongs in a report about using this model for IMDb reviews, and why?
Check your understandingCheck yourself
  1. Reviews cut off at 256 tokens scored 0.8446, against 0.9068 for reviews that fit. A colleague concludes that using 512 tokens would raise the cut-off group's accuracy to about 0.9068. What's wrong with that reasoning?

So far every model has run on our own computer. The last filter is where the model should run at all.

Checkpoint Before you move on from §4

Why reproduce a card's benchmark then test your data?

5

Hosted APIs vs. Local Models

There are two ways to use a pretrained model:

  • Locally: you download the weights and run them on hardware you control, a laptop, a server or a cloud machine you rent. Everything in this module so far has been local.
  • Through a hosted API: someone else runs the model on their servers. Your code sends the input over the internet and receives the output. Hugging Face, the major AI labs and cloud providers all offer this. An API (application programming interface) is simply a defined way for one program to ask another for something.

A hosted call looks like this. The listing is shown but not run, because it needs an account and an access token, a secret key that identifies you:

from huggingface_hub import InferenceClient

client = InferenceClient(token="hf_...")        # your access token, from your Hugging Face account settings
result = client.text_classification(
    "The soundtrack alone is worth the price of the ticket.",
    model="distilbert/distilbert-base-uncased-finetuned-sst-2-english",
)
print(result)                                   # labels and scores, computed on a remote server

Which models are available this way, from which providers and at what price changes often, so check the provider's current documentation. The trade-offs, though, are stable:

Question Local model Hosted API
Setup Download weights, install libraries, provide hardware An account, a token and a few lines of code
Which models Any model whose weights you can download Whatever the provider offers, including models too big to run yourself
Cost Hardware and electricity, roughly fixed Pay per request or per token, growing with use
Speed No network delay; limited by your hardware Network round trip on every call; powerful hardware behind it
Privacy Data never leaves your machine Data is sent to the provider; check their terms
Versioning You pin the exact weights (Lesson 7.1, Section 7) The provider may update or retire a model
Availability Works offline; you handle failures Depends on the provider's uptime and rate limits

Neither is better in general. A prototype, or a feature that needs a model far bigger than your hardware, often starts on a hosted API. A product with steady high volume, strict privacy rules or a need for exactly reproducible behaviour often ends up local. Many teams use both: a hosted model to explore, and a local model once they know what they need. Lesson 7.6 returns to this choice with measured costs.

Key terms

  • Hosted API: a service that runs a model on the provider's servers and returns results to your code over the internet.
  • Access token: a secret key that identifies you to a service; keep it out of notebooks and code you share.
  • Rate limit: a cap on how many requests a service accepts from you in a given time.

Common mistake: pasting an access token directly into a notebook and then sharing the notebook. Anyone who reads it can use your account. Store tokens in an environment variable, or log in once with hf auth login, and keep them out of your code.

Check your understandingCheck yourself
  1. A hospital wants to classify doctors' notes by urgency. A hosted API offers a model that scores slightly better than the best local one. List two questions you would need answered before choosing the hosted option.
Checkpoint Before you move on from §5

What helps compare hosted and local inference?

Putting It All Together

Let's reassemble the selection method.

Start from the problem, not the model. Five questions (task, inputs, hardware and speed, quality on whose data, deployment and licence) filter the Hub's huge catalogue down to a shortlist.

The task maps to a pipeline tag: classification, question answering, summarisation, generation, embeddings, image classification or zero-shot image classification. Models of the same architecture family can serve different tasks with different heads. We ran three: the classifier, a generator that fell into a loop under greedy decoding, and an embedding model that rated two film sentences with no words in common at 0.734 similarity.

Size and context can be checked before downloading. Safetensors headers gave stored-number counts from 22,713,728 to 7,615,616,512, and parameters × bytes turned them into memory: Qwen2.5-7B needs 15.23 GB in a 16-bit format, too much for a typical laptop. Context lengths ranged from 512 to 131,072 tokens. For a 512-token encoder, attention is only 10.0% of the multiplications, so cost grows roughly in proportion to length, as our timings confirmed. Beyond 4608 tokens, attention's quadratic cost dominates.

Quality claims need two checks. First, reproduce the claim on its own terms: our run on SST-2 validation gave 0.9106, matching the card's metadata (though not its text, which says 91.3). Then measure on your data: on IMDb validation the same model scored 0.8820, with 0.9068 on reviews that fit in 256 tokens and 0.8446 on those that didn't. And always ask what the model saw during training, because contamination inflates scores.

Where it runs is the final choice: local models give control, privacy and fixed costs; hosted APIs give convenience and access to bigger models, at a per-use cost and with your data leaving your hands.

Check How Example from this lesson
Task pipeline tag on the Hub sentence-similarity for semantic search
Memory safetensors header × bytes per number Qwen2.5-7B: 15.23 GB in 16-bit
Input limit max_position_embeddings in the config DistilBERT 512, Qwen2.5-7B 131,072
Claim reproduced? run on the card's own benchmark SST-2 validation: 0.9106 = card metadata
Holds on your data? run on your validation set IMDb val_eval: 0.8820
Contamination? card's training-data section pretrained on bookcorpus, wikipedia
The Model-Selection Checklist. Check interface, parameter storage versus runtime memory, context, benchmark protocol, training exposure and deployment terms before adopting a model.

Figure 7.3.3. Check interface, parameter storage versus runtime memory, context, benchmark protocol, training exposure and deployment terms before adopting a model.

Summary

  • Choose models by describing the problem first: task, inputs, hardware and speed, quality on your data, deployment and licence.
  • Each Hub model has a pipeline tag naming its task. Common tasks include text classification, question answering, summarisation, text generation, sentence similarity, image classification and zero-shot image classification.
  • Greedy text generation can loop ("It's a very good movie" twice in 20 tokens); sentence embeddings compare meaning, not words (0.734 for two film sentences with no shared words).
  • Safetensors headers reveal a model's size without downloading it; parameters × bytes per number estimates weight memory. Qwen2.5-7B needs 30.46 GB in float32 or 15.23 GB in 16 bits.
  • Context length ranged from 512 tokens (the BERT family) to 131,072 (Qwen2.5-7B).
  • At encoder-sized inputs, linear layers dominate compute: attention is 10.0% of the multiplications at 512 tokens and equals the rest only at 4608. DistilBERT's measured time grew roughly in proportion to length.
  • Verify a card's claim on its own benchmark, then measure on your data. The SST-2 model scored 0.9106 on SST-2 validation (matching its metadata) but 0.8820 on IMDb validation: 0.9068 on reviews that fit in 256 tokens, 0.8446 on those cut off.
  • Ask what a model was trained on: contamination inflates scores.
  • Hosted APIs offer convenience and bigger models, with per-use costs and data leaving your control; local models offer privacy, fixed costs and pinned versions.

Glossary

Term Meaning
Pipeline tag The Hub's label for the task a model performs, also used as the pipeline task name.
Text classification Assigning one label to a whole text.
Question answering Finding the span of a passage that answers a question.
Summarisation Producing a shorter version of a long text.
Text generation Continuing a prompt with new text.
Sentence embedding One vector for a whole sentence, placing similar meanings close together.
Zero-shot image classification Classifying an image into labels given as words at run time.
Greedy decoding Generating by always choosing the most likely next token.
Safetensors header The start of a safetensors file, listing each tensor's name, shape and number type.
bfloat16 (BF16) A 16-bit number format, 2 bytes per number.
Weight memory Memory needed to hold a model's parameters: parameters × bytes per parameter.
Context length The maximum number of tokens a model accepts at once.
Truncation Cutting an input down to a maximum number of tokens.
Benchmark A fixed public dataset and scoring rule for comparing models.
GLUE A collection of English language-understanding benchmarks, including SST-2.
Dev set Another name for a validation set.
Domain shift A difference between the data a model was built on and the data it meets in use.
Contamination Evaluation data appearing in a model's training data, inflating its scores.
Hosted API A service that runs a model remotely and returns results over the internet.
Access token A secret key that identifies you to a service.
Rate limit A cap on how many requests a service accepts in a given time.

Wrap-up

Knowledge check

Quick check

1

Where should model selection start?

2

Does parameter storage equal total runtime memory?

3

What helps compare hosted and local inference?

Questions and discussion

Ask about this lesson, share what you found in a lab, or help someone else. Posts are public on the course's GitHub Discussions: anyone can read them, and you need a GitHub account to post.

Opening it loads giscus from giscus.app.