Learning by building · Mini AI Stack · Draft for review

I did not build a tiny model experiment. I built a miniature AI factory.

Qwen2.5 0.5BSFT + 5 RL pathsvLLM + SGLangOpenAI compatible providerOne L4, evidence first

The original plan sounded modest: train a small model, try modern reinforcement learning, and expose it through an inference API. In practice, that sentence hid an entire AI product stack: data contracts, reward design, policy updates, reference policies, serving engines, routing, authentication, rate limits, usage accounting, artifact custody, and the uncomfortable discipline of asking whether the system learned anything at all.

I expected the story to be about which RL method won. The real story was learning why modern post-training is inseparable from the inference and operational machinery around it. The flat result did not erase that journey. It made the system more valuable because the stack had enough receipts to tell the truth.
5post-training paths built: SFT, REINFORCE, PPO, RLOO, GRPO
2separate serving engines exercised behind one provider boundary
21seed and method campaign cells with saved adapters and held-out evaluation

The project stopped being a notebook when each choice created a new obligation

A training script can make a loss decline. A product-grade learning system must survive a more adversarial question: what happens when a reward improves, an engine returns a different completion shape, a caller exceeds a quota, a model route changes, an experiment is interrupted, or a result looks better than it deserves to?

RENDERED LIVE · THE CONTRACT CHAIN
flowchart LR D["Frozen template families\n512 train · 128 held-out"] --> S["SFT cold start\nLoRA adapter"] S --> R["RL methods\npolicy + reward + reference"] R --> E["Exact-match evaluator\nJSON evidence"] E --> A["Checksummed artifacts\nlaptop + OCI"] R --> M["Model registry"] M --> V["vLLM / SGLang"] M --> L["Remote OpenAI-compatible backend"] V --> P["FastAPI provider\nauth · route · quota · usage"] L --> P P --> C["OpenAI-compatible clients"]

The diagram is deliberately small, but the boundaries are real. Frozen split families made it harder to accidentally describe template familiarity as reasoning. A model registry separated public model names from a backend. The provider owned the customer-facing contract rather than allowing every engine to leak its own assumptions. Artifact copies on the laptop and existing OCI host meant a deleted GPU VM did not take the evidence with it.

I built five ways to push a policy, because the differences matter when the reward is expensive

“RL” is often used as if it describes one thing. It does not. Each family makes a different bargain between simplicity, sample efficiency, stability, memory, and the kind of feedback the system can actually obtain. I implemented the small versions not to claim a frontier reproduction, but to make those bargains concrete.

Cold start

SFT establishes the language of the task

Supervised fine-tuning gives the policy an answer-shaped starting point. It is the cheapest path when verified demonstrations exist. It is also the control: without it, a reward-only method must spend scarce samples discovering basic format and behavior.

Direct policy gradient

REINFORCE is honest about variance

Sample a completion, score it, and make rewarded actions more likely. The idea is clean. The tradeoff is noisy credit assignment: rare sparse rewards and long completions make variance expensive. In the stack it uses the same deterministic exact-match reward as every other path.

Clipped policy update

PPO trades theoretical purity for safer movement

PPO constrains how far the new policy can move from the policy that generated the data. That clipping is valuable when a good policy can be damaged by one oversized update. The cost is more moving parts: rollout bookkeeping, advantage estimates, and a reference behavior to compare against.

Leave-one-out baseline

RLOO attacks variance without a learned critic

RLOO uses sibling samples as a baseline. Instead of fitting a separate value model, each outcome is compared with the other outcomes from the same prompt. It is attractive when a critic is undesirable or unstable, but it demands grouped samples and careful batch construction.

Group relative updates

GRPO makes the group the critic

GRPO normalizes rewards inside a response group. For verifiable tasks, that can replace a large learned critic with relative evidence among candidates. It remains operationally demanding: generation count, global batch divisibility, reference behavior, KL control, and rollout cost are all part of the algorithm in practice.

Estimator boundary

KL is a choice, not a decoration

