Skip to content
Skip to lesson
Back to Topics
LearnProject Deep DivesDeep Dive - Tinker
⚡HardFine-Tuning & Training

Deep Dive - Tinker

Loss is lower, but the next sample still returns the old answer. Tinker splits that failure between your loop and the hosted LoRA adapter.

45 min read
Learning path
Step 198 of 199 in the full curriculum

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

At 09:12, your clamp run reports lower loss, but the next sample still returns return x. The local grader sees reward 0. The remote worker accepted forward_backward and optim_step. Before changing optimizer settings, ask three narrower questions: did the loop send the intended tokens, did the worker update the intended adapter, and did the sampler read that update?

Your Python process renders prompts, runs the verifier, and decides what evidence to submit. Hosted workers execute model math, hold adapter and optimizer state, and produce samples. You get handles and results back. You don't get the worker's placement decisions or its hidden gradients.

Tinker is Thinking Machines Lab's managed hosted (LoRA) service for this job. A local Python process builds prompts, environments, rewards, and loss inputs. Remote workers run forward and backward passes, apply optimizer updates, and generate samples. Reinforcement learning (RL) uses the grader's reward to guide these updates; supervised fine-tuning (SFT) instead learns from supplied answers. The public software development kit (SDK) and Tinker Cookbook are Apache-2.0 open-source code. The hosted service, model catalog, data handling, and API access are a separate product with their own terms.[1][2][3]

Follow one four-completion clamp group across forward_backward, optim_step, sample, and save_state. The function should return x inside the inclusive interval [lo, hi] and the nearest bound outside it, with lo <= hi. The source walkthrough uses SDK 0.27.0, commit 0ce5bf2484891570b59f8d330a2daf793646917e, and Cookbook commit 600b802d09be13e03756bfdc31e8c864fed90e7b, rechecked September 23, 2026. The local examples need no API key and submit no hosted jobs. A pinned client fixes local code, not the remote service implementation.[2][3]

Local TrainingClient sends datums to a hosted training model. That model publishes a snapshot used by hosted inference. Local SamplingClient sends prompts to inference and receives tokens; weights stay hosted.
The client objects are local handles. Adapter updates, sampler weights, and model execution remain hosted. Publishing a new snapshot is a separate transition from updating the training model.

Start with the boundary

Before reading an API call, predict who can answer each failure: a malformed Datum, an unavailable model, or a missing checkpoint. Tinker has three names that look like one product but describe different surfaces.

SurfaceRoleWho operates itWhat you can change
Tinker serviceHosted worker pool, storage, scheduler, and model runtimeThinking Machines LabYour requests, data, loss choices, and loop timing
tinker SDKPython client, types, futures, command-line interface (CLI), and API conversion codeOpen-source contributors under Apache-2.0Client code, tests, adapters, and local orchestration
tinker-cookbookOpen recipes for , RL, , distillation, evaluation, and exportOpen-source contributors under Apache-2.0Dataset builders, environments, reward functions, and recipes

The hosted service isn't the same thing as the GitHub repository. Cloning the SDK gives you client code, not the worker pool, model weights, scheduler, storage, or failure-recovery system. Using the service also doesn't force a fixed high-level RL trainer. The low-level calls are intentionally small.[4][2]

Treat the model catalog as a capability response, not as a constant in your Python package. Current docs describe LoRA fine-tuning for dense and (MoE) models from about 1B to 1T+ parameters, including vision models, and publish a live models page.[4]

The model page separates current training IDs, inference offerings, and retired models. A Hugging Face checkpoint isn't automatically a valid Tinker training ID. Check get_server_capabilities and the current model documentation before selecting a base model; separately verify its loss, modality, export, and account constraints. A list of model names doesn't encode every capability.[5]

A model may fit the service's architecture adapter and still be absent from your account, restricted by third-party terms, or unavailable for a particular loss or input modality. Treat the capability response and live model page as run-start evidence, not as static metadata in a blog post.

Local control plane, remote data plane

Start with one local question: what should happen to the next completion? The is the code that answers it. Your process loads data, renders a , calls an environment, grades clamp, computes group-relative advantages, selects a loss, and decides when to checkpoint. It can also start many requests concurrently with asyncio.

The data plane answers a different question: how does the model produce that completion? Hosted workers receive serialized model inputs and loss tensors, run distributed forward and backward computation, apply the , and return compact outputs. Sampling workers generate token IDs and optional log probabilities. Storage workers make named checkpoints addressable through tinker:// paths.

Diagram showing Hosted sample tokens + logprobs, Local verifier reward + advantage, Local datum shifted targets, Hosted update adapter step, and Explicit publish sampler snapshot.
Hosted sample tokens + logprobs, Local verifier reward + advantage, Local datum shifted targets, Hosted update adapter step, and Explicit publish sampler snapshot.

This split lets you change a reward function without changing distributed model code. It also tells you where to look when the run fails. Local code can produce a malformed target . Remote workers can reject a model or run out of capacity. A sampler can use an older checkpoint than the trainer. A retry can duplicate an operation unless request and checkpoint names are idempotent.

When a result looks wrong, ask, "Where does this state live?" Your loop constructs prompt rows, reward labels, and experiment configuration; requests then send selected data to the service. LoRA parameters, optimizer moments, and worker placement stay hosted. A future is a handle to completion, not a download of model-parameter . Custom losses can differentiate returned log probabilities locally, which is a different gradient boundary.

The four primitives

When clamp produces a bad sample, don't start with endpoint names. Predict which state transition should have changed: gradient accumulation, adapter weights, sampler weights, or a recovery artifact. These four primitives make that transition observable.

