# Affine (Bittensor SN120) v2: the miner contract Generated 2026-11-14 10:23:36 UTC from snapshot `ddba3209ffa25411a83e61f286006cecc64249a701924fe21fb3817c7ab9dfc2`. The machine-readable companion is mining.json. It has the same `snapshot_digest`. If the two digests differ, an update is in progress: fetch both files again. These two files are the whole miner contract. The document of the local tool (section 10) explains a tool; it sets no rule. You upload one JSON file per hotkey: prompts, rollouts and rewards. At round start the validator copies it, checks it, and trains one bounded LoRA from the current head M with the published recipe. Every LoRA of the round and a null copy of M are scored on the same public tasks with the same seeds. At most one LoRA wins, and only if it beats M by more than 1 standard error. The winner is applied to the head and its miner becomes a king. Only kings earn. ## 1. Network and head - Network: Bittensor Finney, netuid 120. Environment: `math` (pin `none`). - Head M `version_id`: `8a24aba6380a8c0a9a106e60758f23fde38dc20dc2eb9316b4fc28d92d9e17ed` (parent `6ae7347b8b8bf8f6a6c78bfde5378c2e8303b3f8f0e8121a9e31a42f30fcadba`) - `base_hash`: `10757e879efe5b916ae92c019fbefbcce880b5e424a05c0198c26f8ee1a901d7`; fp32 master digest: `d1e50d1ff16aa99a9bbcdf0f856ed98d03dc9cb2d4caabd1df31724586c52a7f` - Made by round `r000006` at 2026-11-14 08:02:22 UTC. - Head manifest (every file with its sha256): `public/heads/8a24aba6380a8c0a9a106e60758f23fde38dc20dc2eb9316b4fc28d92d9e17ed.json` - `params_hash`: `b81b9198f70637a54b15a70edfb02c6a5d6366a049a202838324e4ad21115a30`; `recipe_id`: `97a5a220719049eb1a4cd9ea8c3988aaeee610d49e29acf10ca7111f595ec3b3` - Public objects are served under: https://affine.io. Each key below is a path under it, except the weight files. - Weight files are not served under the public address. Signed download links for the files of the current head (the base, the stacked LoRAs and the fp32 master) are in https://affine.io/public/read_links.json. Each link works until `expires_at`; fetch the file again for new links. Check each file against its `sha256` and `size`. `head_version_id` in that file names the head of the links. To mine you need the `base` and the `loras`. You do not need the `master`: it is only for a check of a fold. - This file and mining.json are also at https://affine.io/llms.txt and https://affine.io/mining.json. There the two files are always a matching pair. - Send a client name of your own (the HTTP header `User-Agent`) with each read of a file under `public/` and of a link file (section 5). The site can answer HTTP 403 to a read with the default client name of Python's `urllib` (`Python-urllib/`), also for a file that exists. curl, wget and the Python package `requests` send a name that the site accepts. HTTP 403 does not say that a file is missing: with an accepted name a missing file gives HTTP 404. Each command that this file names sends a name of its own. No LoRA is stacked on the base. ## 2. The training recipe (recipe_id `97a5a220719049eb1a4cd9ea8c3988aaeee610d49e29acf10ca7111f595ec3b3`) The validator trains one LoRA from head M for each admitted batch. It runs the same code and the same recipe for every batch. Your batch is the only training data. A batch made against an older head is trained on the current head. `recipe_id` is the sha256 of the canonical JSON (sorted keys, no spaces) of the recipe object in mining.json (`params.recipe`). A new recipe gets a new id, and it is shown here before the next round starts. | field | value | meaning | | --- | --- | --- | | `method` | grpo-lora | GRPO on the LoRA weights only; the base weights are frozen | | `steps` | 1 | optimizer steps over your batch | | `lora_lr` | 1e-05 | learning rate of each step | | `max_grad_norm` | 1 | gradient clip: with steps and lora_lr, this is the step-size cap | | `global_norm_cap` | none | optional cap on the norm of the whole adapter update | | `group_size` | 0 | rollouts per GRPO group; 0 = the groups of your file | | `sample_temperature` | 1 | temperature your rollouts were sampled at | | `rank` | 16 | LoRA rank | | `alpha` | 32 | LoRA scale | | `dropout` | 0 | LoRA dropout | | `target_modules` | q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj | layers that get an adapter | | `ppo_clip` | 0.2 | the probability ratio (trained policy / head) of a token is clipped to 1 +- this value | | `dual_clip` | 3 | a rollout with a negative advantage: its loss per token is at most this many times its advantage | | `kl` | 0 | KL term; 0 = off | | `support_top_k` | 20 | the policy-gradient term uses only tokens the head could sample (top-k, then top-p) | | `support_top_p` | 0.95 | top-p of that support | | `derail_window` | 32 | a rollout that hit the cap, with a negative advantage: window of the derail test, in tokens | | `derail_threshold` | -3 | the derail point is the first token where the mean log-probability of the window is below this | | `derail_tail` | 64 | tokens later than this many after the derail point do not train | | `train_max_output_tokens` | 32768 | training reads at most this many output tokens of a rollout; it is not the scoring cap | | `train_max_context_tokens` | 36864 | training reads at most this many prompt + output tokens of a rollout | | `code_digest` | e4ae285f4b60121b0737d04d193fec92291df3189d739e07138df6b4ceaf697d | sha256 of the training code | | `image_digests` | 9a4b16ff282b21baf44350e54c852ed59586b9efb6fe09e5cd2f1e288f4c108a | worker images that may train and score | - Training code: `sn120_v2/gpu/` in https://github.com/AffineFoundation/affine/tree/v2/v2 (sha256 `e4ae285f4b60121b0737d04d193fec92291df3189d739e07138df6b4ceaf697d`). - `code_digest` is the id of the file set `sn120_v2/gpu/*.py` (section 9). The box refuses to train or score when its own files have another id. `python -m sn120_v2.gpu.plan` prints the `code_digest` of your copy of the code. - Each prompt is one user message, rendered with the chat template of the head and the generation prompt. ### What you may set You may set only these fields, in the `train` object of your file. Each one replaces the recipe value above for your LoRA only. | key | default | allowed | | --- | --- | --- | | `train.lora_lr` | 1e-05 | a number from 1e-06 to 0.0001, both ends included | | `train.steps` | 1 | a whole number from 1 to 4, both ends included | | `train.group_size` | 0 | a whole number from 2 to 64, both ends included | | `train.sample_temperature` | 1 | a number from 0.5 to 1.5, both ends included | | `train.rank` | 16 | a whole number from 8 to 64, both ends included | | `train.alpha` | 32 | a number from 8 to 128, both ends included | | `train.target_modules` | q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj | a list: a non-empty subset of q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj | - These are the bounds for launch. The bounds are part of the recipe: a change of a bound gives a new `recipe_id`. - Every key is optional. A key that you do not set keeps its default. - Values are never clamped. A value outside its bound, a value of the wrong type, or any other key under `train` makes the whole file invalid. - `train` is allowed in affine-batch-v2, affine-batch-v3. It is not allowed in affine-batch-v1. - Your `train` values are published only if your batch wins. - A file with `train` keys that only v1 had (`lr`, `kl`, `step_mult`, the knobs) is invalid. The seat is skipped and not charged, as with any invalid file (section 4), until the miner uploads a valid file. ### What the trainer does with them - `group_size`: 0 means one group per prompt, with all rollouts of the prompt as the file has them. A value G above 0 cuts the rollouts of each prompt, in file order, into groups of G; a last group with fewer than G rollouts stays a group. The advantage of a rollout is its reward minus the mean reward of its group. The default is 0. - `sample_temperature` T: you state the temperature at which you sampled your rollouts. The trainer divides the logits of the head by T before it scores the tokens of a rollout. Evaluation always samples at temperature 1. The default T is 1. - `global_norm_cap`: the largest allowed Frobenius norm of the whole adapter update, sqrt(sum over all target modules of ||(alpha / rank) * B @ A||_F^2), in the units of the model weights. If the trained update is larger, every B matrix is multiplied by cap / norm. The train report states the norm before the cap and the norm after it (`step_norm` is the norm after the cap). The number is set in the params at the mainnet genesis; unset means no cap. It is unset now: there is no cap. - Train caps: a rollout above `train_max_output_tokens` (32768) or `train_max_context_tokens` (36864, prompt + output) is truncated, not rejected. The train report counts the truncated rollouts. A rollout with no output token left is dropped and counted. ### The training procedure This is everything that the trainer does with a file, in the order in which it does it. The code is in three files: `sn120_v2/gpu/trainer.py` (the checks around the job), `sn120_v2/gpu/train_job.py` (the job on the GPUs) and `sn120_v2/gpu/batch.py` (rows, advantages and token caps). Section 10 has the command that runs this code on your machine. - Model: the base files of head M (`base_hash`, section 1), loaded as `bfloat16` in the model class that their `config.json` names. The model is in evaluation mode and all its weights are frozen. The box trains and scores only a head with no stacked LoRA: it refuses a head whose LoRAs are not folded. - Recipe: the values of the table above, with your `train` values in their place. The trainer runs only the method `grpo-lora`, with no dropout and with the KL term off. - Seed: the first 63 bits of sha256("|train|"), as a whole number. The train report states it. - LoRA layers: each linear layer whose name is one of `target_modules` (q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj) gets two matrices: A (rank x inputs) and B (outputs x rank). The layer then gives layer(x) + (alpha / rank) * B(A(x)), with rank 16 and alpha 32. A starts with values drawn uniformly from -1/sqrt(inputs) to +1/sqrt(inputs), from a `torch.Generator` on the CPU with the seed above, layer by layer in the order of `named_modules()`. B starts at zero, so the model is head M before the first update. A and B are float32, and only A and B train. - Rows: one row for each rollout of the groups that the file check keeps (section 5), in file order. The advantage of a row is its reward minus the mean reward of its group (`group_size`, above). The trainer does nothing else to it: no division by a standard deviation and no clip. - Tokens of a row. Prompt: `tokenizer.apply_chat_template([{"role": "user", "content": prompt}], add_generation_prompt=True, tokenize=False)`, then `tokenizer.encode(text, add_special_tokens=False)`. The template gets no other argument. Output: `tokenizer.encode(output, add_special_tokens=False)`. The tokenizer is the one of the base files. - Room: room = min(`train_max_output_tokens`, `train_max_context_tokens` - prompt tokens). An output shorter than the room gets the end-of-turn token `<|im_end|>` after its last token. An output as long as the room or longer is cut to the room and gets no end token: the trainer takes it as a rollout that did not end (a capped row). A row with no output token or with no room is dropped. - Rows that train: the rows whose advantage is not zero. With no such row the batch is not trained: `skip` (section 4). - The score of a token. The trainer takes the logits of the model at each output token, as float32. The support at a token: take the 20 tokens with the highest logits (`support_top_k`), divide their logits by T (`sample_temperature`) and take the softmax over them. From the most likely token down, a token is in the support while the summed probability of the tokens before it is below 0.95 (`support_top_p`). log q of the token of your rollout = its logit / T - log(sum of exp(logit / T) over the support). - First pass, before any update: the trainer stores log q of every output token under head M. This is the old policy. The mask of a row holds its tokens that are in the support of head M. A token outside the mask never trains. - Derail rule, only for a capped row with a negative advantage. Take the plain log-probability of each output token under head M (all logits, no temperature). The derail point is the first token at which the mean of the last 32 such values (`derail_window`), that token included, is below -3 (`derail_threshold`). The derail point and the 64 tokens after it (`derail_tail`) stay in the mask. Each later token leaves the mask. A row with fewer tokens than the window has no derail point. - Loss, at each step, for a token of a row with advantage A: ratio = exp(log q now - log q of the first pass). "Now" is the model with its LoRA at that step, with the support of that model. Token loss = max(-A * ratio, -A * clip(ratio, 1 - `ppo_clip`, 1 + `ppo_clip`)), with `ppo_clip` 0.2. With A below 0 the token loss is at most -A * `dual_clip`, with `dual_clip` 3. A token counts when it is in the mask of its row and in the support of the model now. Row loss = the sum of its token losses / the number of tokens in its mask. A row with an empty mask gives no loss. Step loss = the sum of the row losses / the number of rows. That number counts each row that was not dropped, also a row with advantage 0. - Steps: 1 (`steps`). One step is one pass over all rows that train and then one update. The gradient of the step loss, over all A and B together, is clipped to the norm 1 (`max_grad_norm`). Then comes one update with the learning rate 1e-05 (`lora_lr`). The optimizer: adam, fresh per LoRA, betas 0.9/0.999, eps 1e-8, weight decay 0. The learning rate has no schedule. A gradient that is not finite stops the training: `skip`. - GPUs: the job has the GPUs of its slot (section 3). It puts one copy of the model on each GPU, but not more copies than rows that train. The rows that train go to the copies in turn, the longest row first. The gradients of the copies are added before the clip. The job never trains on a CPU: with no GPU it stops. - After the last step: the cap `global_norm_cap` (above), then a check that each weight is finite. If one is not: `skip`. - The LoRA is these files: `adapter_config.json`, `adapter_model.safetensors`, `train_report.json`. The weight file holds A and B of each layer as float32, with the names `base_model.model..lora_A.weight` and `base_model.model..lora_B.weight`. The config file has the PEFT layout; its `base_model_name_or_path` is the `base_hash`. The train report holds the optimizer, the seed, the recipe values that were used, the loss and the gradient norm of each step, the count of the mask tokens of the rows that trained, the count of the rows, of the truncated rows and of the dropped rows, `step_norm` and `step_norm_before_cap`. `lora_hash` is the id of this file set (section 9). - A failure of the box in a training (not of the batch) is tried again: at most 3 tries in all, inside the training time. Then the entry is `no_decision` (section 4). - What a training of your own repeats: the seed gives the same A for the same file and round seed. The trained B can differ between two machines, and between two runs on one machine; no run has measured by how much. The tests of the code run this training on a CPU, on a very small model. The box runs it on GPUs. ## 3. The round now - phase: RUNNING (IDLE: no round is open; RUNNING: training and scoring; APPLY: decided, the new head is being published). The last closed rounds are in mining.json under `history`. - round_id: r000008 - head_version_id: 8a24aba6380a8c0a9a106e60758f23fde38dc20dc2eb9316b4fc28d92d9e17ed - params_hash: b81b9198f70637a54b15a70edfb02c6a5d6366a049a202838324e4ad21115a30 - recipe_id: 97a5a220719049eb1a4cd9ea8c3988aaeee610d49e29acf10ca7111f595ec3b3 - round_seed: a963d1d2be4d6f612efcc3cf9fad98f1f6c6a047edbc7756eb16362cef86dfef - irt_snapshot_id: irt-a8cdba8348286daf48862a548858770ca5ea96afe1ab61d846889c8cfca7c9a1 - n_tasks: 2000 - opened_at: 1794651816 - closed_at: 0 - outcome: none - seats: 6 - skipped: 239 | queue_pos | uid | hotkey | batch_sha256 | status | delta | se | z | cap_rate | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 0 | 959 | 5GNdwJi7vnGnF6cZ4TyRo3W4UC5Zy1rWdb3dSrwXbB3S6bkL | e89e078723a07c965b2445aba1c0dc525267720cf6f2d5d47d62c29d137413b4 | none | none | none | none | none | | 1 | 146 | 5DZB6XvmN8A3dSU5x9hiNvvRRLLAfXJcWDQQWicX34eU6edR | 8265a19a16b2416477d0af5c2e51ca5219cbd30036c558d98cf179a2a34a6933 | none | none | none | none | none | | 2 | 963 | 5C4hs65bXAxvpWZaokYEEugiwVRuuWNLLJZf4yKvrvfXGfdg | c4acf7e240fb33d339c7959cf0d06f15a10aa57c1f9e5e26eb7ddb4b022b9b87 | none | none | none | none | none | | 3 | 965 | 5E7TakWHUKnkeyRKhPtjCqZHkE6yEt5FmQHkJEZPVjndhtRS | 3d60e701ea980954f09ac25138616da39c1c4db6e45ae8819e080f03546171d8 | none | none | none | none | none | | 4 | 983 | 5FsVE291LVnUpz4E3cV6P2xEgndGubEh1pXnNARTEa4qjAiS | ce68265f0d8cad4aa599bf3b6afe0c1a05b4fe7d8c920e77f27d184bade95478 | none | none | none | none | none | | 5 | 931 | 5DVYWVrQrXLMM5AGspMYKzaVVkfYi9gvtarWo6DEvb16e3JX | dc3faa6811b821f74c8d30197204bddd05d84fd9ba32ffb8d458e0d3f23d411c | none | none | none | none | none | - A round takes at most 6 batches and needs at least 1. It does not wait for a full round. Registrations for the next round are always open. - Round loop: (1) pin head M, the recipe, the task draw and the public round seed; (2) copy the next valid files in queue order; (3) train one LoRA per file; (4) score every LoRA and a null copy of M (no adapter) on the same tasks and seeds; (5) apply at most one winner; (6) publish; (7) start again. ### The time of a round All times count from the lease: the moment a round takes its entries. A round ends early, as soon as every LoRA is dropped or decided. - Time limit: 100 minutes from the lease to the promotion of the winner. The box stops scoring 10 minutes before it, at minute 90 (the stop time). The stop is hard. The 10 minutes are for the grading of the last chunk, the judging and the promotion. - Training time: the first 40 minutes. Batches train in queue order. Each batch trains on its own slot of GPUs, and the slots train at the same time. A slot is floor(8 / batches) GPUs, and at least 1. So with 8 batches or more, each batch trains on one GPU, 8 batches at a time, and each other batch waits for a free GPU. - At the end of the training time no new training starts, and each training that still runs is stopped. Such a batch is `not_trained`: you are not charged, your seat is first in the next round, and no failure is counted for the seat. Scoring starts when all training is done or stopped. - One exception: a batch that started at the lease had the whole training time. If the first try of its training did not finish in that time, that is a failure of ours: `no_decision`. Section 4 says when such a failure is counted for the seat. If a try of its training failed before (a failure of the box) and a later try is stopped at the end of the training time, the batch is `not_trained` as above: the box took a part of its time. - The box can start its work on a round later than 120 s after the start of the lease, for example when the validator's program started again in the round. Then no batch had the whole training time: a batch that does not finish is `not_trained` as above, and no failure is counted for the seat. - Scoring runs in chunks of 500 tasks. Null M and every open LoRA answer each chunk. An open LoRA is a LoRA that was not dropped at a look (section 6). - The stop time cuts every look, the first one too. The box starts no new chunk after the stop time, and a chunk that is not finished at the stop time is not used. There are no further looks. - At the stop time, with the first look (500 tasks) complete: each open LoRA is judged by the win rule of section 6 on the whole chunks that null M and every open LoRA finished. Those tasks are then the whole test: z strictly above 1 can win. Such an entry is charged, win or lose, as in a full round. - At the stop time, with fewer than 500 tasks scored: each trained LoRA is `not_scored`. You are not charged, your seat is first in the next round, and no failure is counted for the seat. - The lease ends 120 minutes after it starts. A box result that arrives after the end of the lease is never used: the round then has no result (section 4). No task is scored after the stop time: the time between the stop time and the end of the lease is only a reserve, so that a round that stopped in time does not lose its result. - `no_decision` as the status of an entry means: not charged, and the seat goes to the front of the queue (section 4). ## 4. The queue Seats that wait: 246. The next seats, in order: | position | uid | hotkey | registration_block | file_valid | front | demoted | held | | --- | --- | --- | --- | --- | --- | --- | --- | | 0 | 692 | 5CGpPvsA3VznKYEps7VKMBTZMMnvN9k6bbKogmfG4gFBe9R1 | 9422310 | no | yes | no | no | | 1 | 699 | 5CC5bsq82cnh3nPhSqHe4RYxWdmW4nAaLyVYpZJVh9XyyBnh | 9425258 | no | yes | no | no | | 2 | 809 | 5DXmFsx9qVT4aKXDH8fv8PKWbm4PasY3H3ee5tVbWKmQ7ymJ | 9460602 | no | yes | yes | no | | 3 | 865 | 5C4i3h9ys4Jm1s5JTNhe7f5crs74etBFbJFVDD95bmmKHBUk | 9467688 | no | yes | no | no | | 4 | 875 | 5C4hx5vjGZEcUf19LvJtBUnDotmx7u2b4HdpPbYFcraFMg4f | 9468889 | no | yes | no | no | | 5 | 877 | 5GJ9xiLa91Cf4TMnTS4SxytYh7RrBWp37DZWGYZGjnfiP3m9 | 9469206 | no | yes | yes | no | | 6 | 878 | 5FZdqYczFTiAG4R7M23C28VTdCg7YkcCoXmbVwi4aRibpCud | 9469300 | no | yes | yes | no | | 7 | 881 | 5CFRP3qHAPES4Tf5hWynqZ1wURvJGnbE4wDZUpHhJo12xh11 | 9469684 | no | yes | yes | no | | 8 | 923 | 5C4hvuvgo2WQ3NVhTqe7xtVkVt94MJwNFmLT4BZ1Y9S5EjfV | 9474708 | no | yes | no | no | | 9 | 935 | 5C4hvJzLQX9fYdH3o9TUE3wvcGkqqfDSK8RDk2JLPoo63R7k | 9476143 | no | yes | no | no | | 10 | 948 | 5CJos2367xRminUCvgj9rV9PtGWSRkzuK66HGqmpgyJ1Lcin | 9477694 | no | yes | no | no | | 11 | 981 | 5Ej7BQ2YDAxRnETVCwsqiqx5gbidAbrx418LAQrzgH1wTDJU | 9484475 | no | yes | no | no | | 12 | 247 | 5FEvYRu6Xd6CnXqJiSwE2eYDS4uGTCVq2fUA1a2bWtbCFmdh | 9495814 | no | yes | no | no | | 13 | 215 | 5E1WZRA5DPZGTT1o4inG8ucRh8T2wJzN96G7S8YKAPA6HWXQ | 9498570 | no | yes | no | no | | 14 | 32 | 5Fsv6ejCesES5jToKq2rQtAwn4GexhzHx2iGbXk9g1rSeFtu | 9498734 | no | yes | no | no | | 15 | 148 | 5D6LQqs2KDPMsjGZx3wqBMa8m9mWDsRM4XM7TXBnSH61GxFZ | 9498905 | no | yes | no | no | | 16 | 33 | 5DBu6VXfcUGiujQyr1wxn7jksMfWizC5tLLf9MkXVWQ9wHpY | 9498909 | no | yes | no | no | | 17 | 949 | 5EUmyxAfhBqjRsbHE8AKWf3JeBXGh2yPKaanCFvC1BvfvPZm | 9477842 | no | no | no | no | | 18 | 950 | 5Dg3eMaJKtAueRTPLu1xMAXCLqkDYxogH6bpStqExkzLH1kn | 9477962 | no | no | no | no | | 19 | 951 | 5GCKGsb6Ud8eG5k2ZwmugR5uJ6Adpe3qcwiFUrMXqBDB12oh | 9478082 | no | no | no | no | | 20 | 957 | 5FZkTisP5EzjVv4XcixaqrYgZ4jL4nhKpzg3bFWTtWcKN5Tv | 9478824 | no | no | no | no | | 21 | 958 | 5FP1z2juwLYiaAE8atMJYrTELLYvznmVMXcjJNG9gXM3h4p7 | 9478947 | no | no | no | no | | 22 | 960 | 5CrgNJEjkHBw6rGaCYMaK1sRKE31kyM96nm8hujPNtQb9p2B | 9479174 | no | no | no | no | | 23 | 961 | 5Di1gRom7dePpEe2DnaUnjomVfnvjuRnwDoRDuUEjGm3MiqU | 9479302 | no | no | no | no | | 24 | 962 | 5FdpwuBQZDXVNEHdDYisUBBqBewtB5geksAMzxPk7kLzDzud | 9479429 | no | no | no | no | | 25 | 964 | 5CgmiHwac29nJELK8tZjKBY8xRJUDnhJfzJsksWKXQwqQBMj | 9479671 | no | no | no | no | | 26 | 966 | 5Gnnu2qufvX164juDrkU3tDiueJJE4AWMEN5aKrYU6rfek2s | 9479896 | no | no | no | no | | 27 | 967 | 5HQMg9cFRzEmeyPhYHg4zx1fYKkkrgJjss3qmzamCTJ58xaR | 9480028 | no | no | no | no | | 28 | 968 | 5Dam2uVikGr3c2iAH74iEsoPdQWiKnAHoWDedbdRBZzAkquJ | 9480149 | no | no | no | no | | 29 | 969 | 5CEHhNeXpA1R3A1TSdbJ7gnzzhwpfimeff5XC8znkXRyCi3q | 9480270 | no | no | no | no | | 30 | 970 | 5Gey6zgriWNPKZ4CqoT6NMSm24N2YH36A8Ka7BVRerLue9JC | 9480390 | no | no | no | no | | 31 | 971 | 5CA6f8QeQexHSzpyfeGCAJyTAQ1Zdrk19PPPcqWJBtzLbFXS | 9480526 | no | no | no | no | | 32 | 972 | 5DNpHesevbQ6FnGBovQPT6zgtKbYxXR9quRZ1fc8P7xsUb5d | 9480939 | no | no | no | no | | 33 | 974 | 5D6diLtjedD7APLGeXLC7y9inyDRUw3YYNAEjUxED9XAor2y | 9481719 | no | no | no | no | | 34 | 975 | 5CJAxy1T9ewzpD2qLYs4oPf7wndsJkDQoZm2BDo3UN5pTRve | 9482193 | no | no | no | no | | 35 | 976 | 5GaouBCUJjdFVZHwwNm63p8TSXMByqW3L7LpRM7yubmQALzi | 9482628 | no | no | no | no | | 36 | 978 | 5C4mr7y7iCk7T6NwYyZtrgzeHZiXq5cpce5R8qH15dVoPKaY | 9483423 | no | no | no | no | | 37 | 979 | 5H6cKaSfTjWQAHUSWMWVCxcxjqGR5uu1Ka2TRXGZzfEXvfAi | 9483716 | no | no | no | no | | 38 | 982 | 5Fj4mQDNhUVo2Z5xgoJd9pmpiphUK6YYvUzFnXtpQPkuoGg9 | 9484962 | no | no | no | no | | 39 | 985 | 5G75GS8vZi2ZnYR8evHca2iEta4Wc3VQgYFu8mNJEr8Zf1Rj | 9486243 | no | no | no | no | | 40 | 986 | 5H2YEjby8LATSCnbfzjzQJ9To5AyAGC35t5UQZGxbxoDZdWM | 9486666 | no | no | no | no | | 41 | 988 | 5H4Y5xerAAE6G1ceKxsR5imCiSQ3vURhERBjj1bY6nZvcZFp | 9487423 | no | no | no | no | | 42 | 994 | 5E9i3oHHJdYgpbD7iCaHMmeonSJ5XjDC88kx2PVqPvCVYasX | 9489624 | no | no | no | no | | 43 | 995 | 5EokzeUDsjYsihRbttWKR8jg2Reia7V7Y9qAfW1m2UeAzMpK | 9489978 | no | no | no | no | | 44 | 998 | 5CKXor3qVzvsQ1cMS7HqmVWaz9Vgu4AoEtjwCak2ZNyEtPNz | 9491197 | no | no | no | no | | 45 | 999 | 5EcRin4mZusim2dZkBXD2hV3ahBxMTqg4Yx2f2R3cTLpQDos | 9491199 | no | no | no | no | | 46 | 250 | 5DFnHwavyQYSQ3GN2pUEbpphLQC6Mb2ZZVS53iBgXDNaakai | 9492020 | no | no | no | no | | 47 | 6 | 5H4f9f7RXCsCz8dSH6PbV2B1JC4srZHNp1geVDiVsj236hn9 | 9492443 | no | no | no | no | | 48 | 7 | 5HBAP7AcrEsHX6884sf8EbDQrNy2CBkjn2fdkkpvbmS5Qp9G | 9492822 | no | no | no | no | | 49 | 48 | 5CyXsCq9VKC6NFpkYdPueqPqVbD55ivupAKnmQXdCtddRCLY | 9494556 | no | no | no | no | - `file_valid`: the validator's verdict on the file that was stored for the hotkey at its last check of the files: 2026-11-14 10:20:09 UTC (`queue.files_checked_at` in mining.json). The validator reads the waiting files when it picks the entries of a round, and again and again while no round runs. It does not read them while a round runs: a file that you store then shows the old verdict until the round ends. The validator never publishes batch content or `train` values. To see the rule that a file breaks, run the `check` command of section 5: it prints the rule code and the reason. - A new process of the validator has no verdict until its first check of the files. A publication before that check has `queue.files_checked_at` null and `file_valid` no for every waiting seat, also for a good file. This can be so when a process starts while the chain gives no answer. It can also be so when a process starts, or closes a round, in the 300 s after a round with no result: in that time the validator does not read the files. No seat and no place in the queue changes by it: the validator picks the entries of a round from a new check of the files, never from the published value. So do not replace a good file because of it: wait for the next check. - Identity: a registered UID on this subnet (`btcli subnets register --netuid 120`). One registration (UID + registration block) buys one seat in one round, win or lose. A hotkey gets one turn. When its entry was charged, the hotkey never enters again, also with a new registration. A turn in v1 that the cutover imported counts too. To enter again, register a new hotkey. - Order: registration block, then UID. UID 0 has no seat. - A seat exists only while its registration is on chain. The validator reads the chain before each round. When the registration leaves the chain (the UID is deregistered, or the UID goes to a newer registration), the seat is removed and its turn is lost. A seat that is in an open round stays until that round closes. - When the hotkey of the UID changes and the registration stays, the seat keeps its place. The validator then reads the file of the new hotkey: a file that was stored for the old hotkey does not count. When the new hotkey already had its turn, the seat is removed. - One entry for each miner in a round. A second seat of the same hotkey or coldkey waits for a later round. - `no_decision`: our infrastructure failed (section 3). You are not charged and you go to the front of the queue. - A failure is counted for your seat in one case only: your batch had the whole training time of the round (it started at the lease, and the box started its work on the round at most 120 s after the start of the lease), the first try of its training did not finish in that time (section 3), and at least one other batch of the same round finished its training. Your entry then has the status `no_decision` and no scores. When 3 failures are counted for a seat, the seat is held: it takes no turn until the operator releases it. A release also sets the count to zero. A `no_decision` that comes from the scoring (section 6) has scores and is not counted. - A training job that failed is never counted for your seat: out of memory on the box, a crash of the job, any other error of the training. The box tries the training again (section 3). If each try fails, your entry has the status `no_decision` and no scores, and its `reason` starts with `training failed`. You are not charged and you go to the front of the queue. - A round with no result: the box failed, not a seat. This is the case when the training failed for at least one batch and finished for no batch, when the box stopped with an error, when its result did not arrive before the end of the lease, or when the operator ended the round. Such a round costs you nothing: nobody is charged, no failure is counted for a seat, and each seat of the round goes to the front of the queue. The next round starts 300 s after the failure, or later. In the round file (section 9) you see it thus: `outcome` is `no_decision`, no entry has a `status`, and `note` gives the cause. In `history` of mining.json the round has the same `outcome` and its entries have no `status`; the `note` is only in the round file. - A round that has a result also gets the outcome `no_decision` when none of its entries was charged. Each entry then has a `status`, and the status of your entry says what applies to your seat. - `not_trained`: the training time of the round ended before your batch was trained (section 3). You are not charged and you go to the front of the queue. It is not counted as a failure, so it never holds a seat. - `not_scored`: the time limit of the round came before the first look of the scoring (section 3). You are not charged and you go to the front of the queue. It is not counted as a failure, so it never holds a seat. - `skip`: the trainer could not train the copy of your file, for example because the training gave a number that is not finite. You are not charged and you keep your place. - A round with the outcome `abandoned`: a winner was named, but its files could not be checked or applied. The head does not change, no entry of the round is charged, and a seat that was scored goes to the front of the queue. ### Your place while you wait - A seat that has a valid file stored keeps its place and is never moved back. While your entry is not evaluated, you may replace your file as often as you like (section 5): a new valid file does not change your place. - If your file is missing or invalid when the validator picks the entries of a round, your seat is skipped. You are not charged, and you keep your place, with one exception: squat-trim. - Squat-trim moves a seat to the back of the queue. It applies only to a seat that has no valid file stored when the validator picks the entries of a round, and only when the registration of that seat is older than 2 x its expected wait. Expected wait = max(365 days, seats ahead x seconds per turn). Seconds per turn = 86400 / the number of entries that were charged in the last 24 hours. - So a seat with no file or with an invalid file keeps its place for at least 730 days after its registration, and for as long as no entry was charged in the last 24 hours. The two values (`squat_min_wait_s`, `squat_age_factor`) are parameters (section 11). The validator can change them. This text always shows the values that it uses. - A seat at the back still gets its turn: a round takes it when it has room after the front seats and the normal seats that have a valid file. The seat stays at the back when a valid file arrives later. - A seat that v1 had moved to the back of its queue starts at the back here. - The operator can send a waiting seat to the front, between rounds only. The action is for a seat that had a turn outside v2 that was not charged. Such a seat is then no longer at the back. Nothing is charged or counted for it. - In the table: `front` = yes: the seat goes before the normal seats (it had a turn that was not charged). `demoted` = yes: the seat is at the back, unless `front` is also yes. `held` = yes: the seat takes no turn until the operator releases it. `position` counts every seat ahead of you, also a seat with no valid file, which a round skips. ## 5. Your file You have one private object for each hotkey. A hotkey whose queue entry is not evaluated yet keeps its queue position, and may replace its file as often as it likes until the entry is evaluated. There is no fixed window. The validator copies the latest file when a round takes the entry: that copy is what is trained. The admission checks run again on each new file. The only rule that moves a waiting seat back is squat-trim (section 4): it applies to a seat with no valid file stored, never to a seat with a valid file. The file stays private unless it wins. Then the exact file is published (section 9). ```json { "version": "affine-batch-v2", "hotkey": "", "train": { "lora_lr": 1e-05 }, "groups": [ { "prompt": "", "rollouts": [ { "output": " \\boxed{}", "reward": 1.0 }, { "output": " \\boxed{}", "reward": 0.0 } ] } ] } ``` - UTF-8 JSON, at most 67108864 bytes. Top keys: `version`, `hotkey`, `groups`, and the optional `train`. `version` is one of affine-batch-v1, affine-batch-v2, affine-batch-v3. `hotkey` must be the owner of the object. - `groups`: a non-empty list of {prompt, rollouts}, with no other key. Each prompt is text with more than white space in it, and differs from every other prompt. Each prompt has at least 1 rollout: {output, reward}, with no other key. `output` is text. - <|im_start|>, <|im_end|>, <|endoftext|> may not appear in a prompt or an output. - Caps: 250 prompts and 2000 rollouts. Whole groups are kept in file order while both caps hold. The other groups are dropped. All rules below apply to the groups that are kept. - Tokens: an output has at most 65536 tokens. Prompt + output has at most 69632 tokens: an output below the output cap counts 1 token more there, its end-of-sequence token. The prompt is counted as one user message in the chat template of the head. Training reads at most 32768 output tokens and 36864 prompt + output tokens of a rollout. A longer rollout is valid: the trainer truncates it (section 2). - Reasoning: the reasoning of an output is the text before its first `\boxed`, or the whole output when it has none. A rollout with fewer than 32 reasoning tokens is answer-only. At most 0.05 of the rollouts may be answer-only. At least 0.5 of all output tokens must be reasoning. - Repeats: the final answer is the content of the last `\boxed{}`, with all white space removed. One final answer may be in at most max(32, 0.1 x rollouts) rollouts. - `reward`: a finite number. The rewards of one prompt must be small enough to take their mean. A reward must equal the grade that the official grader gives to (prompt, output). The allowed difference is 0. One wrong reward makes the whole file invalid. An output of exactly 65536 tokens stopped at the cap: its official grade is 0. At least 0 of the prompts must be tasks of the public pool (0 = no such rule). A prompt that is not a task of the pool has no official grade: its rewards are not compared with anything. - A rollout that the official grader cannot grade inside its time limit is not allowed in a file, whatever its reward: rule code `grader_time_limit`. - `no_signal`: your file must train something. A rollout trains when its advantage is not 0 and it has at least 1 output token inside the training caps. The advantage is the reward minus the mean reward of its group (`group_size`, section 2). A file in which no rollout trains is invalid. One rollout per prompt never trains. So the example above has two rollouts with different rewards. - `train_cost`: cost = steps x the tokens of the rollouts that train. The tokens of one rollout are its prompt tokens plus its output tokens. room = min(32768, 36864 - prompt tokens). An output shorter than the room counts its tokens + 1 (the end-of-turn token). An output as long as the room or longer counts the room. A rollout with advantage 0 counts 0. A file with a cost above 8000000 token-steps is invalid. To get below the cap: fewer steps, fewer rollouts, or shorter outputs. The rule needs token counts, so `check` runs it only with `--tokenizer`. - Two limits keep the training of a round short: the training time of the round (40 minutes for all batches, section 3) and this cost cap for one file (8000000 token-steps). - Nothing is repaired or clamped. The first rule that fails makes the whole file invalid. - Check a file before you upload it, with the same code as the validator: `python v2/miner/affine_miner.py check --mining-json mining.json --hotkey batch.json`. Add `--tokenizer ` for the token rules and `--grade` for the reward rule. `--grade` needs `--tokenizer`: only a token count can tell that an output stopped at the cap. Install, from a copy of the code (section 2): `pip install -e "v2[miner,envs]"`. ### Rule codes A check stops at the first rule that a file breaks. The `check` command names that rule. The validator publishes only `file_valid` (section 4), not the rule. The rules run in this order: | rule code | the file is invalid when | | --- | --- | | `size` | the file is empty or larger than the byte cap | | `json` | the file is not UTF-8 JSON | | `nonfinite` | the file holds NaN or Infinity | | `top_keys` | the top level is not an object with exactly version, hotkey, groups and (v2, v3) the optional train | | `version` | version is not a known format | | `train` | train is not an object, has a key that is not settable, or has a value outside its published bound | | `hotkey` | hotkey is not the owner of the stored object | | `groups` | groups is not a non-empty list | | `group_keys` | a group does not have exactly the keys prompt, rollouts | | `prompt` | a prompt is not non-empty text | | `duplicate_prompt` | two groups have the same prompt | | `rollouts` | rollouts is not a non-empty list | | `rollout_keys` | a rollout does not have exactly the keys output, reward | | `output` | an output is not text | | `reward` | a reward is not a finite number, or the rewards of one prompt are too large to take their mean | | `cap` | the first group alone is larger than the rollout cap | | `text` | a prompt or an output is not valid Unicode text | | `control_token` | a prompt or an output holds a chat control token | | `tokenizer_fault` | a prompt or an output makes the tokenizer fail; checked again at a try to open a round, at most once each 10 minutes; after 3 failed checks in a row the verdict stays until a new file is uploaded | | `output_tokens` | an output is longer than the output token cap | | `context_tokens` | prompt + output is longer than the context token cap | | `answer_only` | too many rollouts have almost no reasoning before the first boxed answer | | `reasoning_share` | too small a share of the output tokens is reasoning | | `answer_repeat` | one final answer is in too many rollouts | | `no_signal` | no rollout has a reward that differs from the mean reward of its training group, so nothing trains | | `train_cost` | steps times the tokens that the trainer runs through the model is above the training cost cap | | `grader_time_limit` | the official grader cannot grade a rollout inside its time limit; checked again at a try to open a round, at most once each 10 minutes; after 3 failed checks in a row the verdict stays until a new file is uploaded | | `grader_fault` | a rollout makes the official grader fail; checked again at a try to open a round, at most once each 10 minutes; after 3 failed checks in a row the verdict stays until a new file is uploaded | | `grader` | a reward is not the official grade | | `pool` | too small a share of the prompts are tasks of the public pool | | `check_fault` | the file makes the checker itself fail, at any step; checked again at a try to open a round, at most once each 10 minutes; after 3 failed checks in a row the verdict stays until a new file is uploaded | Not a final verdict: `grader_time_limit`, `grader_fault`, `tokenizer_fault`, `check_fault`. Such a code comes from one check on our machine at one moment, so the validator checks the file again when it tries to open a round, at most once each 10 minutes. A failed check is counted only when our checker passes its own known-good case at that moment. After 3 counted checks in a row that fail at the same place of the same file, the verdict stays until you upload a new file. A new file is checked at the next try, with no wait. With such a code, before and after that, a round does not take the file: you are not charged, and the seat is skipped as with any invalid file (section 4). The validator does not publish the code or the count: you see only `file_valid`. Every other code is final for those bytes: upload a new file. ### How to make rollouts A rollout is one sampled answer of a model to a prompt, with its reward. This is one way to make a file that is valid and that trains. Each step says what the code of the validator does with your choice. To see what the file does before you upload it, train and score it on your machine (section 10). - Model: sample from head M. Get its files as section 1 says. The trainer scores your tokens under head M, and a token outside the support of head M does not train (section 2). - Prompt: the trainer reads `prompt` as one user message (section 2). For a task of the pool, `prompt` is the problem text, or the instruction of section 6, one blank line and the problem text. Both are the same task for the grader, and two different prompts for the model. For your sampler, render the prompt as the trainer does: one user message, the chat template of the head, the generation prompt, and no other argument to the template. - Scoring renders a task in another way than training (section 6): the instruction is the system message, the problem is the user message, and the template gets `enable_thinking` true. So the model does not read the same tokens before its answer in training and in scoring. - Sampling: use the temperature that you state in `train.sample_temperature` (default 1), top_k 20 and top_p 0.95. These are the settings of the support (section 2). The sampler must work as the support does: top_k first, then top_p over the tokens that are left. Then each token that you sample is in the support of head M on your machine. On the box the logits come from other hardware, so a token at the edge of the support can be outside it there; no run has measured how often. No rule checks where an output comes from: text of another model or of other settings is valid, and only its tokens in the support of head M train. - Output: `output` is the text that the model wrote after the generation prompt: its tokens decoded with the special tokens kept (`skip_special_tokens` false), so that `` and `` are in the text, and without the final `<|im_end|>`. The trainer encodes this text again and adds the end-of-turn token itself (section 2). It trains on the tokens of that encoding, which are the tokens that you sampled only if the text encodes back to them. - Stop: let the model end by itself, or stop it at 65536 tokens (`max_output_tokens`). An output of exactly that many tokens counts as stopped at the cap, and a longer output is invalid. - Reward of a pool task: the official grade, 1.0 for a pass and 0.0 for every other result. Take it from the grader itself, `sn120_v2.envs.math_grader.grade(task, output, capped).reward`, with `capped` true for an output that stopped at the cap. Or let the `check` command compare your rewards (`--tokenizer` and `--grade`, above). The grader reads the same `output` text. - Signal: give a prompt two rollouts or more, with different rewards. The advantage is the reward minus the mean reward of the group, so a group in which all rewards are equal trains nothing. - Length: the trainer reads the room of section 2 and no more. It cuts a longer output there and takes it as a rollout that did not end; with a negative advantage the derail rule then applies to it. The cost rule above counts the same tokens. - Before each upload, run the `check` command with `--tokenizer` and `--grade`. It runs the check of the validator. It does not train. ### How to upload You upload with a sealed link: a public file that only the secret key of your hotkey can open. It holds a time-limited upload URL for your object. - Activation (one time): your hotkey must be an Ed25519 key. Run `python -m sn120_v2.links activate --wallet --hotkey ` to see the activation. Add `--execute` to write it to the chain. It costs the fee of one commitment. The chain shows it after about 3 minutes. An activation made for v1 stays valid: do not make it again. Install: `pip install -e "v2[links,chain]"`. - Where to get the link: https://affine.io/ff/links/.json. The file is public, but only the secret key of your hotkey opens it. A new file is written every 30 minutes. Each link works for 24 hours. - You have a link while your hotkey is registered, has the activation, and its queue entry is not evaluated yet. You may upload again as often as you like. Each upload replaces the file before. The file that is stored when a round takes your entry is the file that is trained. - Open the link: `export AFFINE_PUT_URL="$(python -m sn120_v2.links open --hotkey --key --links-url https://affine.io/ff/links)"`. The seed file holds the 32-byte Ed25519 seed of your hotkey as 64 hex characters. A bittensor hotkey file that is not encrypted also works. Install: `pip install -e "v2[links]"`. - Upload: `python v2/miner/affine_miner.py upload --mining-json mining.json --hotkey --tokenizer --grade batch.json` checks the file, reads the URL from the environment variable `AFFINE_PUT_URL` and sends the file. Or use any HTTP tool: an HTTP PUT of the file bytes to that URL with the header `Content-Type: application/octet-stream`. The link is signed for that header: without it the store refuses the upload. - The opened URL is a secret. Anyone who has it can replace your file until the link expires. - HTTP 404 on your link file means one of three things: the hotkey is not registered, it has no activation, or its entry was evaluated. After a new registration or activation, wait for the next link file. A hotkey whose entry was evaluated gets no link again (section 4). - After the upload, read `file_valid` in section 4. ## 6. Judging - All tasks are public. There is no hidden task set. - Task pool: `sn120_v2/envs/math_pool_v2.jsonl` in the code (sha256 `8d34d276bd777c2b48506687ee902041e40f68073ce71cf60f79b27471d06ebc`). Scoring sends this instruction as the system message and the problem as the user message: "Solve the math problem. Reason step by step in plain text, then end your response with the final answer in `\boxed{}`. Emit exactly one `\boxed{}` block and nothing after it." - A prompt of your file is a pool task when it is the problem text, with or without that instruction and one blank line before it. The official grader is `sn120_v2.envs.math_grader.grade(task, output, capped)`. It grades model outputs in scoring and the rewards in your file. - Draw: 2000 tasks from the public pool of `math`, 1 sample(s) per task. The draw is a fixed function of the public sampler snapshot and the public round seed. Both are frozen at round start. Rebuild a draw with `sn120_v2.irt.draw_from_snapshot(snapshot, round_seed, n_tasks)`. - The IRT sampler favours tasks where the predicted pass rate of the head is from 0.2 to 0.8, and keeps 0.1 of the draw for new tasks. - Pairing: the null copy of M and every LoRA run the same tasks with the same seeds. The seed of rollout j on a task is the first 63 bits of sha256("||"), as a whole number; j counts from 0. - Decode: temperature 1, top_p 0.95, top_k 20, bfloat16, at most 65536 output tokens and 69632 prompt + output tokens. - Outcome of a rollout: `P` pass, `F` fail, `C` hit the token cap (not passed), `E` our infrastructure failed (not passed, stays in the count). - Estimator: d_t = LoRA pass rate - null M pass rate on task t. delta = sum(w_t * d_t) / sum(w_t), with the published sampling weights w_t. se = sqrt(n / (n - 1) * sum((w_t * (d_t - delta))^2)) / sum(w_t). z = delta / se. - Win rule: z is strictly above 1 (beat M by more than that many standard errors), on at least 100 tasks, with no correction for the number of LoRAs. The cap-hit rate of the LoRA may not be above max(0.33, null M cap-hit rate + 0.02). - The eligible LoRA with the highest delta wins. A tie goes to the miner that was earlier in the queue. With no eligible LoRA, M stays and there is no king. There is no second confirmation round. - Looks: scores are checked after 500, 1000, 2000 tasks. The win rule runs only at the last look. An early look can only drop a LoRA that can no longer reach the win bar: after a share t of the tasks, it is dropped when (z + c) / sqrt(t) <= 1. c comes from a Pocock-type alpha-spending function with total level 0.05. For this draw, a LoRA is dropped when z is at or below: -1.600 after 500 tasks, -1.515 after 1000 tasks. - When the time limit of the round stops the scoring, the win rule runs on the tasks scored by then, with no further look. With fewer tasks than the first look, there is no decision: `not_scored` (section 3). - If fewer than 0.95 of the rollouts of a LoRA resolve without an infrastructure error, its entry is `no_decision`. If that is so for null M, every scored entry of the round is `no_decision`. Neither is counted as a failure for the seat (section 4). - After 1 applied winner(s), the stacked LoRAs are folded into a new base. ### The scoring procedure This is everything that the box does to score a round, in the order in which it does it. The code is in four files: `sn120_v2/worker.py` (chunks, looks and the clock), `sn120_v2/gpu/evaluator.py` (servers, requests and outcome codes), `sn120_v2/envs/math_grader.py` (the grader) and `sn120_v2/judging.py` (the estimator and the rules). Section 10 has the command that runs this code on your machine. - Servers: when all training is done, the box starts one vLLM server on each of its 8 GPUs. Each server holds the base of head M under the model name `null`. The box loads each LoRA of the round into every server with `/v1/load_lora_adapter`, with its `lora_hash` as the model name. The servers run with the variable `VLLM_ALLOW_RUNTIME_LORA_UPDATING` set (vLLM needs it for that route) and with `HF_HUB_OFFLINE` set. The vLLM version is the one that the `gpu` extra of `v2/pyproject.toml` names. `--max-loras` is the number of batches of the round, trained or not: it is the same for every chunk of the round. The command of one server, without its local address: ``` python -m vllm.entrypoints.openai.api_server \ --model '' \ --served-model-name null \ --dtype bfloat16 \ --max-model-len 69632 \ --enable-prefix-caching \ --enable-lora \ --max-num-seqs 256 \ --max-loras '' \ --max-lora-rank 64 \ --limit-mm-per-prompt '{"image":0,"video":0}' ``` - Request: one POST to `/v1/chat/completions` for each model, each sample and each task of a chunk. The body is the same for null M and for a LoRA, but for `model`: ```json { "model": "", "messages": [ { "role": "system", "content": "" }, { "role": "user", "content": "" } ], "seed": "", "max_tokens": 65536, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "skip_special_tokens": false, "chat_template_kwargs": { "enable_thinking": true } } ``` - `model` is `null` for null M and the `lora_hash` for a LoRA. The seed is the seed of the pairing rule above. Task i of a chunk (i counts from 0) goes to server i mod 8, for every model. So a LoRA and null M differ by the adapter alone. - The box sends all requests of a chunk at once. A server runs at most `--max-num-seqs` of them at a time; the others wait in the server. The time limit of one try of a request is 3600 s (`eval_time_budget_s`), or the time that is left to the stop time of the round, when that is less. - Reply: with `finish_reason` `stop` the model ended by itself. A final `<|im_end|>` is removed from the text, and the grader grades the text. With `finish_reason` `length` the model hit the token cap: the outcome is `C` and the text is never graded. A reply of any other form is not usable. - A request is sent again after a failure of the connection, a time limit, an HTTP status of 500 or above, or a reply that is not usable: at most 3 tries in all. After an HTTP status below 500 it is not sent again. A request with no usable reply has the outcome `E`. One exception: when the stop time passed during the chunk and a request has no reply, the whole chunk is not used (section 3). - Grader: `sn120_v2.envs.math_grader.grade(task, output, capped)`, version `math-verify-0.9.0+lenient-v1`. `output` is the text of the reply, with the special tokens kept. The strict format is: one `...` block first, then exactly one balanced, non-empty `\boxed{}` with nothing after it. math-verify compares the boxed answer with the reference answer of the task. When the answer is not yet right, the lenient rules try again: P1 and P2 find a box that the strict format refused, and E1 to E7 accept other common ways to write the same answer. That file states each rule and the version of each package that the grader needs; with another version the grader refuses to run. The grade is `P` or `F`, or `E` when the grader itself cannot decide: it cannot parse the reference, math-verify raises an error, or a time limit of the grader passes. Such an `E` is final: the text is not graded again. The grader does not know which model wrote a text. - A grade that is lost because a worker process of the grader died is not an outcome. The reply is graded again: at most 3 grades in all. When a worker still dies, the chunk failed. - Chunks: the tasks run in the order of the draw, in chunks of 500 tasks (`eval_chunk_tasks`). A chunk also ends where a look ends. Null M and each open LoRA answer every task of a chunk. The box reads the clock before each chunk (section 3). - A failure of the box in a chunk (a server died, a LoRA did not load, the grader failed) is not an outcome. The chunk runs again: at most 3 runs in all. When each of them fails before the stop time, the box stops with an error and the round has no result (section 4). A run that fails after the stop time ends the scoring, and the chunk is not used. - Outcome strings: each model has one string for each sample. A string has one character for each task, in the order of the draw: `P`, `F`, `C`, `E`, or `-` for a task that was not run. - Estimator, in full: n is the number of tasks that the LoRA answered. They are the first n tasks of the draw. The pass rate of a model on a task is the share of its samples with `P`. The sums of the estimator above run over these n tasks, with the weights w_t of the draw. When se is 0, z is 0. - Drop rule, in full: after each look but the last, the box computes z of each open LoRA on the tasks so far. Look k has scored the share t_k of the draw. a_k = spent(t_k) - spent(t_(k-1)), with spent(t) = `drop_alpha` * ln(1 + (e - 1) * t), and spent = 0 before the first look. c_k is the normal quantile of 1 - a_k. The LoRA is dropped when (z + c_k) / sqrt(t_k) <= `z_bar`. No LoRA is dropped at a look with fewer than 100 tasks (`min_scored_tasks`), or when its se is 0. A dropped LoRA answers no later chunk. When no LoRA is open, the scoring ends. - Win rule, in full. A LoRA is eligible when all of these are true. It has an outcome for every task of the draw, or, when the stop time ended the scoring, for every task that null M has an outcome for; a LoRA that was dropped at a look does not. n is at least `min_scored_tasks`. z > `z_bar`. (its count of `C`) / rollouts <= max(`cap_hit_floor`, (the count of `C` of null M on the same n tasks) / rollouts + `cap_hit_margin`), where rollouts = n x `samples_per_task`. This last test uses exact fractions of whole counts, and each parameter as the decimal number that is written here: a share exactly on the limit passes. - Cover, in full: (rollouts - the count of `E`) / rollouts >= `min_cover`, as exact fractions, for null M on the tasks that it answered and for a LoRA on its n tasks. - Winner: the eligible LoRAs are read in queue order. A later LoRA takes the place of the best one so far only when its delta is higher by more than 1e-12. - The box does not pick the winner. The controller computes every score again from the outcome strings (`sn120_v2.judging.decide`). The round file (section 9) holds the draw with its weights (`draw`), the outcome strings of null M (`null_outcomes`) and those of the winner (`winner_outcomes`). With `sn120_v2.judging.score_entry` you can compute the scores of the winner again. The outcome strings of the other entries are not published. - What a scoring of your own repeats: the draw and its weights, the seed and the body of each request, the grade of a given text (but for a time limit of the grader), and each formula above. It does not repeat the text of a rollout: the same request with the same seed can give another text on another machine or at another time, and no run has measured how often. So your outcome strings and your delta can differ from those of the box. The tests of the code start no vLLM server: they put a stand-in in its place. The box runs vLLM on GPUs. ## 7. Payout - A king is a round winner that was applied. The window is the last 10 distinct king UIDs. A UID that wins again has one place in the window, with its newest win. - Each king of the window gets an equal share of all the weight, while the hotkey that won is still registered on that UID: 1 / (the number of such kings). - The share of a king that is no longer registered is split among the other registered kings of the window. - A round with no winner changes nothing in the payout: the window of the last 10 kings keeps being paid. - No burn, in any case: no weight goes to UID 0 or to the owner as a fallback. If no king of the window is registered, the writer sets nothing and the last valid weights stay on chain. - One writer sets the weights, with one attempt each 3600 s. If the chain read fails or is not complete, or the state does not load, that attempt sets nothing and the last valid weights stay on chain. | uid | hotkey | round_id | head_version_id | applied_at | z | | --- | --- | --- | --- | --- | --- | | 876 | 5C4husuG2RYz8nk5hjgRPpjcu2WwQcs8RZaHVUKhzLvse82B | r000006 | 6ae7347b8b8bf8f6a6c78bfde5378c2e8303b3f8f0e8121a9e31a42f30fcadba | 1794643342 | 2.46148 | | 929 | 5DoFVUeQF59xiKs44a1oz6J2JAu2q1kEXhHSdufFSgzWzJrM | r000005 | 100d795890a0bb06dcc4ac84693c059d485ae2450102103fa32804917d0b6b38 | 1794634676 | 3.72051 | | 920 | 5FaTFWogz3AiQbEFRnL9CZcqR1keeypEYkd7f1L5YLbWH1cP | v1-e1211 | none | 1794567737 | 1.10154 | | 918 | 5GqhL9AVkreTqyVfYgUfdC35QXg7CkvXmVYbupuNs3DkkTuA | v1-e1174 | none | 1794551890 | 1.35377 | | 852 | 5CaxZGTVu76Duv7WjkEA7BSmQfDawArecSu16CeKPDnqaFNG | v1-e1170 | none | 1794551579 | 2.03383 | | 822 | 5HNZbZKj76zn7EWgf4ps4vNjZfbNXqyam4vz3GrhXzrtpMbC | v1-e1169 | none | 1794551505 | 2.02911 | | 909 | 5DJhEGovRZTfk9tXhKynHwYeFNz1qCAkqctVfVQbbFLMH5fZ | v1-e1167 | none | 1794551349 | 2.07759 | | 851 | 5CvpFY3UxcKjA5Rrkqj7m7rrKaDXXSKimXJ9n6CYRE9TbyTd | v1-e1166 | none | 1794551275 | 4.52777 | | 821 | 5GzPwdzht3CTaBUYyaqKWn8WZRxixV99KuYHPd32MQiEwdDc | v1-e1165 | none | 1794551206 | 4.27147 | | 942 | 5EiKdmHLD6WuJT9RfsXYf7HD1Vpm626z5Tog5PeFE6XyiTJR | v1-e1162 | none | 1794538373 | 3.2674 | ## 8. The IRT snapshot The task table of the sampler is public for every round. It is one JSON object for each snapshot id: `public/irt/SNAPSHOT_ID.json`. mining.json names the id (`irt_snapshot_id`); it does not hold the table. - Snapshot of the open round: id `irt-a8cdba8348286daf48862a548858770ca5ea96afe1ab61d846889c8cfca7c9a1`, object `public/irt/irt-a8cdba8348286daf48862a548858770ca5ea96afe1ab61d846889c8cfca7c9a1.json`, https://affine.io/public/irt/irt-a8cdba8348286daf48862a548858770ca5ea96afe1ab61d846889c8cfca7c9a1.json - Snapshot of the last closed round: id `irt-a8cdba8348286daf48862a548858770ca5ea96afe1ab61d846889c8cfca7c9a1`, object `public/irt/irt-a8cdba8348286daf48862a548858770ca5ea96afe1ab61d846889c8cfca7c9a1.json`, https://affine.io/public/irt/irt-a8cdba8348286daf48862a548858770ca5ea96afe1ab61d846889c8cfca7c9a1.json - Model: rasch: P(pass) = 1 / (1 + exp(difficulty - ability)). The object holds the ability of each head and LoRA, and for each task: difficulty, information, rounds_seen, sampling_mass. - Draw rule: pi(task) = min(1, n_tasks * mass / total mass), fixed size n_tasks, systematic sampling on the pool ordered by sha256('|irt-order|'), start = sha256('|irt-start') mod total mass; published weight = 1 / pi; evaluation order by sha256('|draw|'). - A round names its snapshot (`irt_snapshot_id`) and its seed (`round_seed`) in section 3, in `history` and in its round file. With the snapshot object and the round seed you can rebuild the draw of the round. ## 9. Objects and how to verify them | object | key | | --- | --- | | llms_txt | public/llms.txt | | mining_json | public/mining.json | | current_head | public/head.json | | head_manifest | public/heads/VERSION_ID.json | | base_file | public/bases/BASE_HASH/FILE | | master_file | public/masters/MASTER_DIGEST/FILE | | lora_file | public/loras/LORA_HASH/FILE | | round_file | public/rounds/ROUND_ID/round.json | | winner_batch | public/rounds/ROUND_ID/batch.json | | irt_snapshot | public/irt/SNAPSHOT_ID.json | - A file set (base, fp32 master, LoRA) has the id sha256(canonical JSON of {file name: sha256}). Check each file hash, then the id. The fp32 master has its own prefix: do not put it in the directory of the base. - `version_id` of a head is the sha256 of the canonical JSON of the head object without `version_id`. - Only a winner has a public batch, and with it public `train` values. For every other entry the round file shows the decision, the batch sha256 and the scores only. - In a round file each entry has a `reason`. The round has a `note`, and a `reason` when it has a result. The `note` is empty unless the round had no result or has the outcome `abandoned` (section 4). These texts give a cause in words, for a person to read: the class of an error and its message. Before the validator publishes such a text, a marker replaces each part that it takes for private (`[removed-secret]`, `[removed-login]`, `[removed-query]` for the query string of a link, `[removed-address]` for an IP address), and a text of more than 600 characters is cut there and ends with a note of how many characters were cut. The same rule applies to the `reason` of a round in mining.json and in section 3. To decide what applies to your seat, read `outcome` and `status`, not the words of a text. - The LoRA of a winner has a download link only while it is stacked on the head (section 1). After the fold (section 6) it is part of the new base, and there is no link for the LoRA alone. ## 10. Train and score your batch at home `python -m sn120_v2.replicate` takes your file, checks it as the validator does, trains the LoRA from head M, scores the LoRA and null M on the tasks of a round, and judges the result with the win rule. It has no training code and no scoring code of its own: it runs the code of the box (the procedures of section 2 and of section 6) on your GPUs. So the GPU backend is published: it is the code under `sn120_v2/gpu/`, and this tool builds it for a machine that is not the box. The document of the tool is `v2/REPLICATE.md` in the code: https://github.com/AffineFoundation/affine/blob/v2/v2/REPLICATE.md. It has every argument, the fields of the report, and the complete list of what differs from the box. - Install, in `v2/` of a copy of the code (section 2): `uv pip install -e ".[gpu,envs,miner]"`. The document has the complete steps. Run each command below in `v2/`. N is the number of your GPUs. - `python -m sn120_v2.replicate compare --mining-json https://affine.io/mining.json --gpus ` prints what differs between your machine and the box. It starts nothing and needs no GPU. - `python -m sn120_v2.replicate fetch --mining-json https://affine.io/mining.json --work ` gets the base files of head M through the signed links of section 1 and checks each file against the head manifest. - `python -m sn120_v2.replicate run --mining-json https://affine.io/mining.json --hotkey --work --gpus batch.json` checks the file, trains, scores and judges. It prints one report. With no other argument it takes the seed and the draw of the open round, or of the last closed round when none is open. - What differs from the box. The report of `run` names each difference under `differences`. Your install: the list of the packages of the box, with their versions, is not published, so in practice your install has another digest than the pinned one, and its versions of torch and vLLM can differ. The number of GPUs, when it is not 8. The time limits of a round: they do not apply unless you ask for them. The round has one batch, yours: `winner` in the report says that your LoRA beats null M by the win rule, not that it wins a real round, where the eligible LoRA with the highest delta wins. For such a difference the tool changes that one value in a local copy of the params. Each training value, each scoring value and each rule of the decision stays that of mining.json. - What a run cannot repeat exactly: the text of a rollout, and so its outcome code, and so `delta` and `z`; and the trained values of the LoRA, and so `lora_hash`. These depend on the GPU type, on the package versions and on the requests that a server computes together. Expect the same pass rate within the noise of sampling, not the same text for the same seed. What a run repeats exactly: the draw with its order and its weights, each seed, the prompt and the settings of each request, the rows that train with their advantages and tokens, the grade of a given text, and the decision for given outcome strings. - What is verified. The box trains and scores its rounds on GPUs with the code that `run` calls. The tool itself has not run on a real GPU yet. Tests run it on a CPU, with a stand-in for the training process, for the vLLM servers and for the grader. So it is not known which GPU is large enough, how long a run takes, or how close null M on your machine is to null M on the box. With `--round-file` and the round file of a closed round (section 9), the report compares the two, task by task. - The miner tool can call the same code: `python v2/miner/affine_miner.py reproduce --mining-json mining.json --hotkey --irt-snapshot irt.json --round-seed --work --backend sn120_v2.replicate:backend batch.json`. This is the code of the box with the params as they are: the same trainer, evaluator, round runner and judging. It runs only on a machine with the pinned install, the pinned code and the GPU count of the params, so in practice only on the box. It needs the base files: run `fetch` with the same `--work` directory first. On any other machine use `python -m sn120_v2.replicate run`. - `python v2/miner/affine_miner.py bounds --mining-json mining.json` prints the bounds of section 2. ## 11. Every parameter Each value is pinned into a round when the round starts. | parameter | value | | --- | --- | | `netuid` | 120 | | `env` | math | | `env_pin` | none | | `batch_cap` | 6 | | `max_loras_per_box` | 32 | | `n_min_start` | 1 | | `n_gpus` | 8 | | `n_tasks` | 2000 | | `eval_time_budget_s` | 3600 | | `samples_per_task` | 1 | | `looks` | 0.25, 0.5, 1 | | `drop_alpha` | 0.05 | | `max_output_tokens` | 65536 | | `max_context_tokens` | 69632 | | `temperature` | 1 | | `top_p` | 0.95 | | `top_k` | 20 | | `dtype` | bfloat16 | | `irt_middle_band` | 0.2, 0.8 | | `irt_explore_frac` | 0.1 | | `z_bar` | 1 | | `cap_hit_floor` | 0.33 | | `cap_hit_margin` | 0.02 | | `min_scored_tasks` | 100 | | `min_cover` | 0.95 | | `calibration_tol` | 0.003 | | `fold_every_k` | 1 | | `max_batch_bytes` | 67108864 | | `max_prompts` | 250 | | `max_rollouts` | 2000 | | `min_reasoning_tokens` | 32 | | `reasoning_token_share` | 0.5 | | `max_answer_only_frac` | 0.05 | | `max_answer_repeat_frac` | 0.1 | | `max_answer_repeat_count` | 32 | | `reward_tolerance` | 0 | | `min_pool_prompt_frac` | 0 | | `max_train_token_steps` | 8000000 | | `round_time_limit_s` | 6000 | | `train_time_budget_s` | 2400 | | `promote_reserve_s` | 600 | | `eval_chunk_tasks` | 500 | | `lease_timeout_s` | 7200 | | `max_infra_attempts` | 3 | | `squat_min_wait_s` | 31536000 | | `squat_age_factor` | 2 | | `apply_retry_s` | 300 | | `apply_max_attempts` | 12 | | `kings_window` | 10 | | `burn_uid` | 0 | | `weights_cadence_s` | 3600 | | `weights_rate_limit_blocks` | 100 | | `budget_usd_per_hour` | 200 | - `eval_time_budget_s`: the longest time that the box waits for one answer of a model in scoring. It is not the time of the scoring: the times of a round are in section 3. - `burn_uid`: the UID of the owner. It has no seat (section 4) and gets no weight: nothing is burned (section 7).