The stack measured k1 and k3 forms against the frozen base reference. Their estimates were close on 32 development prompts, but they are not identical by definition. More importantly, a label on a configuration is not a causal ablation unless the loss formula actually changes.

INTERACTIVE · PICK THE FAILURE MODE

Click a situation. This is the practical decision tree I wanted before I started writing trainer code.

Select a situation to inspect the tradeoff.
Under the hood: what the implementations actually share
All paths use Qwen/Qwen2.5-0.5B-Instruct, PEFT LoRA adapters, frozen arithmetic template families, and conservative exact-match normalization in src/mini_stack/rewards/exact_match.py. The entrypoints are scripts/train_sft.py, train_reinforce.py, train_ppo.py, train_rloo.py, and train_grpo.py. The campaign contract gives each run an explicit seed, SFT adapter dependency, isolated output directory, and metrics path. That common substrate made method differences easier to inspect and made campaign artifacts collectable.

The model is only half the machine: an adapter becomes a service through a chain of compromises

Training an adapter is not the same as making it useful to another application. The adapter needed merging for engine compatibility, a registry entry, a serving runtime, a caller-facing schema, operational limits, and a way to explain which backend handled a request. This is where a model experiment begins to look like an inference product.

RENDERED LIVE · REQUEST LIFECYCLE
sequenceDiagram participant Client as OpenAI client participant API as FastAPI provider participant Route as deterministic router participant Engine as vLLM / SGLang / remote participant Ledger as usage ledger Client->>API: chat, responses, or completions request API->>API: bearer auth + token bucket API->>Route: resolve public model name Route->>Engine: normalized prompt Engine-->>API: completion + token counts API->>Ledger: requests and latency API-->>Client: OpenAI-compatible response

The provider surface is intentionally narrow but real: /health, /v1/models, /v1/completions, /v1/chat/completions, /v1/responses, /usage, and /metrics. The application owns bearer-key validation, deterministic routing, rate limits, token accounting, streaming completion behavior, and Prometheus-style metrics. That separation is not ceremony. It stops a client integration from being coupled to one engine’s server, one vendor’s model label, or one local deployment shape.

ConcernImplementation boundaryWhy it matters in a provider ecosystem
Model identityVersioned model registry and deterministic routerClients call a stable public name while the backend can change.
AccessHashed bearer key checkAuthentication must happen before costly inference work.
Cost and abusePer-identity token bucket and usage ledgerA provider needs a way to say who consumed what and when.
CompatibilityOpenAI-shaped chat, responses, completions, streamingExisting clients can integrate without bespoke engine code.
Backend choiceAsync protocol boundary for vLLM, SGLang, and remote OpenAI-compatible backendsServing behavior becomes a replaceable implementation choice.
A live proof, not a mocked checkbox. The provider acceptance artifact recorded a real OpenAI-compatible completion of 42, 10 prompt tokens, 1 completion token, and 1578.84 ms recorded latency. That does not prove universal reliability. It proves the provider boundary accepted a live remote backend without exposing a provider-specific contract to the caller.

The engine comparison taught a less glamorous lesson: a benchmark is a protocol, not a number

vLLM and SGLang were both exercised on the same L4 with the same merged SFT adapter. The smoke run returned 139.72 ms in vLLM and 108.58 ms in SGLang for one warmed prompt. It would have been easy to call that a winner. It would also have been wrong.

vLLM 0.8.3

139.72 ms

16 prompt tokens, 9 completion tokens. The returned completion included 42. This is evidence that the serving path worked for that request, not a global throughput claim.

SGLang 0.4.4.post1

108.58 ms

16 prompt tokens, 16 completion tokens. The completion continued beyond the desired response and reached the output cap. That makes the one-request comparison non-equivalent.

The practical lesson: engine selection depends on workload shape, scheduler behavior, cache policy, batching, output length, concurrency, and stopping correctness. A single latency number without those controls is an anecdote wearing a benchmark costume.