PrimitiveClientInputRemote effectReturned evidence
forward_backwardTrainingClientDatum list, loss name, optional configComputes loss and accumulates gradients for current adapter statePer-datum outputs, metrics, and a future
optim_stepTrainingClientAdam hyperparametersApplies accumulated gradients and advances adapter plus optimizer stateOptimizer metrics and a future
sampleSamplingClientTokenized prompt, count, sampling parametersGenerates one or more completions from a model snapshot IDs, stop reason, optional log probabilities
save_stateTrainingClientName, optional time to live (TTL), overwrite flagPersists train-resume checkpoint, optionally with optimizer state on loadtinker:// checkpoint path

The SDK also exposes save_weights_for_sampler and save_weights_and_get_sampling_client. Those operations publish a sampler-compatible adapter snapshot. save_state writes a training checkpoint; load_state_with_optimizer restores its weights and optimizer state. A sampler checkpoint evaluates or serves a chosen weight version. Keep those consumers separate when you name and inspect artifacts.

The synchronous entry points return future-like objects. Their _async variants don't all have the same return contract: awaiting forward_backward_async gives an APIFuture that still needs result_async(), while awaiting sample_async gives the completed SampleResponse. Neither changes the server's ordered training schedule into an unbounded asynchronous parameter server.[2]

forward_backward receives a Datum

Before sending one datum, predict what the remote loss can validate: model input plus tensors named by that loss. A Datum has model_input and loss_fn_inputs. model_input is a token or multimodal sequence. For , loss_fn_inputs commonly contains target_tokens and weights. For RL, it can include sampled logprobs, advantages, target_tokens, and an action mask before cookbook code removes or consumes the mask.

Large lists may become multiple requests. The SDK's client-config defaults are 1,024 datums and 5,000,000 estimated pre-compression protobuf bytes per chunk; server-provided configuration can change them. The chunker doesn't split an individual oversized datum, so the byte value isn't a universal hard cap. Ordered sequence IDs preserve logical request order even when chunks are submitted concurrently.

The loss name is a protocol value. The pinned SDK declares cross_entropy, importance_sampling, ppo, cispo, and dro as supported identifiers, while a model or service deployment may enable fewer options. Validate the current server capability and documentation when a run starts.[2][4]

optim_step advances adapter state

If loss improves but a resume behaves differently, inspect the update boundary first. optim_step consumes gradients accumulated by preceding forward/backward work and updates trainable parameters with AdamW-equivalent behavior. SDK documentation calls out a default of zero, which differs from some defaults.[2]

The optimizer state stays remote, so you don't download a Python optimizer object after every step. That keeps the loop small but makes checkpoint choice consequential. Loading weights alone resets optimizer history; loading with optimizer restores saved moments when the checkpoint contains them.

The operation ordering is part of correctness. An optimizer request submitted before its matching forward/backward result can still be valid because the service sequences requests for that training client. A separate client, a retry, or an accidental concurrent loop can violate the intended order. Give one component ownership of a training client, and log request IDs with step numbers in the local loop.

sample returns evidence for learning

Before trusting a reward, predict what evidence the sampler must return. SamplingClient.sample accepts a ModelInput, number of completions, and SamplingParams. Each returned sequence includes generated token IDs and a stop reason. If requested, it includes one per generated token. compute_logprobs can score prompt tokens for a separate comparison.

The decoded string answers the verifier's question; token identifiers (IDs) and log probabilities answer the learner's. If you decode and re-tokenize before constructing an RL Datum, a different tokenizer version or normalization rule can shift token boundaries. The unit test may still pass while the policy-gradient target points at the wrong positions.

num_samples is a group primitive. Four independent clamp completions give a group over which the Cookbook centers rewards. Four separate calls can work, but one grouped call makes the comparison and batching explicit. Concurrent calls across different prompts add another axis of parallelism.[4][3]

save_state creates a recovery point

After an update, ask which consumer needs the bytes. save_state("step-0042") stores a named training checkpoint. load_state restores model weights but not optimizer state. load_state_with_optimizer restores both when available. A TTL can make a checkpoint expire, and overwrite controls whether the name can be replaced.[2]

For evaluation, save sampler-compatible weights instead. Export is another path: for models with downloadable compatible weights, Cookbook utilities can build a adapter or merge it into a Hugging Face model. Not every hosted model necessarily permits the same export. Runtime compatibility, base-model availability, and applicable terms still govern deployment.

LoRA is the adaptation boundary

Those four calls describe movement. The next question is what the optimizer can change.

Tinker is a LoRA training service, not a full-parameter training service. LoRA keeps the frozen base WW and learns a low-rank update. Thinking Machines Lab writes the adapted matrix as

W′=W+γBA,W' = W + \gamma BA,

where AA and BB are small trainable matrices and γ\gamma is a constant scale. Many implementations, including the original LoRA paper, take γ=α/r\gamma = \alpha / r with rank rr and a scale hyperparameter α\alpha. The service stores and updates the adapter path while sharing the base model across runs. That's how a multi-tenant worker pool can serve many experiments without copying a full model and optimizer state for each run.[6][1][7]

LoRA doesn't mean "small model." Tinker has documented support for large dense and MoE models, including trillion-parameter examples in its announcements. The memory savings come from trainable state and optimizer path, not from making every base-model weight cheap to move. A huge model still needs a compatible hosted topology, checkpoint format, and model-specific adapter implementation.[8][4]

Rank sets adapter capacity. A larger rank can express a broader update but costs more adapter parameters and optimizer state; measure task quality to compare ranks. Training only attention, only a multilayer perceptron (MLP), or the unembedding layer changes which behavior can move. Thinking Machines Lab's LoRA study found attention-only LoRA underperformed MLP LoRA even after matching parameter count, and that RL matched full fine-tuning at very low ranks in their math setup. The create_lora_training_client options make those choices explicit. Compare runs with target modules held constant before attributing a change to rank.[7]

