Training a small model to run a house
I trained Qwen3-1.7B, an open model small enough to run on a consumer GPU, to control a house through Home Assistant. On the Home Assistant community's voice-command test it went from 75.0% to 89.8%.
The method is reinforcement learning with GRPO. The model never sees a correct answer. It tries each command eight times, a real Home Assistant checks which tries left the house right, and the model is nudged toward those. Training took seven hours on one RTX 3060, and the result is published as hua-1.7b.
The rest of this post explains how it works, step by step, down to the math of a single training step.
01 The tasks
403
tests across 85 pretend homes, 4,318 sentences in all.
The Home Assistant community has a public test that scores models on this, with a leaderboard. Each task is a sentence and a synthetic home. The model answers with a tool call, Home Assistant runs it, and the task passes only if the whole home ends up the way it should.
The test has three parts. The assist-mini set is 196 spoken commands in small homes with few devices, the one the leaderboard is sorted by. The assist set is 460 commands in a medium-sized home, written as corner cases to trip models up. The questions set is 370 questions about the house, where the right response is an answer, not an action.
Every answer can be checked by running it: the house ends up right or wrong, 1 or 0. That makes this a good fit for reinforcement learning.
I trained on my own tests, built by a generator in the same format as the community's. The community's three sets were kept out of training and used only to score the model.
02 How it works
The model reads two things: Home Assistant's own prompt for the home, then the command. The prompt lists the rooms, each device with its name and its kind (light, lock, sensor), and the tools the model can use. A tool is an action Home Assistant can carry out, such as TurnOn or TurnOff. The model never changes the house itself. It writes a tool call: which tool, and which device to use it on. For "Unlock the August Lock", the right tool call is:
TurnOff
name: August Lock
kind: lockThe name picks the device. The kind is there because the lock's sensor is also named August Lock. On a lock, TurnOff means unlock. Home Assistant runs the call, and the scorer compares the whole home with the expected end state.
GRPO (Group Relative Policy Optimization) was introduced by DeepSeek in the DeepSeekMath paper (2024). The model gets one prompt and writes a group of answers to it. Each answer is scored. Answers that score above the group's average are made more likely, and answers below it less likely. Each answer is judged only against its own group: that is the "group relative" in the name.
Here, the prompt is one command and the group is eight answers. Each answer scores 1 if the house ends up right, 0 if not. Compare each answer with the group's average. Answers above it get their probability nudged up; answers below it get nudged down. That is one step. Move to the next command and repeat, 1,600 steps in all.
The numbers behind one stepFive short parts, worked on one real sentence.OPENCLOSE
1 The model answers eight times
The model gets "Unlock the August Lock" (the test from section 01) and answers it eight times. Each answer scores 1 if the house ends up right, 0 if not.
The trainer also gave a point for any valid tool call. In 1,587 of the 1,600 steps all eight answers earned it, so it changed nothing.
2 Better answers are pushed up, worse ones down
The average score is 0.75. An answer above the average is made more likely. An answer below it is made less likely. The size of the push is the gap from the average, divided by the spread of the scores (0.463):
push (score − average) / spread
right answer (1 − 0.75) / 0.463 = +0.54
wrong answer (0 − 0.75) / 0.463 = −1.62
3 Equal scores push nothing
The pushes always add up to zero: six of +0.54 and two of −1.62. If all eight answers score the same, every push is zero and the model learns nothing. That is why the first run failed.
4 Only the unsure parts move
The model writes an answer one token at a time (a token is a word or part of a word), and it picks each token with some probability. A push up raises the probability of every token in that answer. A push down lowers it.
Before training, the model was already over 99.9% sure of 29 of the 32 tokens in the right answer, so those barely change. The learning happens at the three places where it was unsure, shown before training and after all 1,600 steps:
5 How the weights change
The loss is minus the push times the log probability of each token, averaged over all the tokens of the eight answers. Backpropagation gives the gradient, and each weight takes a small step against it:
p.data += -lr * p.gradwith lr = 5e-6, on the LoRA weights only. The real optimizer is AdamW, which sizes each weight's step separately; the idea is the same.
Each step took 16.4 seconds. Almost all of it is the model: writing the eight answers, then updating its weights. The scorer checks 62 answers a second.
03 What broke
90%
of the first run's steps had nothing to learn from: all eight answers scored the same.
The first run chose its training data by test, and that was the mistake. Before training, I asked the untrained model every sentence eight times and kept every test it passed only some of the time: 353 tests, 3,793 sentences. But a test has about eleven sentences, and each step uses only one. Inside a mixed test, most sentences still came back right all eight times or wrong all eight times. When the eight scores are equal, every answer sits at the average, so every nudge is zero and the step teaches nothing. I stopped the run at step 197.
04 The fix
616
of the 4,318 sentences, kept for training.
The fix is to choose by sentence, not by test: keep only the sentences the untrained model got right on some tries and wrong on others. The DAPO paper (2025) does the same thing during training and calls it dynamic sampling.
05 The result
75.0 89.8
on the community's voice-command test, before and after training.
The first two bars are the same untrained model, run two ways. The gap between them comes from the setup, not from training. Training is the step from the second bar to the third.
06 How it compares
89.8
on the same 196 tests: level with the leaderboard's 4B models, and with gpt‑oss‑120b.
Training put a 1.7B model level with models many times its size. On assist-mini, hua-1.7b scores above ministral-3-3b, a 3B model, and level with the leaderboard's 4B models. It is also level with gpt-oss-120b and qwen3-235b, which are about 70 and 140 times larger. On the larger assist test, the bigger models keep their lead: gpt-oss-120b scores 85.0, against 68.0 for hua-1.7b.
07 Why not just show it the answers
68.4 59.2
on questions about the house, after supervised fine-tuning on every answer. Trained by pass or fail, hua‑1.7b scored 70.0.
Supervised fine-tuning on all 4,318 stored answers gave the best command score of the four models, 94.4 on assist-mini, and cost 9 points on questions. The cause is what it was shown: every stored answer is a tool call, so it learned to answer questions with a tool call too. Pass-or-fail training on top could not repair this, for two reasons. It trains on commands only, so nothing pushed the questions back. And the model already got almost every command right all eight times, so only 10 of 1,600 steps changed anything. RL's Razor (2025) finds the same: reinforcement learning makes a model forget less than supervised fine-tuning.
08 What is next
Three runs come next. The first applies the same recipe to a smaller model, FunctionGemma-270M, which scores 9.4 on the leaderboard today. The second is supervised fine-tuning on a set that includes questions, then pass-or-fail training on top, so the model keeps its questions score. The third is more pass-or-fail training on new tasks written for the misses that remain, most of them on the larger assist test.
The model, its serving code and the leaderboard scores are linked below.