The most useful part of the stack was the boundary I could test without a GPU

The GPU was necessary for post-training and engine exercise. It was not necessary for every important engineering decision. Schemas, canonical serialization, configuration hashes, campaign state transitions, routing, authentication, rate limits, usage accounting, and the OpenAI-compatible contract all ran locally. That separation made the expensive campaign smaller and made the GPU hours evidence-producing rather than debugging time.

First, make the experiment resumable

A campaign state machine records planned, running, completed, and failed stages. The publication orchestrator creates isolated output and evaluation paths for every seed and method. This is what turns “I ran something” into a recoverable execution record.

Then, preserve the evidence before deleting the machine

Each campaign archive was copied to the laptop and existing OCI storage. SHA-256 values matched before the L4 VM was deleted. The final metric-complete archive hash was 1383a5eb7c3757bac825604d9712e4ed783f4ee24df173868d2c6dbda25adf5f.

Finally, separate completion from readiness

The Definition of Done matrix marks custom provider behavior, routing, streaming, usage, two-copy artifacts, and model serving as complete where evidence exists. It preserves partial status for the integrated end-to-end GCP provider campaign and for KL ablation strength. That distinction is how a project stays credible when it is still growing.

The result that refused to flatter me

The final metric-complete campaign ran seeds 13, 42, and 97 through SFT, REINFORCE, PPO, RLOO, GRPO, and two GRPO estimator labels. Every one of the 21 cells completed. Every saved adapter was then evaluated on the held-out division-template family. Every result was 4 correct answers out of 128.

MethodSeed 13Seed 42Seed 97Mean
SFT4/1284/1284/1280.03125
REINFORCE4/1284/1284/1280.03125
PPO4/1284/1284/1280.03125
RLOO4/1284/1284/1280.03125
GRPO4/1284/1284/1280.03125
GRPO k1 and k3 labels4/1284/1284/1280.03125

GRPO still produced a meaningful internal training signal. One completed cell recorded mean rollout reward 0.79167 and KL 0.37643, then scored 4/128 on the held-out task. That is the central learning: a reward-bearing optimization loop can be internally alive without producing the generalization outcome we care about.

Fine print I refuse to hide
This is a deliberately small, deterministic arithmetic environment. The held-out split is template-disjoint, so it is harsh by design. The experiment does not establish that no RL method can help a small model, that the model cannot reason, or that these methods are equivalent in general. It establishes a narrower fact: under this data, reward, model, budget, and evaluation contract, none of the implemented update paths moved the held-out score. Also, GRPO k1 and k3 were operationally matched labels, not a loss-formula ablation. A label is not evidence of a causal difference.

What I would carry into a real AI team

For ML engineers

Pick the learning loop that matches your feedback, not the algorithm with the best reputation

Verified answers, preferences, process rewards, and long agent trajectories produce different credit-assignment problems. The reward source should decide the post-training design. Build a clear SFT control and a hard held-out boundary before spending on rollout sophistication.

For platform teams

An inference engine is a component. A provider is a contract.

vLLM, SGLang, and a cloud backend are execution choices. Authentication, routing, rate limits, usage, model identity, streaming semantics, and evidence are the product boundary. Put that boundary in one testable place.

For CTOs and CAIOs

The cost of a run is not the bill. It is the bill plus what you cannot explain afterward.

An on-demand L4 can be affordable when the local work is ready, output paths are isolated, campaign evidence is explicit, artifacts are copied twice, and the machine is deleted immediately. A cheap run with no provenance is expensive learning.

For researchers

Negative transfer can be the finding when the measurement chain is intact

The value here is not a leaderboard claim. It is a compact system that keeps reward traces, held-out scoring, serving limits, configuration boundaries, and artifact custody attached to the same story.

My final lesson is simpler than the implementation: do not treat a model, a trainer, an inference engine, and a provider as four separate projects. They are one chain of accountability. Training changes a policy. Serving exposes it. The provider governs it. Evaluation tells you whether the whole system earned the right to make a claim.