LoRA also changes what "full control" means. You control the algorithm, data, reward, sampling schedule, and loss inputs. The provider controls arbitrary base-weight tensors, kernel fusion, placement, and collective algorithms. This division makes Tinker useful for post-training research while leaving infrastructure-level training decisions outside the client API.

SFT first: a token-level contract

A low-rank update still needs a token-level contract. Start with supervised clamp before adding rewards. Before you submit the datum, predict which positions should be allowed to change the adapter.

A rendered conversation becomes an input sequence and a target sequence. A weight tensor marks which target positions contribute to . Prompt positions usually receive zero weight; assistant positions receive one or a chosen fractional weight.

For one datum, the core alignment is:

Positionmodel_inputtarget_tokensweightsTraining meaning
0prompt ID 10prompt ID 110Don't train on predicting prompt text
1prompt ID 11first action ID 201Predict the first answer token from the prompt
2action ID 20second action ID 211Predict the next answer token

These are toy IDs, not a real tokenizer vocabulary. The full sequence is [10, 11, 20, 21]: inputs omit its last token, targets omit its first token, and weights follow the target role. Masking by the input role would incorrectly drop the first answer token at position 1.

The arrays must share the expected shifted length. The Cookbook's conversation_to_datum helper handles renderer-specific role markers, target shifting, and response masks. Read that helper when a loss curve looks impossible. A one-token offset can produce a decreasing loss while training the model on the wrong targets.

The pinned conversation_to_datum also defaults to reduction="mean": after shifting, it normalizes positive-total weights to sum to one per example. Our toy [0, 1, 1] becomes [0, 0.5, 0.5], preserving the first-answer position. With reduction="none", the raw weights stay intact and longer answers with more trained tokens contribute more terms to the summed loss. Record this choice when comparing runs: changing reduction changes example weighting, even with identical tokens and masks.[3]

The smallest SFT loop has one forward_backward and one optim_step. The Cookbook submits both before waiting so they can share a worker clock cycle. It repeats the pair while recording loss and evaluation metrics. A decrease in loss means the adapter predicts the chosen weighted tokens more easily. Run the unit test separately to evaluate functional behavior.

RL: preserve tokens, logprobs, and credit

Loss going down doesn't mean the grader is happier. RL adds a second model interaction: the current policy samples a completion, an environment or verifier assigns reward, and the loop converts that evidence into a training datum. The reward function can be local and deterministic, a remote grader, a unit-test , or another model. Tinker doesn't choose it for you.

Keep the clamp group of four completions:

Completion iiBody (decoded)Reward RiR_i
0return min(max(x, lo), hi)1
1return max(min(x, lo), hi)0
2return min(max(x, lo), hi)1
3return x0

The group mean is Rˉ=0.5\bar{R} = 0.5. Centered advantages are A=(0.5,−0.5,0.5,−0.5)A = (0.5, -0.5, 0.5, -0.5). The reward objective encourages completions 0 and 2 and discourages 1 and 3; shared parameters mean no individual probability change is guaranteed. If all rewards were 1, every centered advantage would be zero. A Kullback-Leibler (KL) divergence penalty, which discourages departure from a reference policy, or another auxiliary objective could still contribute gradients.

The Cookbook's basic path turns that prediction into a sequence:

  1. Select a group of prompts.
  2. Sample several completions per prompt from current sampler weights.
  3. Grade every completion.
  4. Center rewards within each group: Ai=Ri−mean⁡(Rgroup)A_i = R_i - \operatorname{mean}(R_{group}).
  5. Optionally filter constant-reward groups. The helper retains one group with a warning if all are uniform; that fallback avoids an empty batch but doesn't create reward signal. The pinned training config disables this filtering by default.
  6. Build Datum objects with sampled log probabilities and token-aligned advantages.
  7. Submit forward_backward(..., loss_fn="importance_sampling").
  8. Submit optim_step with the chosen Adam parameters.
  9. Save and evaluate a sampler checkpoint.

The group baseline is one -reduction choice. Code tasks might use unit-test pass rate, while tool-use environments can combine success, invalid-call penalties, and latency budgets. Preference models may return dense scores. Your loop maps each outcome into advantages and masks before the model receives a token-level signal. That mapping turns an evaluation decision into a training decision.

The RL datum shape

Before looking at fields, predict what should happen when a trajectory branches. The Cookbook's trajectory converter emits one or more datums per trajectory. One trajectory can split when a later observation isn't a prefix of the sequence accumulated so far. That happens naturally with tool calls, branches, or context compaction.

The resulting loss_fn_inputs include:

FieldShape ideaSourceMeaning
target_tokensOne target per input positionShifted sampled sequenceToken to score
logprobsOne sampled logprob per targetSamplerBehavior-policy evidence
advantagesOne scalar copied across action tokensGroup rewardCredit assigned to action
maskZero for observation, one for actionTrajectory converterWhich positions are trainable

Observation positions receive zero logprob, zero advantage, and zero mask. Action positions receive the sampled logprob and trajectory advantage. A parse-error policy can set an action mask to zero while retaining the rest of the trajectory. The invalid turn stays visible in metrics without teaching the model to reproduce malformed output.

The named loss consumes only the fields it supports. importance_sampling compares current log probabilities with sampled behavior-policy log probabilities. (ppo) adds clipping behavior. The cispo and dro losses use other importance and truncation choices. The Cookbook's train_step removes mask before submission: zero advantages suppress the reward term at observation positions, while the mask remains useful for local diagnostics and auxiliary terms. Sending a mask field alone isn't a substitute for zeroing the corresponding advantages.[3]

