Running a Tunix Recipe: From a YAML File to an RLCluster

There is no tunix command. You run a module, hand it a YAML file as the first positional argument, and a few hundred lines of config code turn that file into a tokenizer, a dataset, a device mesh per role, three or four models, a rollout config, a cluster and a learner, in that order, before a single token is generated. This reads the whole launch surface at a pinned commit of google/tunix: the four sources config is merged from and the exact rule that makes setting one key by both an environment variable and the command line an error; the two base YAML files and the eleven keys that exist in only one of them; the five sections where a partial override silently deletes every sibling key you did not restate; the unknown-key check that catches a typo at the top level and cannot see one a single level down. A recipe turns out not to be a YAML file at all but a Python module exposing create_dataset, and a reward function is any module-level function in a file you name. The signature exhibit walks one real launch command from the file to the objects, with the arithmetic the code actually does at each step.

Concept · AI / ML. The source ↗

A free, interactive, animated visual explainer of Running a Tunix Recipe: From a YAML File to an RLCluster — built to be understood, not skimmed.

Questions

How do you launch a Tunix training run from the command line?
You invoke a module, not a command. There are no console-script entry points in the project metadata, so a GRPO run is python3 -m tunix.cli.grpo_main followed by a path to a YAML file, then any number of key=value overrides; PEFT and SFT go through tunix.cli.peft_main and PPO through tunix.cli.ppo_main. The first positional argument must be the config path and nothing else: the loader raises if there are fewer than two arguments or if the second one contains an equals sign. Two base files ship with the library, tunix/cli/base_config.yaml and tunix/cli/base_agentic_config.yaml, and either name is special-cased so a script can pass the short relative path from any working directory. In practice you rarely type the command yourself. The examples directory carries shell wrappers such as examples/rl/grpo/gsm8k/run_gemma2_2b.sh that set shell variables, compute warmup and decay steps with awk, and then assemble the same python3 -m invocation with an override_config_file argument and sixteen dotted overrides.
In what order does Tunix merge configuration?
Four sources, later ones winning. First the base YAML named by the first positional argument. Second an optional override_config_file=path.yaml, which is not merged as a document: it is flattened into dotted key=value strings and pushed through the same override path as the command line. Third the command-line key=value arguments themselves, appended after the file overrides so they win. Fourth, environment variables whose name is the uppercased key with a T_ prefix, so batch_size is set by T_BATCH_SIZE. There is one hard rule between the last two: setting the same top-level key by both an environment variable and a command-line argument raises rather than picking a winner, with the message that you are passing overrides by both CLI and ENV and this is not allowed. A T_ variable that does not match any key in the base YAML is also an error, on the reasoning that it is assumed to be a mistake, and so is one that is not entirely uppercase.
What is a recipe in Tunix?
A Python module, not a YAML file. The config key data_module holds a specifier that gets imported and called, and the module must expose create_dataset returning a grain.MapDataset; it may also expose batch_fn, which becomes the custom batch function later. The specifier is richer than a plain module path: it accepts module, module:function, module:function(arg=value), and an absolute path ending in .py followed by a colon and a function name, with the arguments parsed out of the string by Python’s own ast module. Everything under the data_config key is forwarded to that function as keyword arguments. The one recipe that ships, tunix/cli/recipes/deepscaler_data.py, reads a JSON training set and a parquet eval set through fsspec so both can be GCS paths, and returns a dataset whose every element has prompts, question and answer. Writing a recipe is the supported way to train on your own data, and it means you never edit the library to launch a new run.
Why is my Tunix config key being ignored?
Almost certainly because you misspelled it one level down. The loader checks unknown keys, but it checks them against the top level only: the command-line parser turns rollout_config.temperture=0.7 into a single top-level key named rollout_config, that key does exist in the base YAML, and the check passes. The merge then adds temperture alongside temperature rather than rejecting it, because the recursive merge writes any key the source carries. What happens next depends on which section you were editing. The rollout section is filtered against the field names of the RolloutConfig dataclass before it is constructed, so an unrecognised key is dropped without a message and the run proceeds on the old value. The GRPO, PPO and training sections are splatted into their dataclasses whole, so the same typo raises a TypeError about an unexpected keyword argument. And a typo at the top level, rollout_confg rather than rollout_config, is caught immediately with a message naming the key.
How do you add a custom reward function to a Tunix GRPO run?
Write module-level functions with the signature (prompts, completions, **kwargs) returning one float per completion, put them in a file, and name that file in the reward_functions list. The loader tries a plain module import first when the string contains no slash, and otherwise loads the file from the project root with a source file loader. In the default mode it then takes every module-level function that does not begin with an underscore and whose __module__ matches the file, so one file can carry several reward signals and they all become separate reward functions; the shipped gsm8k file carries four of them, scoring exact tag format, approximate tag format, the extracted answer, and the first number after the answer tag. Set verl_compatible: true instead and the loading mode changes completely: each module is wrapped into a single function that calls compute_score once per completion with the ground truth pulled out of a reward_model argument.

Related explainers