Conversation
--save-detections/--save-timing write per-detection and per-stage TSVs so builds can be compared for identical output. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
benchmark.sh wraps hyperfine over vide_images with git-rev-labeled artifacts; .gitignore covers the TSV and benchmark outputs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
quad_segment_maxima's 4-deep maxima loop re-fit identical segments for every outer combination; memoize fit_line results per maxima pair. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fixed sigma=1/cutoff=0.05 kernel (size 7) was recomputed with exp() for every cluster; compute it once per thread. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The error, low-pass, and maxima loops indexed errs[] with % sz; a single branch adjustment wraps without the integer divide. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fit_quad malloc'd five buffers and compute_lfps calloc'd a sixth for every cluster; thread one growable per-task scratch struct through quad_segment_maxima/compute_lfps/do_quad_task instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The old ptsort copied the whole range into temp at every recursion level. Alternate between two buffers so data is copied only at the <=5-element leaves. Same splits, leaf networks, and merge tie behavior, so the sorted order is bit-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
check.sh diffs --save-detections TSVs against a baseline run with per-field epsilon gates (id/hamming exact, coords and margin tolerant). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lets sqrt() and friends compile to bare (vectorizable) instructions instead of errno-guarded libm calls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Free each cluster in its quad task (parallel, cache-warm) instead of a serial ~11k-free loop after fit_quads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each merge-tree level's pairwise merges fan out to the worker pool instead of running serially on the calling thread. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bucket selection becomes a mask instead of an integer divide, and the table is sized to the slab (well under one entry per 64 slab pixels) so it stays cache resident instead of spread across 0.2*w*h callocs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With the hash table sized independently of the slab, the pool-chunk array is seeded small and realloc-doubled as chunks fill. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The collection loop already iterates buckets in order, so the secondary key is just the bucket index instead of a re-hash. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consecutive boundary points almost always extend the same cluster, so remember the last entry and skip the hash lookup when the id matches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Most pixels are interior to a region and never reach a black/white boundary neighbor; compute rep0 (and its size gate) lazily on the first one instead of eagerly for every boundary-candidate pixel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Read uf->size[rep] + 1 directly instead of calling unionfind_get_set_size in the hot boundary loop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
key64 = monotone(slope) << 32 | ~index. Full-u64 merge comparisons reproduce the historical merge's take-right-on-tie rule (left-run elements always carry smaller original indices -> larger complements); leaf networks compare the slope word only, matching no-swap-on-tie. Bit-identical ordering, but the sort moves 8-byte keys instead of 12-byte structs and compares without function calls; points are gathered once at the end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drive cluster construction from row runs: component representatives resolve once per run (cached) instead of per boundary pixel, 127 spans skip wholesale, and the same (y, x, neighbor) emission order is kept so output is bit-identical. Cluster points accumulate in fixed-size chunks bump-allocated from a per-task pool instead of doubling zarrays (a frame can produce ~3M points; growth reallocs were copying tens of MB per frame). Clusters materialize into exact-size zarrays at collection. Output verified byte-identical over the 133-image corpus. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
threshim and the four tile min/max arrays are kept on the detector and reused instead of allocated and freed every frame; every entry is rewritten before use. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Only run heads (plus the lazily-initialized last column) enter the union-find: the run-driven cluster pass resolves representatives through run heads, so the per-pixel parent fill (~25MB of writes/frame) was waste. Unions operate on head ids -- same union graph, same nodes, so roots, sizes, and output are identical. The serial stitch becomes a run-pair pass and the per-pixel line code is gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
While a run and the run below keep overlapping, each pixel emits exactly its (0,1) and (1,1) points into the same cluster entry, so emit them in a tight loop without per-pixel pointer or gate checks. Output verified byte-identical over the 133-image corpus. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The per-point window error loop computes the small eigenvalue inline (identical arithmetic) for windows that don't wrap the cluster ends; only the wrapping windows still call the general fit_line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge comparisons are data-dependent coin flips; select with a conditional move instead of a ~50%-mispredicted branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same output: clusters ascend by (hash, id) and split clusters concatenate their points in task order, exactly as the pairwise tree produced. Saves a workerpool barrier per level and all intermediate lists. Detections verified within 1e-4 px of baseline (the build now uses -march=native, whose FMA contraction shifts coordinates by ~1e-4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The slope was only ever used to order points, and the ordering now runs on packed u64 keys built directly in fit_quad's angle loop, so the stored float is dead weight. Dropping it cuts a third off all point traffic (chunk storage, merges, collection copies, sort gathers). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sorted point array was only consumed by compute_lfps. Sort just the key array and have compute_lfps gather points through the key indices, skipping the per-cluster point permutation and its scratch buffer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
__AVX2__-guarded vectorization (scalar fallback kept) for the threshold stage's 4x4 tile min/max and the 3x3 tile blur. Integer min/max only, so results are exact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rows were being run-length encoded up to four times per frame in scalar byte loops (union-find tasks, stitch, and gradient clustering each re-scanned threshim). A single two-pass parallel build (SIMD count via change masks, prefix-sum, SIMD fill) now produces packed row-run tables cached on the detector, and all consumers read those instead of re-scanning the 6.4MB image. Output verified within 1e-4 px over the 133-image corpus. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wraps the two-phase PGO build (bit-identical output). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The task chunking can leave the final row outside every task (the historical code left those pixels lazily initialized and unconnected). The read-only cluster finds crashed on such nodes' 0xffffffff markers for image heights that hit this case. Initialize them as size-0 singletons, matching the historical gate behavior exactly. Fixes test_tag_pose_estimation segfaults; all 8 ctest pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The descending qsort only existed to read one order statistic (the max_nmaxima-th largest window error); quickselect finds the same value in linear time without libc comparator calls. Output verified within 1e-4 px over the 133-image corpus. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Components and sizes are invariant to the find implementation; the two root chains are independent, so walking them interleaved hides latency. Output verified within 1e-4 px over the 133-image corpus. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A virtual-cursor 4-way merge produces exactly the composition of the two 2-way merges it replaces, so the merge tree's passes over the data halve while the output stays bit-identical (verified byte-equal detections). Leaf handling and odd-size levels keep the 2-way code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each thresholded line (including its right-edge fixup) is RLE'd while cache hot into per-task buffers whose rows are contiguous, so the packed table builds with one memcpy per task; the separate count and fill passes over threshim disappear (build_frame_runs remains for the deglitch path, which rewrites threshim afterwards). Bottom partial rows are handled in a serial tail. Output verified within 1e-4 px; all 8 ctest pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The k-way merge was append-realloc'ing each fragment of a boundary- crossing cluster into the previous ones (most tall clusters split across the 40 row slabs, costing ~8ms/frame in realloc copies). Gather each equal-id group from the heap and concatenate into one exact-size allocation; unsplit clusters keep the zero-copy pointer move. Output verified within 1e-4 px over the 133-image corpus. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eight-lane accumulation summed once at the end; the dot only decides the reversed-border sign, which sits far from zero for any usable cluster, and the detection set is unchanged on the corpus. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Balances per-chunk overhead against load balance across the worker pool. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
taskset the four workers onto a single CCD so the small shared structures don't bounce between L3s when the scheduler spreads them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One allocation backs all of a task's collection entries instead of ~11k mallocs+frees per frame. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clusters are now a single allocation (header + points) instead of a zarray shell plus separately-malloc'd data: one malloc and one free per cluster, and merged fragments concatenate into one exact-size block. Output verified within 1e-4 px; all 8 ctest pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Decoding does not modify the quad; the copy only matters when several families restart from the same fit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The k-way heap walk now only records fragments and group boundaries; the exact-size concatenations fan out to the worker pool into order-preserving output slots. Output verified within 1e-4 px; all 8 ctest pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pass 1 now walks points sequentially (the cluster's raster-coherent emission order), keeping its four image-gradient loads cache-local instead of scattering them around the perimeter in angle order; the prefix pass gathers the per-point values from the small scratch arrays through the sorted key indices instead. Identical arithmetic per point. Output verified within 1e-4 px; all 8 ctest pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same projection expressions; the matd element loads happen once per quad instead of per bit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four steps per iteration: both bilinear samples, the bounds checks, and the gradient test run as 4-wide doubles; rejected lanes contribute an exact 0.0 to the weight accumulators, so only the final reduction order differs (within 1e-4 px on the corpus). Indices are clamped so masked lanes still load safely. Scalar tail handles max_steps % 4. All 8 ctest pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jun 10, 2026
Member
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Detector optimization campaign — 88 → 39.8 ms/image (2.21×)
End-to-end speedup of the AprilTag detector on the 133-image
vide_imagescorpus(4 threads, default params, tagStandard52h13), output byte-equivalent to baseline at
every commit (4583/4583 detections; coords within 1e-4 px — FMA-contraction noise only).
History is organized as one optimization per commit (70 commits) so each change can
be reviewed and bisected in isolation.
PERF_NOTES.mddocuments the methodology, theper-stage breakdown, the load-bearing invariants (notably the angle-sort tie order that
no standard sort reproduces), and the measured dead ends.
Where the time went (stage ms, baseline → now)
threshold tasks; buffers cached across frames.
per adjacent same-value run pair; interleaved-root connect; no per-frame reset.
storage; per-run rep+gate cache with read-only finds; k-way heap merge with
parallelized fragment concatenation; flat single-allocation clusters.
window-error/filter/maxima loops; 4-way fused merge sort; SoA moments; memoized
segment fits; per-task scratch reuse.
graymodel hoists; vectorized refine_edges sampling.
Build / verification
-DBUILD_EXAMPLES=ON); AVX2 paths are__AVX2__-guarded withscalar fallbacks.
benchmark.sh/ab.sh/check.share the measurement andoutput-equivalence harnesses.
Run-driven gradient clusters …,Shrink struct pt …) aresingle cohesive commits — their parts are co-designed and have no separable compiling
intermediate; everything else is split to one optimization per commit.