A custom loss can run on log probabilities through forward_backward_custom, but the SDK still translates your derivative into a backend-supported weighted cross-entropy pass. If the local custom loss is C(ℓ)C(\ell) for returned log probabilities ℓ\ell, it sends weights −∂C/∂ℓ-\partial C/\partial \ell. Backend cross-entropy is −∑twtℓt-\sum_t w_t\ell_t, so this negative sign reproduces the custom loss's derivative. These are gradients with respect to log probabilities, not downloaded adapter-parameter gradients.[2]

Logprob alignment is the hidden invariant

Suppose a sampler returns four response tokens and four log probabilities for one clamp completion. Predict where the first score belongs before building the datum. The training input usually contains prompt context plus the response prefix, while the target array contains response tokens shifted one position to the left. Prompt padding is deliberate: it keeps positions aligned while assigning zero learning signal to context.

If the arrays differ by one token, an operation can fail loudly. Worse, accidental padding can make lengths agree while applying the wrong credit to every token. Log a short prefix of tokens, log probabilities, advantages, and masks for one group before launching a long run.

Make the reward reproducible enough to debug. Save the prompt ID, sampler checkpoint path, sampled tokens, stop reason, reward components, environment version, and advantage statistics. A reward value without that evidence can't tell you whether the model improved or the grader changed.

Toy sequence 10,11,20,21 becomes inputs 10,11,20 and targets 11,20,21. Action mask 0,1,1 and behavior logprobs 0,-0.4,-0.8 align with the targets. The first action is predicted from the last prompt token.
Mask and logprob entries belong to targets, not input tokens. Position 1 trains the first action even though its input token still belongs to the prompt. Masked prompt targets remain available as context.

The pipeline clock

Correct datums can still miss GPU time if the client waits between calls. Tinker worker pools execute training clients in lock-step clock cycles. A cycle can include forward/backward work and an optimizer step for one or more LoRA models sharing the pool. Multi-tenancy keeps small batches from leaving the pool idle, but it also means a small request can observe latency shaped by the pool's schedule.[4]

Submit work early, then await its results. In the documentation's illustrative schedule, waiting for a forward/backward result misses the next admission boundary and makes the pair span three cycles. Treat that count as a scheduling example; measure end-to-end latency with an initialized client. Both snippets assume an initialized training client, batch, and optimizer parameters.[9]

naive_clock_cycle.py
fwd = await client.forward_backward_async(batch, "cross_entropy")
await fwd.result_async()
opt = await client.optim_step_async(adam_params)
await opt.result_async()

The pipelined loop submits both operations before waiting:

pipelined_clock_cycle.py
fwd = await client.forward_backward_async(batch, "cross_entropy")
opt = await client.optim_step_async(adam_params)
fwd_result = await fwd.result_async()
opt_result = await opt.result_async()

The service can place both in one cycle because the training client preserves request order. For several batches, enqueue the next forward/backward plus optimizer pair before consuming the current result. The Cookbook's train_step uses this pattern and returns training log probabilities for diagnostics.[4][3]

Pipeline depth isn't free. A deeper queue can hide network latency and keep busy, but it can also increase local data waiting for a result and complicate failure handling. In RL, over-enqueueing samples can also increase policy lag. Keep a bounded number of in-flight groups and log sampler checkpoint version against trainer step.

Diagram showing Cycle N Naive: forward/backward Pipelined: both operations, Cycle N+1 Naive: result arrives Next admission missed, and Cycle N+2 Naive: optimizer step Pipelined pair already done.
Cycle N Naive: forward/backward Pipelined: both operations, Cycle N+1 Naive: result arrives Next admission missed, and Cycle N+2 Naive: optimizer step Pipelined pair already done.

The clock isn't the same as GPU kernel overlap inside a model. A worker may overlap collective communication and matrix operations internally while your client still misses a cycle by waiting before submitting the next request. Read both layers when tuning throughput.

Official docs warn against adding client timeout-and-resubmit loops around Tinker calls. If an application deadline expires, record the outstanding request, stop scheduling replacement work, and reconcile its status through the supported SDK behavior. Bound concurrency and total run duration without blindly duplicating expensive requests.

Checkpoints and export paths

Even a fast loop needs a named artifact you can resume or serve. Before saving, decide which question the artifact must answer: can training continue, can a sampler evaluate it, or can another runtime load it? Tinker has three artifact states.

ArtifactPurposeRestores optimizer?Typical consumer
Training stateResume the same loopYes with load_state_with_optimizerTraining client
Sampler weightsEvaluate a chosen policy snapshotNot a training resumeSampling client, OpenAI-compatible adapter
Merged Hugging Face modelPortable base plus LoRA updateNo training history, SGLang, Transformers, offline inference

The checkpoint name is part of your experiment schema. Include a run ID, step, dataset revision, and reward version in your own metadata even if the service accepts a shorter name. A checkpoint path says where bytes are; it doesn't explain why that version should be trusted.

save_state can preserve a training resume point. save_weights_for_sampler makes a sampler path. save_weights_and_get_sampling_client combines save and client creation for a convenient evaluation step. The Cookbook's weights utilities download the adapter, build an HF model, build a PEFT adapter, or publish a checkpoint to a hub. Each export choice creates a new license and security review surface.[4][3]

Merging W′=W+γBAW' = W + \gamma BA turns an adapter into a standalone model directory. It doesn't make the base-model license disappear or preserve remote optimizer state. Keep the original tinker:// path and training metadata next to the exported model so a future evaluator can reproduce the exact adapter, base revision, tokenizer, and renderer.

Evaluation should use a fresh sampler client for the saved path or a path explicitly tied to the training client. A long-lived sampler doesn't automatically see the latest adapter. A serving client built from an older path can produce plausible text from a stale policy and make a reward curve look flat, so record the policy version with every trajectory.

API, service, and data boundaries

Both pinned source snapshots use Apache-2.0. That license lets you inspect, modify, and redistribute client and recipe code under its terms. It doesn't grant rights to Thinking Machines Lab's hosted service, third-party model weights, or every dataset copied into a recipe.[2][3]

