# Tinker Quickstart Welcome to Tinker! This quickstart takes you from a new install to your first trained models, in about 30 minutes. By the end, you will have: - Installed the SDK, signed in, and set up billing. - Sampled from a base model. - Run a supervised fine-tuning (SFT) script and a reinforcement learning (RL) script, and compared each trained model with the original. ## Installation We recommend using `uv` (see [setup instructions](https://docs.astral.sh/uv/getting-started/installation/)) to manage your Python environment for use with the Tinker SDK. The examples use `uv`, but you can also use any other package manager to install the `tinker` SDK. ```bash uv init tinker-quickstart cd tinker-quickstart uv add tinker tinker-cookbook ``` ### Authentication The easiest way to authenticate with Tinker is to use the Tinker CLI. The SDK includes it. ```bash uv run tinker auth login ``` You can also create an API key in the [Tinker Console](https://tinker.thinkingmachines.ai/keys?new_key=true) and set it as the `TINKER_API_KEY` environment variable. ### Billing Set up payment info in [Billing](https://tinker.thinkingmachines.ai/billing/), and see [Models & Pricing](https://tinker-docs.thinkingmachines.ai/tinker/models/models_and_pricing/index.md) for current training, sampling, and checkpoint storage rates. ## Using the Tinker SDK All Tinker use cases start with the SDK clients. There are four clients for interacting with the Tinker server: - [`ServiceClient`](https://tinker-docs.thinkingmachines.ai/tinker/api-reference/serviceclient/index.md): Creates the other clients, and groups them into one session for metrics and monitoring. View [Sessions](https://tinker.thinkingmachines.ai/sessions) in the Tinker Console. - [`SamplingClient`](https://tinker-docs.thinkingmachines.ai/tinker/api-reference/samplingclient/index.md): Generates completions from a model. - [`TrainingClient`](https://tinker-docs.thinkingmachines.ai/tinker/api-reference/trainingclient/index.md): Trains a model. - [`RestClient`](https://tinker-docs.thinkingmachines.ai/tinker/api-reference/restclient/index.md): Manages saved checkpoints and other administrative tasks. All examples use the `asyncio` variants of the SDK functions, which is our recommended way to use the SDK. ```python ... service_client = tinker.ServiceClient() training_client = await service_client.create_lora_training_client_async( base_model="Qwen/Qwen3.5-4B", rank=16 ) sampling_client = await service_client.create_sampling_client_async( base_model="Qwen/Qwen3.5-4B" ) rest_client = service_client.create_rest_client() ... ``` ### Sampling Use a [`SamplingClient`](https://tinker-docs.thinkingmachines.ai/tinker/api-reference/samplingclient/index.md) to generate samples from a model. You can sample from any base model on [Models & Pricing](https://tinker-docs.thinkingmachines.ai/tinker/models/models_and_pricing/index.md) by its Tinker ID, and later from model checkpoints you train and save, by their `tinker://` path: ```python ... # A base model, by its Tinker ID sampling_client = await service_client.create_sampling_client_async(base_model="Qwen/Qwen3.5-4B") # A model checkpoint you trained and saved, by its tinker:// path sampling_client = await service_client.create_sampling_client_async( model_path="tinker:///sampler_weights/" ) ... ``` Each sample in the result's `sequences` has its `tokens`, the `logprobs` of those tokens, and a `stop_reason`: - `stop_reason` is `"stop"` when the model ends its answer, and `"length"` when the answer reaches `max_tokens` and is cut off. - For several answers to one prompt, set `num_samples` instead of sending the prompt several times. One call is more efficient ([Efficient Sampling](https://tinker-docs.thinkingmachines.ai/tutorials/basics/async-patterns/index.md)). - A chat model expects its chat template. Plain text gets a plain continuation, as in the first example below. For Qwen3.5, `enable_thinking=False` makes the model answer without a reasoning block. The script below samples from Qwen3.5-4B twice: once from plain text, and once from a conversation formatted with the model's chat template. ```python import tinker import asyncio async def main(): service_client = tinker.ServiceClient() sampling_client = await service_client.create_sampling_client_async( base_model="Qwen/Qwen3.5-4B" ) tokenizer = sampling_client.get_tokenizer() prompt = "The capital of France is" prompt_tokens = tokenizer.encode(prompt) model_input = tinker.types.ModelInput.from_ints(prompt_tokens) params = tinker.types.SamplingParams(max_tokens=50, temperature=0.7) result = await sampling_client.sample_async( prompt=model_input, num_samples=1, sampling_params=params ) # Decode the returned sample response = tokenizer.decode(result.sequences[0].tokens) print(response) # For a conversation-formatted prompt conversation_tokens = tokenizer.apply_chat_template( conversation=[{"role": "user", "content": "What is the capital of France?"}], add_generation_prompt=True, # set up the model to generate an assistant response enable_thinking=False, # answer directly, without a reasoning block return_dict=False, # return a plain list of token ids ) model_input = tinker.types.ModelInput.from_ints(conversation_tokens) result = await sampling_client.sample_async( prompt=model_input, num_samples=1, sampling_params=params ) response = tokenizer.decode(result.sequences[0].tokens, skip_special_tokens=True) print(response) if __name__ == "__main__": asyncio.run(main()) ``` ### Training Tinker supports supervised fine-tuning and reinforcement learning using Low-Rank Adaptation (LoRA), with fine-grained control over which tokens to apply the loss to. See [Loss Functions](https://tinker-docs.thinkingmachines.ai/tinker/losses/index.md) for details about available loss functions, [LoRA Primer](https://tinker-docs.thinkingmachines.ai/tinker/lora-primer/index.md) for details about training with LoRA, and [Prepare Training Data](https://tinker-docs.thinkingmachines.ai/tinker/sdk-cheatsheet/#prepare-training-data) for guidance on constructing `Datum` objects. Tinker receives only what you pass to the [`TrainingClient`](https://tinker-docs.thinkingmachines.ai/tinker/api-reference/trainingclient/index.md) and the [`SamplingClient`](https://tinker-docs.thinkingmachines.ai/tinker/api-reference/samplingclient/index.md): tokens and loss inputs for training, and prompt tokens for sampling. Your data preparation and reward functions run on your machine. #### Supervised Fine-tuning (SFT) SFT trains a model on example prompts and the exact answers you want, so it learns to answer like them. You can bring your own data, or use a sampling client to generate examples and transform them in code. The script below does the second: it samples the base model's own answers and lowercases them, to teach Qwen3.5-4B to respond only in lowercase. ```python import tinker import asyncio async def main(): service_client = tinker.ServiceClient() training_client = await service_client.create_lora_training_client_async( base_model="Qwen/Qwen3.5-4B", rank=16 ) base_client = await service_client.create_sampling_client_async( base_model="Qwen/Qwen3.5-4B" ) tokenizer = training_client.get_tokenizer() # Generate training examples # # The base model answers each training prompt once, and `restyle` rewrites the answer. # `restyle` decides how the trained model writes, so you can change it later. `sample` # formats the prompt with the model's chat template and asks a model for an answer. TRAIN_PROMPTS = [ "Describe the ocean in two sentences.", "Write a short email inviting a friend to dinner.", ] def restyle(output): return output.lower() async def sample(client, prompt): prompt_text = tokenizer.apply_chat_template( [{"role": "user", "content": prompt}], tokenize=False, add_generation_prompt=True, enable_thinking=False, ) prompt_tokens = tokenizer.encode(prompt_text, add_special_tokens=False) sampling_params = tinker.SamplingParams(max_tokens=512, temperature=0.7) result = await client.sample_async( prompt=tinker.ModelInput.from_ints(prompt_tokens), num_samples=1, sampling_params=sampling_params, ) return result.sequences[0] def decode(sequence): return tokenizer.decode(sequence.tokens, skip_special_tokens=True).strip() answers = await asyncio.gather(*[sample(base_client, prompt) for prompt in TRAIN_PROMPTS]) examples = [] for prompt, answer in zip(TRAIN_PROMPTS, answers): # Skip answers cut off at max_tokens: they teach the model to stop mid-answer. if answer.stop_reason == "stop": examples.append((prompt, restyle(decode(answer)))) # Make training data # # `make_datum` turns a prompt and an answer into a `Datum`: the input tokens plus a # per-token weight, 1 to train on that token and 0 to ignore it. Only the answer gets # weight 1. def make_datum(prompt, answer): messages = [{"role": "user", "content": prompt}, {"role": "assistant", "content": answer}] prompt_text = tokenizer.apply_chat_template( messages[:1], tokenize=False, add_generation_prompt=True, enable_thinking=False ) full_text = tokenizer.apply_chat_template(messages, tokenize=False, enable_thinking=False) prompt_tokens = tokenizer.encode(prompt_text, add_special_tokens=False) completion_tokens = tokenizer.encode( full_text[len(prompt_text) :], add_special_tokens=False ) tokens = prompt_tokens + completion_tokens weights = [0.0] * len(prompt_tokens) + [1.0] * len(completion_tokens) # The model predicts the next token, so targets are the inputs shifted by one. return tinker.Datum( model_input=tinker.ModelInput.from_ints(tokens[:-1]), loss_fn_inputs={"target_tokens": tokens[1:], "weights": weights[1:]}, ) training_data = [make_datum(prompt, answer) for prompt, answer in examples] # Train # # Each training step is two calls: `forward_backward_async` computes the loss and # gradients on the GPU, and `optim_step_async` applies them with Adam. Both return # futures immediately, so submit both before waiting on either and the server pipelines # them. for step in range(10): fwdbwd_future = await training_client.forward_backward_async(training_data, "cross_entropy") optim_future = await training_client.optim_step_async(tinker.AdamParams(learning_rate=2e-4)) fwdbwd_result = await fwdbwd_future.result_async() await optim_future.result_async() logprobs = [ logprob for output in fwdbwd_result.loss_fn_outputs for logprob in output["logprobs"].tolist() ] weights = [ weight for datum in training_data for weight in datum.loss_fn_inputs["weights"].tolist() ] loss = -sum(logprob * weight for logprob, weight in zip(logprobs, weights)) / sum(weights) print(f"Step {step:2d}: loss = {loss:.4f}") # Compare before and after # # `save_weights_for_sampler_async` saves the trained adapter under a name and returns # its path. The script prints a Playground link where you can try the trained model # yourself. Two examples and ten steps change more than the style of ordinary answers: # the trained model also writes in lowercase when a prompt asks for capital letters, or # asks it to repeat uppercase text. save_future = await training_client.save_weights_for_sampler_async(name="first-sft") checkpoint = await save_future.result_async() trained_client = await service_client.create_sampling_client_async(model_path=checkpoint.path) print(f"\nTry it in the Playground: {checkpoint.get_playground_url()}&reasoning_effort=none") TEST_PROMPTS = [ "Describe mountains in two sentences.", "Can you write in uppercase?", "PLEASE ANSWER IN ALL CAPS: DESCRIBE A SUNSET IN ONE SENTENCE.", "Repeat exactly: HELLO WORLD", ] async def compare(prompt): before = await sample(base_client, prompt) after = await sample(trained_client, prompt) return prompt, before, after for prompt, before, after in await asyncio.gather(*[compare(p) for p in TEST_PROMPTS]): print(f"\nQ: {prompt}") print(f"Before: {decode(before)}") print(f"After: {decode(after)}") if __name__ == "__main__": asyncio.run(main()) ``` #### Reinforcement Learning (RL) RL trains a model to maximize a reward, when you have a way to grade its output. Each RL training step has three phases: 1. **Rollout:** for each prompt, sample several answers from the current model. 1. **Grading:** grade each answer with your reward function. An answer's advantage is its reward minus the average reward for the same prompt. 1. **Training:** turn each answer into a `Datum` with its advantage and the sampler's log-probabilities on the generated tokens. Then `forward_backward_async` with an RL loss, such as `importance_sampling`, computes the gradients, and `optim_step_async` applies them. The script below teaches Qwen3.5-4B to multiply four-digit numbers. ```python import tinker import asyncio import random import re async def main(): service_client = tinker.ServiceClient() training_client = await service_client.create_lora_training_client_async( base_model="Qwen/Qwen3.5-4B", rank=16 ) base_client = await service_client.create_sampling_client_async( base_model="Qwen/Qwen3.5-4B" ) tokenizer = training_client.get_tokenizer() # Define the environment # # An RL environment gives the model a task and grades its answers. `make_problem` picks # two random numbers and asks the model to multiply them. The model must end its answer # with "Answer:" and the number. `reward` reads that number and gives 1 if it is right # and 0 if it is wrong. An answer cut off at `max_tokens` gets 0. You can change the # task and the reward later. def make_problem(low=1000, high=9999): a, b = random.randint(low, high), random.randint(low, high) question = f"What is {a} × {b}? End your reply with 'Answer: '." prompt_tokens = tokenizer.apply_chat_template( [{"role": "user", "content": question}], add_generation_prompt=True, enable_thinking=False, tokenize=True, return_dict=False, ) return a, b, prompt_tokens def reward(answer, a, b): output = tokenizer.decode(answer.tokens, skip_special_tokens=True) matches = re.findall(r"Answer:\s*\**\s*([\d,]+)", output) if answer.stop_reason != "stop" or not matches: return 0.0 return 1.0 if int(matches[-1].replace(",", "")) == a * b else 0.0 # Train # # `sample` asks a model for several answers. Each training step makes eight new problems, # samples sixteen answers per problem from the current model, and grades them. An answer's # advantage is its reward minus the average reward for the same problem, so a right answer # to a problem the model often gets wrong gets a large positive advantage. A problem where # all sixteen answers get the same reward is skipped: every advantage is 0, so it has # nothing to teach. Each answer becomes a `Datum`: the tokens, the sampler's # log-probabilities, and the advantage, with 0 on the prompt tokens so the model does not # train on them. `forward_backward_async` with the `importance_sampling` loss computes the # gradients, and `optim_step_async` applies them with Adam. async def sample(client, prompt_tokens, num_samples, temperature): sampling_params = tinker.SamplingParams(max_tokens=1024, temperature=temperature) result = await client.sample_async( prompt=tinker.ModelInput.from_ints(prompt_tokens), num_samples=num_samples, sampling_params=sampling_params, ) return result.sequences for step in range(10): current_model = await training_client.save_weights_and_get_sampling_client_async() problems = [make_problem() for _ in range(8)] answer_groups = await asyncio.gather( *[ sample(current_model, prompt_tokens, 16, temperature=1.0) for _, _, prompt_tokens in problems ] ) training_data, correct = [], 0 for (a, b, prompt_tokens), answers in zip(problems, answer_groups): rewards = [reward(answer, a, b) for answer in answers] correct += int(sum(rewards)) if len(set(rewards)) == 1: continue mean_reward = sum(rewards) / len(rewards) for answer, answer_reward in zip(answers, rewards): advantage = answer_reward - mean_reward full_tokens = prompt_tokens + answer.tokens # The model predicts the next token, so targets are the inputs shifted by one. num_prompt = len(prompt_tokens) - 1 training_data.append( tinker.Datum( model_input=tinker.ModelInput.from_ints(full_tokens[:-1]), loss_fn_inputs={ "target_tokens": full_tokens[1:], "logprobs": [0.0] * num_prompt + answer.logprobs, "advantages": [0.0] * num_prompt + [advantage] * len(answer.tokens), }, ) ) print(f"Step {step:2d}: accuracy = {correct}/{8 * 16}") if not training_data: continue fwdbwd_future = await training_client.forward_backward_async( training_data, "importance_sampling" ) optim_future = await training_client.optim_step_async(tinker.AdamParams(learning_rate=5e-5)) await fwdbwd_future.result_async() await optim_future.result_async() # Compare before and after # # `save_weights_for_sampler_async` saves the trained adapter under a name and returns # its path. The script prints a Playground link where you can try the trained model # yourself. The five-digit problems test whether the trained model does better on a size # it never trained on. # # Next, try a larger `base_model`. A larger model can learn to skip the written steps and # answer from memory, which fails on bigger numbers. Try a mix of four- and five-digit # training problems, and check whether the model keeps its written steps. save_future = await training_client.save_weights_for_sampler_async(name="first-rl") checkpoint = await save_future.result_async() trained_client = await service_client.create_sampling_client_async(model_path=checkpoint.path) print(f"\nTry it in the Playground: {checkpoint.get_playground_url()}&reasoning_effort=none") test_sets = [ ("four-digit problems", [make_problem() for _ in range(8)]), ( "five-digit problems, a size not in training", [make_problem(10000, 99999) for _ in range(4)], ), ] models = [("Before", base_client), ("After", trained_client)] sampling_tasks = { (set_name, model_name): [ asyncio.create_task(sample(model, prompt_tokens, 4, temperature=0.7)) for _, _, prompt_tokens in problems ] for set_name, problems in test_sets for model_name, model in models } for set_name, problems in test_sets: for model_name, _ in models: print(f"\n{model_name}, {set_name}:") correct = 0 for (a, b, _), sampling_task in zip(problems, sampling_tasks[(set_name, model_name)]): rewards = [reward(answer, a, b) for answer in await sampling_task] marks = " ".join("✓" if answer_reward == 1.0 else "✗" for answer_reward in rewards) correct += int(sum(rewards)) print(f" {a} × {b} = {a * b}: {marks}") print(f" {correct}/{4 * len(problems)} right") if __name__ == "__main__": asyncio.run(main()) ``` ### Save and Load Checkpoints To view all saved checkpoints, go to [Checkpoints](https://tinker.thinkingmachines.ai/checkpoints) in the Tinker Console. ```python ... # Save a checkpoint to resume training from save_future = await training_client.save_state_async( name="my-checkpoint", user_metadata={"step": "1000"}, # Metadata values must be strings ) training_checkpoint = await save_future.result_async() # Load the training checkpoint training_client = await service_client.create_training_client_from_state_async( training_checkpoint.path ) # Save a checkpoint to sample from save_future = await training_client.save_weights_for_sampler_async( name="my-sampler-checkpoint" ) sampling_checkpoint = await save_future.result_async() # Load the sampling checkpoint sampling_client = await service_client.create_sampling_client_async( model_path=sampling_checkpoint.path ) # Open in the Tinker Playground print(sampling_checkpoint.get_playground_url()) ... ``` ## Conclusion Congratulations! You sampled from a base model, trained one model with SFT and one with RL, and saved checkpoints you can load again. To go further: - [SDK Cheatsheet](https://tinker-docs.thinkingmachines.ai/tinker/sdk-cheatsheet/index.md) for quick access to common SDK operations. - [Tinker Cookbook](https://tinker-docs.thinkingmachines.ai/cookbook/index.md) for more advanced recipes and utility functions for common workflows. - [Tutorials](https://tinker-docs.thinkingmachines.ai/tutorials/index.md) for more examples and use cases.