Build with AI

How do you turn a model checkpoint into a working app?

Written as of · Published · Updated · By Robert Scales, with AI tools · 4 min read

Short answer

Package the weights in a safe format with a config, a version and a fingerprint, test that it gives the same numbers as the training checkpoint, then wrap it in a small server with input limits and a health check, and measure latency under load. In a measured run, a tiny model served six requests per second, and with 16 simultaneous users each waited 2.6 seconds at the median.

A checkpoint is a file of numbers that only makes sense with the exact code and settings that produced it. An app is something a person can use on a bad day without reading your notebook. The gap between the two is short for a small model, and much of it is ordinary software care.

This project takes the tiny model from project six and carries it across. The script 14_checkpoint_to_app.py does every step and then shuts its server down, using only Python's standard library, PyTorch and safetensors, on this computer's local address (127.0.0.1). It took 26 seconds on an Apple M5 Max.

Step 1: package it

A training checkpoint made with torch.save uses Python's pickle format, and loading a pickle can run arbitrary code. safetensors stores only the tensors. The script saves the weights there and writes a small description next to them.

import hashlib, json
from pathlib import Path
from safetensors.torch import load_file, save_file

def sha(path):
    return hashlib.sha256(Path(path).read_bytes()).hexdigest()

def package(state, cfg, chars, folder):
    Path(folder).mkdir(parents=True, exist_ok=True)
    state = {k: v.clone() for k, v in state.items() if k != "out.weight"}   # shared with tok.weight
    save_file(state, f"{folder}/model.safetensors")
    meta = {"name": "mini-gpt-anne", "version": "1.0.0", "cfg": cfg, "chars": chars,
            "limits": "Writes novel-like nonsense. Not for facts.",
            "sha256": sha(f"{folder}/model.safetensors")}
    Path(f"{folder}/model.json").write_text(json.dumps(meta, ensure_ascii=False))

The limits line matters. Whoever picks this up later should learn what it must not be used for from the file itself. If you plan to share it with others, the licence and model card have their own steps.

The size barely changed: 3.29 MB for the training checkpoint and 3.28 MB packaged. Larger checkpoints that also store optimizer state shrink more, since the app needs only the weights.

Step 2: prove it is the same model

Three tests, each short. Keep them with the app and run them every time something changes.

TestMethodMeasured here
Same numbersCompare outputs of the checkpoint and the packaged model on random inputLargest difference 0.0
Same textSame prompt and seed, twiceIdentical
Refuses damageFlip one bit in a copy of the file, then loadRefused: fingerprint does not match

Step 3: wrap it in a small server

The server has three routes: / for a small web page, /health for a status check and /generate for the model. Most of its code does not concern the model at all. It concerns what a request may ask for.

MAX_CHARS, MAX_PROMPT = 400, 200

def do_POST(self):
    length = int(self.headers.get("Content-Length", 0))
    if self.path != "/generate":
        return self.send(404, {"error": "not found"})
    if length > 2000:
        return self.send(413, {"error": "request too large"})
    try:
        req = json.loads(self.rfile.read(length))
        prompt, n, seed = str(req.get("prompt", "")), int(req.get("chars", 100)), int(req.get("seed", 1))
    except (ValueError, TypeError):
        return self.send(400, {"error": "send JSON with prompt and chars"})
    if not prompt or len(prompt) > MAX_PROMPT or not 1 <= n <= MAX_CHARS:
        return self.send(422, {"error": f"prompt 1-{MAX_PROMPT} characters, chars 1-{MAX_CHARS}"})
    with LOCK:                               # one generation at a time; others wait
        text = generate(model, meta, prompt, n, seed)
    self.send(200, {"text": text, "version": meta["version"]})

Without the limits, one request for 99,999 characters would tie up the machine for everyone. Tested through the real server: a good request returned 200, an empty prompt 422, too many characters 422, an unknown path 404. The answer through the server matched the earlier saved text exactly, which also proves that the whole chain works.