The service terms rechecked on September 23, 2026 (last updated January 10, 2026) distinguish customer datasets, hosted models, fine-tuned models, third-party terms, company models, and available weights. The service may add or remove model access. Check the applicable terms and weight-download documentation before downloading a hosted model.[10]

A hosted run has two budgets. Samples, training tokens, retries, and checkpoint storage consume service capacity or storage; missing token receipts, version IDs, and reward components consume evidence. Put both in the run manifest before scaling. A lower loss is not a cost report, and a reward curve without its policy version is not a reliable evaluation.

Your dataset isn't automatically safe because the SDK serializes it. You are responsible for having the rights to send prompts, images, tool traces, and labels to a hosted service. The terms grant the provider a limited license to access and process customer datasets to provide the service and comply with law. They also commit that Thinking Machines Lab won't use customer content to train or improve their own models. Personal-data deletion and export obligations still depend on the applicable agreement and processing terms.[10]

Keep secrets out of the dataset and logs. API keys belong in or a secret manager. Reward traces can contain private source code, credentials emitted by a tool, or user data. Redact before logging and set checkpoint retention explicitly. A sampler checkpoint can be as sensitive as the training data because the adapter may memorize rare examples.

Data residency, subprocess behavior, storage TTL, deletion, and enterprise commitments are product questions. Don't infer them from an open-source test or from a local tinker:// path. Ask the current service and agreement for the requirement your deployment needs.

The published terms also restrict competitive benchmarking and competitive analysis of the service. Before planning a public hosted-service comparison, check the agreement that applies to your account and obtain any needed authorization. This matters separately from testing your own reward function or reading the Apache-2.0 client source; an enterprise agreement can supersede conflicting published terms.[10]

Team, contributors, and governance

Tinker is built by Thinking Machines Lab. The company maintains the hosted service, SDK repository, Cookbook, documentation, model integrations, and customer-facing terms. Public repositories show code and commit history, but a contributor count or top-contributor list is only a snapshot. Use the pinned commit and live repository history when attribution matters.

Use repository history for attribution and contribution guidance for proposed changes. Commit counts don't establish authority over the hosted runtime, and an accepted SDK contribution doesn't give its author control of service policy. The LoRA study is credited to John Schulman with other Thinking Machines collaborators. That research attribution doesn't identify every SDK maintainer or hosted-runtime engineer.[7]

The Cookbook's contribution guide asks contributors to run Ruff, Pyright, and tests, and describes an open-science workflow. Its code structure separates configuration builders from heavyweight datasets, environments, strategies, and training loops. Those conventions provide governance through code review and reproducibility, not through a formal foundation with independent technical steering.[3]

QuestionEvidence to inspectSafe statement
Who owns hosted runtime?Thinking Machines Lab service terms and announcementsThinking Machines Lab operates the service
Who can change SDK code?GitHub repository permissions and review historyRepository maintainers and accepted contributors
What is licensed?LICENSE files at pinned commitsSDK and Cookbook source are Apache-2.0
Which models are available?Live capabilities and model docsCatalog is dynamic and model-specific
Who owns your data?Current service agreement and data processing addendum (DPA)Follow the contract, not an inference from GitHub

For an incident, route by evidence. Service errors, capacity, and catalog access belong with the service owner. Malformed datums, reward side effects, and renderer drift belong with your loop owner. Model rights, retention, and export obligations belong with the contract owner. Don't call the SDK a community-governed replacement for the service: its inspectable client and recipe layer still depend on a company-operated API.

Strengths

The cluster is on the other side of the SDK

You can test a reward function, a custom loss, or an environment from a CPU-oriented development machine. Thinking Machines Lab handles scheduling, resource allocation, worker placement, and failure recovery for hosted runs. The first experiment is a Python loop, not a cluster bring-up.[1]

Low-level enough for research

forward_backward and sample expose token and logprob evidence that many high-level trainers hide. You can implement GRPO-style relative rewards, -style clipping, DPO data preparation, prompt distillation, or a custom logprob loss. The Cookbook provides a starting point without forcing every project into one environment abstraction.[1][4]

One path from SFT to RL

The same TrainingClient can run cross-entropy SFT and RL losses. The same SamplingClient can generate evaluation completions and behavior-policy traces. That keeps one client boundary in a staged post-training project, while still requiring separate checks for training and evaluation.

Large-model access without local replicas

LoRA lets a service share a frozen base model across training runs. The documented model range includes large dense and MoE models. The general-availability announcement added Kimi K2 Thinking as a trillion-parameter example; the current catalog has moved on, so you still have to read the live page rather than that post.[8][4] A researcher can test an idea on a small model, then check whether a large model is in the current catalog without rewriting the loop.

Export is a separate path

Sampler checkpoints can be evaluated through the SDK or an OpenAI-compatible interface. Cookbook utilities can download and merge adapters into Hugging Face layouts. After you check tokenizer, license, and runtime compatibility, you can move a successful experiment onto an existing serving stack.[8][3]

Weaknesses and limits

Not full-parameter training

LoRA constrains the update space. A behavior that requires broad base-weight changes may not transfer, and a LoRA result doesn't answer full fine-tuning questions. If your research question is optimizer scaling, full-rank catastrophic forgetting, or expert-weight surgery, Tinker isn't the right primary system.

Hosted capacity and catalog are external state

Your code can be deterministic while service capacity, model availability, and scheduling latency change. A pinned SDK doesn't pin the remote runtime. Record capability responses and service metadata at run start, then treat them as part of the experiment evidence.

Network latency shapes algorithm design

