Merit launch timing, October 22, 2026#
The merit kernel evaluates the eight line-search candidates of every knot and
solve. Two questions from issue #19 (Felix Mueh, GTX 1070): whether fusing the
eight candidates into one block per knot helps on a GPU with few SMs, and what
block size the kernel should run at. Both were measured in a user-assigned
quiet window on the RTX 5090, source a791af7, grid-rbd 0.5.4, with
examples/benchmarks/merit_fusion_timing.py: Indy7 and iiwa14 at N32 and N64,
Go2 at N16, batch 1, 8 and 16, two SQP iterations per cold solve from the
hold-at-x seed with the trust region reset, 20 warm-up and 200 timed solves per
cell, the merit time per launch from SolverStats.stage_times_us. Three
conditions: the 5090 as is, and the 5090 throttled through MPS
(CUDA_MPS_ACTIVE_THREAD_PERCENTAGE) to 10 and 25 percent of its SMs, the
stand-in for a 14-SM Jetson Orin or a 15-SM GTX 1070. Every row is a median of
200 solves; the p90 is within a few percent of it in every cell. Raw rows:
merit_timing_20261022.jsonl
(SHA256 2299d15ea2a9).
Columns: merit device time per launch in microseconds at the generated per-robot maximum block size (the former default: Indy7 256, iiwa14 352, Go2 448), at 128 and at 64 threads; the whole solve’s median at the former default and at 64; and the fused launch’s time relative to the one-block-per-candidate launch at the former default block size.
RTX 5090#
plant |
N |
B |
merit, generated max |
merit, 128 |
merit, 64 |
solve, generated max |
solve, 64 |
fused / unfused at the max |
|---|---|---|---|---|---|---|---|---|
go2 |
16 |
1 |
61 |
62 |
63 |
2296 |
2303 |
7.22 |
go2 |
16 |
8 |
175 |
114 |
119 |
2529 |
2422 |
2.50 |
go2 |
16 |
16 |
298 |
169 |
173 |
3072 |
2817 |
1.50 |
iiwa14 |
32 |
1 |
31 |
31 |
32 |
698 |
701 |
4.80 |
iiwa14 |
32 |
8 |
112 |
65 |
46 |
863 |
733 |
1.31 |
iiwa14 |
32 |
16 |
207 |
98 |
72 |
1081 |
798 |
1.33 |
iiwa14 |
64 |
1 |
46 |
32 |
33 |
2287 |
2273 |
3.18 |
iiwa14 |
64 |
8 |
208 |
97 |
72 |
3160 |
2874 |
1.35 |
iiwa14 |
64 |
16 |
397 |
179 |
119 |
3646 |
3057 |
1.10 |
indy7 |
32 |
1 |
28 |
28 |
31 |
591 |
609 |
3.98 |
indy7 |
32 |
8 |
96 |
57 |
53 |
734 |
651 |
1.19 |
indy7 |
32 |
16 |
172 |
98 |
68 |
899 |
687 |
1.18 |
indy7 |
64 |
1 |
42 |
29 |
30 |
1397 |
1373 |
2.63 |
indy7 |
64 |
8 |
173 |
98 |
71 |
1881 |
1669 |
1.18 |
indy7 |
64 |
16 |
327 |
177 |
123 |
2285 |
1849 |
0.97 |
RTX 5090 at 10 percent of its SMs (MPS)#
plant |
N |
B |
merit, generated max |
merit, 128 |
merit, 64 |
solve, generated max |
solve, 64 |
fused / unfused at the max |
|---|---|---|---|---|---|---|---|---|
go2 |
16 |
1 |
148 |
73 |
73 |
2461 |
2310 |
2.91 |
go2 |
16 |
8 |
841 |
387 |
396 |
5692 |
4708 |
1.43 |
go2 |
16 |
16 |
1645 |
736 |
752 |
9271 |
7340 |
1.23 |
iiwa14 |
32 |
1 |
94 |
48 |
41 |
821 |
716 |
1.52 |
iiwa14 |
32 |
8 |
574 |
249 |
159 |
2011 |
1123 |
1.15 |
iiwa14 |
32 |
16 |
1127 |
468 |
287 |
3430 |
1657 |
1.07 |
iiwa14 |
64 |
1 |
163 |
80 |
63 |
2551 |
2335 |
1.68 |
iiwa14 |
64 |
8 |
1141 |
476 |
289 |
5561 |
3766 |
1.08 |
iiwa14 |
64 |
16 |
2259 |
924 |
547 |
8469 |
4846 |
1.08 |
indy7 |
32 |
1 |
81 |
58 |
46 |
707 |
634 |
1.36 |
indy7 |
32 |
8 |
469 |
248 |
173 |
1655 |
1032 |
1.03 |
indy7 |
32 |
16 |
918 |
478 |
322 |
2822 |
1564 |
0.96 |
indy7 |
64 |
1 |
135 |
81 |
63 |
1587 |
1440 |
1.48 |
indy7 |
64 |
8 |
926 |
484 |
328 |
3836 |
2566 |
0.98 |
indy7 |
64 |
16 |
1834 |
944 |
633 |
6195 |
3649 |
0.96 |
RTX 5090 at 25 percent of its SMs (MPS)#
plant |
N |
B |
merit, generated max |
merit, 128 |
merit, 64 |
solve, generated max |
solve, 64 |
fused / unfused at the max |
|---|---|---|---|---|---|---|---|---|
go2 |
16 |
1 |
107 |
63 |
63 |
2381 |
2301 |
4.07 |
go2 |
16 |
8 |
574 |
267 |
275 |
4255 |
3602 |
1.44 |
go2 |
16 |
16 |
1111 |
505 |
518 |
6730 |
5474 |
1.27 |
iiwa14 |
32 |
1 |
66 |
47 |
36 |
765 |
701 |
2.20 |
iiwa14 |
32 |
8 |
390 |
174 |
115 |
1533 |
958 |
1.11 |
iiwa14 |
32 |
16 |
759 |
323 |
204 |
2494 |
1323 |
1.09 |
iiwa14 |
64 |
1 |
113 |
61 |
57 |
2437 |
2329 |
1.27 |
iiwa14 |
64 |
8 |
769 |
328 |
205 |
4633 |
3449 |
1.11 |
iiwa14 |
64 |
16 |
1516 |
625 |
376 |
6570 |
4154 |
1.09 |
indy7 |
32 |
1 |
58 |
45 |
32 |
658 |
609 |
1.90 |
indy7 |
32 |
8 |
321 |
174 |
122 |
1268 |
855 |
0.99 |
indy7 |
32 |
16 |
617 |
324 |
225 |
2049 |
1208 |
0.96 |
indy7 |
64 |
1 |
96 |
59 |
50 |
1503 |
1409 |
1.16 |
indy7 |
64 |
8 |
625 |
330 |
227 |
3060 |
2228 |
0.98 |
indy7 |
64 |
16 |
1230 |
638 |
430 |
4652 |
2955 |
0.96 |
Reading#
The block size is the lever. At 64 threads the merit launch takes two to four
times less than at the generated maximum at batch 8 and 16 in every condition,
and the whole solve 15 to 25 percent less on the 5090 and up to 45 percent less
at 10 percent of its SMs; at batch 1 the sizes tie within a few microseconds.
Go2 is the same at 64 and 128. The merit kernel is latency bound, one knot’s
integrator and cost per block, so the smaller block raises the number of
resident blocks without starving the loops; GRiD’s widest-loop count
(TRACKING_COST_SUGGESTED_THREADS, 224 on iiwa14) is not the operating point.
The default is therefore 64 threads (GATO_MERIT_THREADS), bit-identical to
the former default (goldens unchanged, the thread-invariance gate in the
register tests).
The fused launch does not help. One block per knot evaluating the eight
candidates in turn never won: within the repeat spread on Indy7 at batch 8 and 16
(0.96 to 0.99 of the unfused time) and slower everywhere else, up to seven times at
batch 1 (Go2), in all three conditions: the candidates
serialize inside the block while the GPU, even at 10 percent of its SMs, still
had room for the candidate blocks. The SM-count rule that selected it is
removed; the path stays as GATO_MERIT_FUSED=1 for a measurement on hardware
that behaves differently, and the kernel gate keeps it bit-identical.
Jetson AGX Orin, October 24, 2026#
Felix Mueh ran the same breakdown on a Jetson AGX Orin 32 GB at 40 W (JetPack
6.2, CUDA 12.6, GATO 0.1.5 at 4d5f8c2, the GPU clock between 306 and 816 MHz
during the run) and posted it on issue #19: eight SQP iterations, batch 1, 4 and
16, ten interleaved repetitions per cell, the per-stage times from
SolverStats.stage_times_us, mean and sample standard deviation in
milliseconds per solve. The four launches are 32, 64 and 256 threads per block
and the fused launch at 64.
Warm solve (the previous solution as the seed):
launch |
B |
solve |
merit |
linsys |
kkt |
schur |
merit share |
|---|---|---|---|---|---|---|---|
32 |
1 |
22.73 ± 1.89 |
2.73 ± 0.21 |
16.34 ± 1.46 |
1.77 |
1.28 |
12.0 % |
64 |
1 |
22.76 ± 2.06 |
2.94 ± 0.28 |
16.18 ± 1.63 |
1.75 |
1.28 |
12.9 % |
256 |
1 |
25.07 ± 3.35 |
6.05 ± 0.88 |
15.45 ± 2.10 |
1.67 |
1.22 |
24.1 % |
64 fused |
1 |
26.23 ± 1.60 |
5.72 ± 0.29 |
16.75 ± 1.21 |
1.79 |
1.31 |
21.8 % |
32 |
4 |
26.16 ± 3.82 |
5.76 ± 0.90 |
15.29 ± 2.29 |
2.89 |
1.55 |
22.0 % |
64 |
4 |
26.19 ± 4.18 |
6.79 ± 1.17 |
14.52 ± 2.37 |
2.74 |
1.45 |
25.9 % |
256 |
4 |
26.26 ± 3.86 |
13.07 ± 1.62 |
9.60 ± 1.61 |
1.85 |
1.00 |
50.0 % |
64 fused |
4 |
27.56 ± 5.17 |
9.13 ± 1.67 |
13.77 ± 2.72 |
2.61 |
1.40 |
33.2 % |
32 |
16 |
34.24 ± 0.48 |
10.30 ± 0.16 |
15.68 ± 0.22 |
5.32 |
2.31 |
30.1 % |
64 |
16 |
37.97 ± 0.58 |
14.04 ± 0.26 |
15.60 ± 0.22 |
5.33 |
2.30 |
37.0 % |
256 |
16 |
63.94 ± 1.32 |
40.82 ± 0.83 |
14.61 ± 0.26 |
5.08 |
2.16 |
63.9 % |
64 fused |
16 |
38.08 ± 0.32 |
14.20 ± 0.18 |
15.54 ± 0.08 |
5.28 |
2.30 |
37.3 % |
Cold solve (the hold-at-x seed):
launch |
B |
solve |
merit |
linsys |
kkt |
schur |
merit share |
|---|---|---|---|---|---|---|---|
32 |
1 |
25.30 ± 0.71 |
3.11 ± 0.03 |
12.32 ± 0.01 |
2.32 |
1.36 |
12.3 % |
64 |
1 |
25.32 ± 1.11 |
3.42 ± 0.04 |
12.32 ± 0.01 |
2.38 |
1.36 |
13.5 % |
256 |
1 |
29.12 ± 0.86 |
7.17 ± 0.04 |
12.32 ± 0.01 |
2.24 |
1.36 |
24.6 % |
64 fused |
1 |
27.77 ± 0.71 |
6.14 ± 0.05 |
12.33 ± 0.01 |
2.41 |
1.36 |
22.1 % |
32 |
4 |
31.33 ± 1.41 |
7.01 ± 0.05 |
12.41 ± 0.01 |
3.73 |
1.74 |
22.4 % |
64 |
4 |
32.98 ± 0.73 |
8.85 ± 0.04 |
12.41 ± 0.01 |
3.64 |
1.74 |
26.9 % |
256 |
4 |
48.67 ± 1.75 |
24.64 ± 1.05 |
12.27 ± 0.35 |
3.47 |
1.74 |
50.6 % |
64 fused |
4 |
36.74 ± 1.29 |
12.51 ± 0.07 |
12.42 ± 0.01 |
3.68 |
1.75 |
34.1 % |
32 |
16 |
61.21 ± 5.05 |
19.08 ± 1.72 |
20.89 ± 2.41 |
10.38 |
4.58 |
31.2 % |
64 |
16 |
65.40 ± 5.85 |
25.23 ± 2.25 |
19.58 ± 2.39 |
9.97 |
4.40 |
38.6 % |
256 |
16 |
93.68 ± 5.76 |
60.47 ± 4.01 |
14.56 ± 0.85 |
7.80 |
3.39 |
64.5 % |
64 fused |
16 |
67.90 ± 3.42 |
26.12 ± 1.50 |
20.57 ± 1.33 |
10.21 |
4.53 |
38.5 % |
The Orin agrees with the throttled 5090 on the two questions above and adds a
third answer. The merit launch at 64 threads grows 4.8 times from batch 1 to
16 on the warm solve, against 15.75 times reported for the old default on the
GTX 1070, and 256 threads costs two to three times the merit time of 64 at
every batch. The fused launch gains nothing: level with the unfused launch at
batch 16 and 15 percent slower at batch 1. The third answer is that 32 threads
beats 64 on this GPU: level within the spread at batch 1 and 4, and at batch 16
the merit time falls from 14.0 to 10.3 ms and the whole solve by 10 percent,
warm and cold. The 5090 tables did not include 32 threads. From 0.1.6 the
configure-time default is therefore 32 on the Jetson family (sm_72 and sm_87,
CMakeLists.txt) and 64 elsewhere; GATO_MERIT_THREADS still overrides
either, and results are bit-identical at any size.
The tables also show where the time goes on this GPU once the merit launch is sized: the linear system. At batch 1 it is 70 percent of the solve, and it does not grow with the batch (0.96 from batch 1 to 16 on the warm solve); the cold-solve linsys time is 12.32 ms with a spread of 0.01 ms in every launch configuration. The October 26 sweep below shows that this is a fixed PCG iteration count per problem, not the iteration cap.
Jetson AGX Orin, October 26, 2026: longer horizons#
Felix Mueh ran a second sweep on the same Orin at GATO 0.1.6 (the build picked
32 merit threads on sm_87 on its own; JetPack 6.2.1, CUDA 12.6, the GPU clock
between 306 and 816 MHz, ten interleaved repetitions, each run in its own
process; scripts on his fork under issue19/). Example 15’s problem on the
Indy7, dt 0.01, eight SQP iterations, cold then warm.
Merit block size 32 against 64 at longer horizons, warm solve, mean and sample standard deviation in milliseconds; the plans are bit-identical between the two block sizes, and the cold solves show the same ratios (1.16 to 1.44):
N |
B |
merit, 32 |
merit, 64 |
merit 64 / 32 |
solve 64 / 32 |
|---|---|---|---|---|---|
64 |
1 |
2.49 ± 0.42 |
3.14 ± 0.39 |
1.26 |
1.09 |
64 |
4 |
6.39 ± 1.77 |
7.48 ± 0.64 |
1.17 |
0.98 |
64 |
16 |
17.98 ± 0.55 |
24.66 ± 0.89 |
1.37 |
1.12 |
128 |
1 |
3.10 ± 0.19 |
3.87 ± 0.21 |
1.25 |
0.98 |
128 |
4 |
9.59 ± 0.49 |
13.01 ± 0.57 |
1.36 |
1.07 |
128 |
16 |
33.19 ± 0.42 |
45.13 ± 0.28 |
1.36 |
1.09 |
256 |
1 |
4.92 ± 0.10 |
6.48 ± 0.11 |
1.32 |
1.02 |
256 |
4 |
16.83 ± 0.24 |
23.14 ± 0.21 |
1.38 |
1.07 |
256 |
16 |
66.07 ± 0.58 |
90.46 ± 0.82 |
1.37 |
1.09 |
512 |
1 |
9.04 ± 0.11 |
12.15 ± 0.13 |
1.34 |
1.01 |
512 |
4 |
33.28 ± 0.36 |
44.93 ± 0.43 |
1.35 |
1.02 |
512 |
16 |
130.35 ± 1.36 |
173.78 ± 0.98 |
1.33 |
1.03 |
The merit launch is faster at 32 threads in all 24 cells; the whole solve gains up to 12 percent at N64 and batch 16 and 1 to 3 percent at N512, where the linear system dominates (the ratios below 1 are within the spread). The sm_87 default of 0.1.6 holds at every horizon tried.
PCG iterations per SQP iteration at the library default max_pcg_iters = 200,
identical in all ten runs and at batch 1, 4 and 16:
N |
cold |
warm |
|---|---|---|
32 |
27, 29, 32, 34, 50, 50, 56, 54 |
59, 59, 60, 60, 60, 61, 60, 58 |
64 |
30, 44, 53, 56, 85, 88, 58, 57 |
89, 30, 85, 29, 86, 55, 29, 30 |
128 |
30, 79, 85, 92, 99, 98, 89, 90 |
85, 77, 76, 47, 89, 82, 60, 84 |
256 |
30, 131, 120, 132, 132, 131, 128, 150 |
111, 101, 93, 88, 84, 39, 44, 97 |
512 |
31, 144, 118, 128, 130, 159, 152, 200 |
127, 126, 192, 115, 112, 105, 103, 96 |
One SQP iteration in the whole sweep reaches the cap (N512, cold, the last one). The 12.32 ms of the October 24 cold solve is therefore a fixed iteration count, and the warm solve at N32 runs more PCG iterations than the cold one (477 against 332), which is why its linear system took longer in the first tables. The linear system’s share of the warm batch-1 solve grows with the horizon: 72 percent at N32, 78 at N64, 85 at N128, 89 at N256 and 96 at N512 (13.9, 17.2, 32.3, 69.6 and 427.1 ms); from N256 to N512 the PCG time grows 6.1 times for 1.5 times the iterations.
The same sweep found that the direct path returned its seed on the Orin on
every horizon (every line search failed; bdsv_first then ran PCG to the cap
on each later iteration). That was a GATO defect on every target other than
sm_120, fixed in 0.1.7 (CHANGELOG); the bdsv timings of the sweep measured
solves that did no work and are not reported. Which of the two paths the
embedded default should be is the next measurement for this page, on 0.1.7.
Jetson AGX Orin, October 30, 2026: the direct solve against PCG on 0.1.7#
Felix Mueh reran the linear-system half of the sweep on GATO 0.1.7, which
fixed the direct path on every target other than sm_120 (CHANGELOG 0.1.7).
Same Orin at 40 W without jetson_clocks (GPU clock samples 306 to 816 MHz),
example 15’s Indy7 problem, eight SQP iterations, ten runs per cell, each in
its own process; scripts on his fork under issue19/, logs on issue #19.
The direct path now takes a step in every iteration. On the golden problem at
batch 1 (issue19/bdsv_golden_check.py) it lands near the committed goldens,
where 0.1.6 had returned its seed:
N |
linsys |
initial merit |
final merit, 0.1.6 |
final merit, 0.1.7 |
golden |
|---|---|---|---|---|---|
32 |
pcg |
63.55 |
26.43 |
26.43 |
26.83 |
32 |
bdsv |
63.55 |
63.55 |
5.03 |
4.00 |
64 |
pcg |
108.22 |
33.89 |
33.89 |
34.10 |
64 |
bdsv |
108.22 |
108.22 |
5.11 |
4.40 |
128 |
pcg |
197.55 |
42.15 |
42.15 |
49.80 |
128 |
bdsv |
197.55 |
197.55 |
2.97 |
3.66 |
Wall time per solve, mean and sample standard deviation in milliseconds, and the ratio of the direct solve to PCG:
N |
B |
warm pcg |
warm bdsv |
ratio |
cold pcg |
cold bdsv |
ratio |
|---|---|---|---|---|---|---|---|
32 |
1 |
18.63 ± 3.61 |
19.55 ± 4.29 |
1.05 |
20.35 ± 3.88 |
26.55 ± 5.10 |
1.30 |
32 |
4 |
21.47 ± 2.49 |
23.45 ± 5.62 |
1.09 |
25.69 ± 4.34 |
32.41 ± 6.18 |
1.26 |
32 |
16 |
38.30 ± 12.58 |
27.95 ± 2.82 |
0.73 |
54.19 ± 10.21 |
54.98 ± 9.46 |
1.01 |
64 |
1 |
24.27 ± 7.26 |
29.11 ± 7.85 |
1.20 |
40.81 ± 6.37 |
43.72 ± 5.85 |
1.07 |
64 |
4 |
25.69 ± 7.77 |
34.16 ± 10.52 |
1.33 |
46.58 ± 6.75 |
52.82 ± 7.52 |
1.13 |
64 |
16 |
55.56 ± 2.14 |
51.78 ± 2.43 |
0.93 |
85.86 ± 9.74 |
81.96 ± 10.35 |
0.95 |
128 |
1 |
37.38 ± 2.44 |
40.71 ± 2.97 |
1.09 |
67.95 ± 8.30 |
71.06 ± 7.84 |
1.05 |
128 |
4 |
50.43 ± 1.52 |
51.40 ± 2.57 |
1.02 |
86.83 ± 7.15 |
80.14 ± 11.94 |
0.92 |
128 |
16 |
123.23 ± 1.78 |
96.58 ± 1.98 |
0.78 |
161.48 ± 11.41 |
135.34 ± 11.28 |
0.84 |
256 |
1 |
78.40 ± 1.35 |
74.72 ± 1.32 |
0.95 |
149.69 ± 18.24 |
108.19 ± 11.15 |
0.72 |
256 |
4 |
101.07 ± 1.39 |
95.77 ± 1.78 |
0.95 |
179.09 ± 15.29 |
132.64 ± 13.75 |
0.74 |
256 |
16 |
271.67 ± 1.78 |
195.00 ± 0.85 |
0.72 |
389.48 ± 13.61 |
239.60 ± 12.24 |
0.62 |
512 |
1 |
442.60 ± 2.39 |
147.27 ± 0.93 |
0.33 |
522.29 ± 12.89 |
184.21 ± 16.62 |
0.35 |
512 |
4 |
575.61 ± 3.08 |
198.82 ± 3.54 |
0.35 |
653.99 ± 8.24 |
236.44 ± 10.35 |
0.36 |
512 |
16 |
1319.45 ± 18.66 |
399.68 ± 0.69 |
0.30 |
1469.95 ± 28.13 |
443.69 ± 6.21 |
0.30 |
The direct solve grows about linearly with the horizon and PCG does not: the warm linear system at batch 1 from N32 to N512 is 14.6, 23.9, 34.9, 65.4 and 130.8 ms for the direct solve against 13.4, 18.8, 31.6, 69.5 and 426.1 ms for PCG. Final merit after the eight iterations, the same at every batch size:
N |
cold pcg |
cold bdsv |
cold bdsv_first |
warm pcg |
warm bdsv |
warm bdsv_first |
|---|---|---|---|---|---|---|
32 |
38.72 |
30.61 |
33.97 |
31.31 |
21.26 |
27.32 |
64 |
51.78 |
45.54 |
50.11 |
38.05 |
31.88 |
39.48 |
128 |
84.69 |
72.53 |
81.01 |
58.75 |
39.18 |
60.02 |
256 |
157.12 |
137.95 |
153.77 |
86.36 |
77.28 |
105.07 |
512 |
297.84 |
257.54 |
290.40 |
217.05 |
145.12 |
244.38 |
The direct solve accepts equal or larger steps than PCG at every horizon
(warm, batch 1: 0.0625 to 0.25 against 0.0078 to 0.125) and ends at the lower
merit in every cell; none of the 900 solves meets the KKT tolerance within
eight iterations, so the table compares progress per iteration. bdsv_first
gains nothing consistent (0.82 to 1.22 of the PCG wall time up to N256, 0.79
to 0.96 at N512) and ends above the direct solve everywhere: on this GPU it is
a loss.
The reading for the default: from N256 the direct solve is faster at every
batch size, by about three times at N512, and at batch 16 from N32 warm; PCG
keeps a few milliseconds at N32 to N128 at batch 1 and 4, by up to a third,
inside the clock spread. From 0.2 the fixed-base default follows the horizon
(gato.linsys_autotune.BDSV_HORIZON): the per-step policy below N256, the
direct solve from N256; the floating base keeps the direct solve. The same
sweep on the RTX 5090 is the open row of this page: the direct solve grows
linearly in N and the crossover can sit elsewhere on a large GPU, so nothing
below N256 changes before it runs.