Python's documentation warns that http.server is not recommended for production, since it implements only basic security checks. It is a good teaching tool, on your own machine. Do not expose it to the internet.

Step 4: measure it with several people at once

One user tells you little. Measured here with 100 generated characters per request on the CPU. p50 is the median wait, half of requests were faster. p95 is what the slowest one in twenty faced.

Users at onceRequestsMedian waitp95 waitRequests per secondErrors
1200.164 s0.169 s6.170
4400.707 s0.745 s5.600
16802.642 s2.813 s6.090

Throughput stays flat at about six per second and waiting grows in step with the number of users. That comes straight from the LOCK line: requests form a line, and nothing fails. It is the normal first shape of a served model. Your options are to batch several requests together, run several copies or generate fewer characters. The process peaked at 250 MB. For a real language model, a dedicated server such as vLLM or llama.cpp does batching and streaming for you, and these same four steps still apply.

Try it: Remove the LOCK from the server, rerun the load test and compare requests per second and errors with the table. Then decide if you would keep it, and write down why.

What is still missing before real people use it?

This project stops at a local demonstration, and nothing below was tested. Before you let anyone outside use an app like this, you need:

AreaWhat to add
AccessSign-in or a key, per-user limits and a production server with encrypted connections
PrivacyA decision on what you log. Prompts may contain personal information, so keep as little as you can, and in Québec check your obligations under Law 25
SafetyA clear statement of what the model is for. Tests with hostile and odd input. A person to contact
UpdatesThe version in every answer, a way to roll back to the previous version, tests that must pass first
MonitoringLatency, errors, and a regular human sample of the outputs. A score from training time goes stale as real use drifts
HonestyTell users that the text comes from a model and that it can be wrong

The model is the smallest part of that table. That is the usual finding at the end of this kind of project: training is the visible step, and the app's trustworthiness rests on all the plain work around it. This is where the series ends, and where the larger roadmap can help you choose what to climb next.

Download the companion code for this series (zip), with every script and the recorded results.

Sources

  1. safetensors README — Hugging Face (GitHub), November 2, 2026
  2. http.server: HTTP servers — Python
  3. nanoGPT (README) — Andrej Karpathy (GitHub), November 12, 2025

From the Guide to Artificial Intelligence, by Robert Scales, published by Holistic Hybrid Inc. Share and adapt it under CC BY-NC-SA 4.0: credit the guide, link to https://guidetoartificialintelligence.com/en/learn/checkpoint-to-working-app/, no commercial use without permission, and keep the same licence.

Get the free Guide to Artificial Intelligence

Sign up for the newsletter and receive the free guide: what changed in AI, one lesson, one prompt to try. Unsubscribe in one click.

How we handle your address

Back to top

Display

Your choices are saved in this browser only. No data is sent.

Text size
100%
Colours
Reading
More settings
Accessibility statement

Get the app

Add the guide to your home screen. It opens like an app, and the pages you have read stay available offline.

Read without a connection: download the articles

iPhone and iPad (Safari, Chrome, Edge)

  1. Tap the Share button (the square with an arrow). In Chrome and Edge, it is in the address bar or the menu.
  2. Choose “Add to Home Screen”.
  3. Tap “Add”.

Android (Chrome, Edge, Samsung Internet, Firefox)

  1. Open the browser menu (⋮ or ☰).
  2. Choose “Install app” or “Add to Home screen”.
  3. Confirm with “Install”.

Computer (Chrome, Edge)

  1. Click the install icon at the right end of the address bar, or open the browser menu (⋮ or …).
  2. In Chrome, choose “Cast, save and share”, then “Install page as app”. In Edge, choose “Apps”, then “Install this site as an app”.
  3. Click “Install”.

Mac (Safari)

  1. Open the File menu.
  2. Choose “Add to Dock”.
  3. Click “Add”.

Firefox on a computer can’t install a site as an app. Use Chrome, Edge or Safari instead.