Every sample, grader, checkpoint, and training request crosses a boundary. A chatty environment can spend more time waiting on transport than grading. Batch prompts, use concurrency, and keep a bounded pipeline. API acceptance doesn't make or high-cardinality debug payloads safe to send.

Staleness can accumulate silently

An RL sampler can continue using an older sampler checkpoint while training updates a newer adapter. A deep local queue can hide that mismatch. Include a policy version in each trajectory, then reject or measure samples beyond your staleness budget.

Service terms still apply

An Apache-2.0 SDK doesn't turn a hosted run into a self-hosted run. Customer data, model weights, available checkpoints, and downstream use are governed by the current service agreement and third-party model licenses.[10]

Failure recovery has a boundary

The service can recover worker failures, but your environment may not be idempotent. If a tool call charges money, mutates a repository, or sends a message, a retry can repeat the side effect. Give every external action an key and store a local trajectory receipt.

A runnable contract check

Use one cheap local gate before spending remote GPU time. This standard-library example grades four known local functions on five fixtures, centers their rewards, and checks a toy trajectory. It doesn't execute untrusted generated code or call Tinker. The fixtures cover below-range, inside-range, above-range, a zero-width interval, and negative bounds; arbitrary programs need broader functional and safety testing.

check_tinker_rl_contract.py
from dataclasses import dataclass, replace
from math import isfinite

def center(rewards: tuple[float, ...]) -> tuple[float, ...]:
    if not rewards or not all(isfinite(r) for r in rewards):
        raise ValueError("rewards must be nonempty and finite")
    mean = sum(rewards) / len(rewards)
    return tuple(reward - mean for reward in rewards)

def has_signal(rewards: tuple[float, ...]) -> bool:
    return len(set(rewards)) > 1

@dataclass(frozen=True)
class RLDatum:
    input_tokens: tuple[int, ...]
    target_tokens: tuple[int, ...]
    sampled_logprobs: tuple[float, ...]
    advantages: tuple[float, ...]
    mask: tuple[float, ...]

def validate(datum: RLDatum) -> None:
    lengths = {
        len(datum.input_tokens),
        len(datum.target_tokens),
        len(datum.sampled_logprobs),
        len(datum.advantages),
        len(datum.mask),
    }
    if len(lengths) != 1 or 0 in lengths:
        raise ValueError(f"unaligned lengths: {sorted(lengths)}")
    if any(type(t) is not int or t < 0 for t in datum.input_tokens + datum.target_tokens):
        raise ValueError("tokens must be nonnegative integers")
    if not all(isfinite(v) for v in datum.sampled_logprobs + datum.advantages):
        raise ValueError("scores must be finite")
    if any(value not in (0.0, 1.0) for value in datum.mask):
        raise ValueError("mask must contain only 0.0 or 1.0")
    if not any(datum.mask):
        raise ValueError("datum has no trainable action")
    if any(m and lp > 0 for m, lp in zip(datum.mask, datum.sampled_logprobs, strict=True)):
        raise ValueError("action logprob cannot be positive")
    if any(mask == 0.0 and advantage != 0.0 for mask, advantage in zip(datum.mask, datum.advantages, strict=True)):
        raise ValueError("masked positions must have zero advantage")

fixtures = ((-1, 0, 2, 0), (1, 0, 2, 1), (3, 0, 2, 2),
            (3, 2, 2, 2), (-4, -3, -1, -3))
candidates = (
    lambda x, lo, hi: min(max(x, lo), hi),
    lambda x, lo, hi: max(min(x, lo), hi),
    lambda x, lo, hi: min(max(x, lo), hi),
    lambda x, lo, hi: x,
)
clamp_rewards = tuple(float(all(f(x, lo, hi) == expected
    for x, lo, hi, expected in fixtures)) for f in candidates)
assert clamp_rewards == (1.0, 0.0, 1.0, 0.0)
assert center(clamp_rewards) == (0.5, -0.5, 0.5, -0.5)
assert has_signal(clamp_rewards)
assert not has_signal((1.0, 1.0, 1.0, 1.0))

full_tokens = (10, 11, 20, 21)
full_action_mask = (0.0, 0.0, 1.0, 1.0)
datum = RLDatum(
    input_tokens=full_tokens[:-1],
    target_tokens=full_tokens[1:],
    sampled_logprobs=(0.0, -0.4, -0.8),
    advantages=(0.0, 0.5, 0.5),
    mask=full_action_mask[1:],
)
validate(datum)

assert datum.target_tokens[1] == 20 and datum.mask[1] == 1
bad_datums = (
    replace(datum, input_tokens=(10, 11)),
    replace(datum, advantages=(0.0, float("nan"), 0.5)),
    replace(datum, mask=(0.0, 0.5, 1.0)),
    replace(datum, mask=(0.0, 0.0, 0.0)),
    replace(datum, sampled_logprobs=(0.0, 0.4, -0.8)),
    replace(datum, advantages=(0.5, 0.5, 0.5)),
    replace(datum, target_tokens=(11, -1, 21)),
)
for bad in bad_datums:
    try:
        validate(bad)
    except ValueError:
        pass
    else:
        raise AssertionError("invalid datum accepted")
for bad_rewards in ((), (float("inf"),)):
    try:
        center(bad_rewards)
    except ValueError:
        pass
    else:
        raise AssertionError("invalid rewards accepted")

print("fixture rewards", clamp_rewards)
print("advantages", center(clamp_rewards))
print("first answer trained; 9 invalid inputs rejected")
Contract check
fixture rewards (1.0, 0.0, 1.0, 0.0)
advantages (0.5, -0.5, 0.5, -0.5)
first answer trained; 9 invalid inputs rejected

Equal lengths still can't detect scores attached to the wrong tokens. Here, constructing both arrays from one full sequence and checking the first action protects the illustrated shift. Real multimodal renderers need their own fixtures. These checks don't prove reward quality, sampler freshness, or learning improvement.

Check the actual SDK without a hosted job

The downloadable SDK contract probe pins the exact Git revision behind the 0.27.0 version label. Installing dependencies needs network access; execution creates no ServiceClient, HTTP transport, or hosted model. From this repository's root, run:

offline_sdk_check.sh
uv run web/src/content/projects/deep-dive-tinker/assets/verify_sdk_contracts.py

The executed receipt records six passing checks and source hashes: wire dtypes, a protobuf roundtrip preserving the first-answer weight, count-based chunk ordering, oversized-datum behavior, empty chunk input, and the SDK's acceptance of mismatched lengths. The pinned SDK's seven native request-serialization tests also passed, including a mocked transport test. None of these checks measures remote ordering, clock-cycle latency, model quality, checkpoint recovery, or distributed training.

Production failure modes

The happy path is short. Production debugging isn't. Start with the evidence closest to the token boundary, then move outward to service state and terms.

SymptomLikely boundaryFirst checkDurable fix
Loss is NaNInput tensor or custom lossFinite target, weight, logprob, and advantage valuesReject non-finite rows before submission
Loss falls but reward doesn'tWrong target or reward alignmentDecode sampled tokens and inspect shifted targetsSave token-level and renderer version
Reward is flat at zeroDegenerate groups or broken graderReward histogram and group varianceSkip constant groups and test verifier offline
Reward rises then collapsesStale policy or excessive updatePolicy version, Kullback-Leibler (KL) divergence, advantage scale, Bound in-flight groups and tune update size
Requests are slow despite small dataMissed clock cyclesSubmission timestamps and cycle gapsSubmit forward/backward plus optimizer before await
Samples time out and retry stormsHomemade client timeoutsasyncio.wait_for around SDK callsWait for in-flight samples; don't stack extra retries
Sample text doesn't changeStale sampler checkpointtinker:// path and save stepRebuild sampler client after save
Resume divergesOptimizer state omittedLoad method and checkpoint manifestUse load_state_with_optimizer for continuation
Exported model differsBase revision or tokenizer driftHash base files and tokenizer configPin base revision and preserve renderer metadata
Retry repeats external actionNon-idempotent environmentTool receipts and retry logsAdd idempotency keys and replay-safe adapters
API returns model unavailableCatalog or terms mismatchCapability response at run startFail fast with model and account evidence
Data appears in logsOver-broad telemetryRedacted request and artifact logsRemove secrets and set retention/TTL
Adapter loads but quality regressesUnsupported target modules or rankTraining-client config and evaluation matrixCompare rank and module scope explicitly

An actionable incident record contains the SDK commit, Cookbook commit, model identifier, capability response, base revision, tokenizer revision, renderer name, loss name and config, optimizer parameters, sampler checkpoint path, trajectory sample, reward components, and checkpoint path. Without those fields, a replay is guesswork. Capture them before cleanup or checkpoint expiry removes the trail.

Retry and idempotency

The SDK retries some transport failures through its client holder. A retry can be safe for a read, but a training request or external environment call needs a sequence contract. The pinned SDK assigns sequence IDs to training requests and preserves turn ordering across chunks. Your environment must add its own idempotency around side effects.[2]

For evaluation, prefer pure functions and immutable fixtures. For a tool write, persist its operation ID and use downstream idempotency or a status lookup. A local "started" record alone can't distinguish a crash before the remote effect from a crash after it. Reconcile uncertain outcomes rather than automatically replaying them.

Catalog and model terms

A model string in a tutorial isn't a production approval. The service can add or remove models. A third-party model can have a separate license, usage restriction, or attribution requirement. Query capabilities, retain the response, and link your run manifest to the model terms you accepted.[10]

Source-reading map

Read the repositories in an order that follows one request instead of scanning every file.

SDK snapshot

  1. src/tinker/lib/public_interfaces/service_client.py: client creation, model capability calls, and the boundary between base and saved models.
  2. src/tinker/lib/public_interfaces/training_client.py: request sequencing, request chunking, forward_backward, optim_step, save, and load behavior.
  3. src/tinker/lib/public_interfaces/sampling_client.py: sample, logprob options, multiprocessing, and sampler creation.
  4. src/tinker/types/datum.py: tensor conversion and serialization assumptions. Construction alone doesn't verify semantic token alignment.
  5. src/tinker/proto/request_conv.py: how a Datum becomes a wire request.
  6. src/tinker/lib/api_future_impl.py: how remote completion and errors reach local code.
  7. src/tinker/resources/training.py and src/tinker/resources/sampling.py: HTTP endpoint names and response conversion.
  8. Repository-root LICENSE and pyproject.toml: license, dependencies, and package identity.

The key path is TrainingClient._run_fwd_bwd to _take_turn to resources/training.py. That path explains why multiple chunks can be submitted concurrently while sequence IDs keep server order. The optimizer path uses the same turn counter. The sampler path is separate and can be created from a saved sampler checkpoint.

Cookbook snapshot

  1. tinker_cookbook/rl/types.py: Env, trajectories, transitions, stop reasons, and group notation.
  2. tinker_cookbook/rl/data_processing.py: advantage centering, trajectory splitting, masks, and datum assembly.
  3. tinker_cookbook/rl/train.py: clock-cycle pipeline, loss selection, training logprob diagnostics, and checkpoint hooks.
  4. tutorials/104_first_rl.py: the smallest readable GRPO-style loop.
  5. tutorials/103_async_patterns.py: concurrent sampling and group batching.
  6. tutorials/501_export_hf.py: adapter download, merge, and validation.
  7. CONTRIBUTING.md: builder conventions, async guidance, testing, and review expectations.
  8. LICENSE: Apache-2.0 grant for the cookbook source.

The SDK and Cookbook revisions are pinned at the start of this walkthrough. Their implementations can be tested locally, but no client-side test pins server availability, scheduling latency, or learning outcomes.

Tinker versus DeepEP

A hosted LoRA loop and an expert-parallel transport library can sit in the same post-training stack, and they still solve different problems. DeepEP's primary interface is expert-parallel dispatch/combine. Its V2 snapshot also has experimental communication primitives beyond expert parallelism. It isn't a hosted training API.[11][12]

DimensionTinkerDeepEP
Main jobHosted LoRA post-training API and recipe layerExpert-parallel communication library for MoE dispatch and combine
Primary userResearcher writing a local SFT or RL loopSystems engineer building a distributed MoE runtime
Compute ownershipThinking Machines Lab's managed workersYour cluster, accelerator fabric, and process groups
Core interfaceforward_backward, optim_step, sample, checkpointsDispatch/combine buffers, all-to-all transport, synchronization
Trainable stateLoRA adapter plus remote optimizer stateNo model-training policy or reward loop
RL abstractionRewards, trajectories, advantages, masks, loss namesNone. It moves expert tokens and gradients efficiently
Model supportService catalog and adapter integrationsRuntime and hardware constraints you configure
License boundaryApache-2.0 SDK/Cookbook plus hosted termsMIT source plus your infrastructure and model terms
Failure focusAPI retries, stale sampler, checkpoint, data and termsRank failure, collective hang, buffer capacity, network topology

DeepEP can sit below a self-hosted trainer that runs models like DeepSeek's MoE systems. It doesn't replace the RL loop. Tinker can help a researcher run a reward experiment on an available MoE model without assembling expert-parallel collectives. It doesn't expose DeepEP's dispatch buffers, remote direct memory access (RDMA) tuning, or expert routing internals.

The projects complement each other in a curriculum. Tinker teaches the algorithm contract and service boundary. DeepEP teaches the communication contract that makes sparse expert training and inference fit a real cluster. Megatron or another distributed trainer connects both contracts into a full-parameter or adapter training runtime.

A hosted service's wall-clock latency and a communication kernel's tokens-per-second number answer different questions. Compare total experiment cost and reproducibility boundaries across the full workflow; a single operation's microbenchmark cannot represent both paths.

A practical reading exercise

Use the tiny deterministic clamp task before trying a large model.

  1. Run the offline contract check and SDK probe above, then build one SFT datum with a prompt, target tokens, and response-only weights.
  2. Submit one forward/backward request for that batch.
  3. Submit its optimizer request before awaiting both results; inspect both outcomes.
  4. Save a sampler checkpoint with a step name.
  5. Sample four completions from the saved path.
  6. Grade the actual completions with a sandboxed evaluator. Don't force the illustrative 1/0/1/0 reward pattern.
  7. Center rewards and inspect token-aligned advantages.
  8. Compare the hosted token receipt with your offline alignment fixture; keep the actual rewards even when they differ from the illustrative group.
  9. Only then add asynchronous sampling or a real environment.

The exercise should produce a run manifest alongside reward numbers. Record the model identifier, rank, target modules, renderer, loss, optimizer settings, checkpoint path, sample tokens, reward list, group mean, and mask counts. A manifest lets another reader inspect every contract boundary; screenshots omit that evidence.

For a large model, scale one dimension at a time. Increase prompt length, group size, number of concurrent environments, adapter rank, or pipeline depth separately. A reward change after four simultaneous changes can't tell you which boundary moved.

A finished clamp run should let you point at the shifted target, the sampler path, and the optimizer moments separately. Interpret the loss after confirming those three inputs.

Evaluation rubric

  • Trace ownership: distinguish local client handles from hosted adapter, optimizer, and sampler state, without inferring server internals from an SDK checkout.
  • Audit one trajectory: derive shifted targets, preserve behavior logprobs, include the first answer token, and explain why equal lengths alone aren't sufficient.
  • Design recovery: pair updates in order, bound in-flight work, choose an optimizer-preserving resume point, and record the sampler version separately.

Follow-up questions

Can an identical-reward group still update the adapter?

Centering makes its reward advantages zero, but a KL penalty or another auxiliary objective can still contribute gradients. Filtering removes the group from the update, including any terms computed on those samples; retaining it doesn't manufacture reward contrast.

Why can evaluation remain stale after a successful optimizer step?

The training adapter changed, but the sampler snapshot didn't. Publish sampler-compatible weights, select that saved path, and record it beside the trajectory. Bind each trajectory to the sampler's loaded policy version; an optimizer receipt records only training-side success.

Mastery quiz

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A Tinker client submits training calls from a local process while model execution happens on hosted capacity. Which boundary should the design expose?
2.A LoRA run updates adapter matrices while the base checkpoint remains unchanged. Which resource claim is accurate?
3.A training loop calls forward_backward on one batch and optim_step on a different logical update. Which invariant is at risk?
4.A sampled response has token IDs [11, 12, 13] but logprobs aligned to [10, 11, 12]. What should the RL datum builder do?
5.Four trajectories have rewards [1, 0, 1, 0], and the batch mean is used as a simple centered advantage. What values result?
6.A restart restores adapter weights but optimizer moments reset. Which Tinker recovery path should be used?
7.A job selects a model by a catalog label that changed capabilities between service deployments. What should the client verify before sending RL data?
8.A toy prompt [10, 11] is followed by answer tokens [20, 21]. Inputs are [10, 11, 20] and targets [11, 20, 21]. Which response-only weights are correct?
9.A loop successfully updates its training adapter, but its SamplingClient still points at an older sampler checkpoint. What makes evaluation use the new policy?
10.A loop awaits forward/backward completion before submitting the matching optimizer request. What change addresses the clock-cycle gap without changing the update order?

10 questions remaining.

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.