Repository navigation
Widen a squashed placement's bound before the per-brick cull (#649) - #680
Merged
Merged
Conversation
A per-axis scale on an item or its layer makes the field a bound on the distance: cscale_nu_dist multiplies the local value by min(s), so the value can be short of the distance by up to q = max(s)/min(s), the two levels' ratios multiplied. The cull dropped an item once its bound was band + pad from the brick, which holds only out to q times that, so a brick in between lost an item whose field was inside the band there. Seeds 5743 and 6290 of the rich undo_bound_oracle_probe sweep reduce to this: 5625 and 151 in-band samples off clay_eval_points, worst 0.068. The cull now widens such a bound by slope * (band + pad) + reach, with slope = q - 1 and reach = slope times the rounding and combine support the bound already carries; a group takes its subtree's widest terms plus slope times its own support (scene::CullSquash). The item is still culled everywhere its field cannot reach. Both terms are exactly zero for a similarity, so an unsquashed document makes the same decisions. The band is new input. CullRegion carries it, set wherever it is known (refill requests, resume and uniform tasks, frontier jobs, try_band, the volume bake, gradient-normal meshing, and clay_eval_grid from the lattice's margin inside the host region). CullIndex::Entry caches the squash, plan() takes the batch's widest band and widens a squashed chain's scan region, and a compile drops a plan made for a narrower band than its region carries. RV_RAWCHECK rich sweep (5001..6500): 35 -> 9 documents, worst 0.0825 -> 0.0170. Plain sweep unchanged at 21. What remains is the chain-pad envelope (mechanism A), which this does not touch.
docs/05 states the widening beside what a per-axis scale costs. The widen-the-cull-for-a-squashed-placement change records the measurement, what building it found (the coarse plan needs the band), and modifies the scene-model cull requirement with three scenarios.
The first cut tested the squash before the plain bound and called item_cull_squash out of line for every dropped item: +11% on the unplanned DeepDocCull2000 and +3.5% on the planned cull, for documents with no per-axis scale at all. The plain test now decides every bound it keeps, and culled_item answers a similarity inline. Interleaved A/B against origin/main, 3 rounds: planned cull +1.1..1.5%, unplanned +3.1%, planned refill +2.7%, index rebuild +4.0%, which is the extra comparison and the 8 bytes CullIndex::Entry gained.
leonardoaraujosantos
force-pushed
the
fix/649-squashed-cull-dilation
branch
from
October 4, 2026 05:46
1059373 to
04533e4
Compare
leonardoaraujosantos
added a commit
that referenced
this pull request
Oct 4, 2026
Covers the twenty PRs since v0.120.1 (#655, #667-#669, #673-#688), ABI minors 0.121.0 through 0.126.0, and the scene format minor 19 -> 20. The device gate block is a placeholder until the iPad run is recorded. Carries the four manual hardware gates forward as waivers at 4401b35: the only kernel-relevant change since 59e42cc is #680's per-brick cull band in include/clay/eval/bake_volume.h, which changes what a brick compiles, not kernel arithmetic, and the parity corpus is unchanged.
leonardoaraujosantos
added a commit
that referenced
this pull request
Oct 5, 2026
Covers the twenty PRs since v0.120.1 (#655, #667-#669, #673-#688), ABI minors 0.121.0 through 0.126.0, and the scene format minor 19 -> 20. The device gate block is a placeholder until the iPad run is recorded. Carries the four manual hardware gates forward as waivers at 4401b35: the only kernel-relevant change since 59e42cc is #680's per-brick cull band in include/clay/eval/bake_volume.h, which changes what a brick compiles, not kernel arithmetic, and the parity corpus is unchanged.
leonardoaraujosantos
added a commit
that referenced
this pull request
Oct 6, 2026
Covers the twenty PRs since v0.120.1 (#655, #667-#669, #673-#688), ABI minors 0.121.0 through 0.126.0, and the scene format minor 19 -> 20. The device gate block is a placeholder until the iPad run is recorded. Carries the four manual hardware gates forward as waivers at 4401b35: the only kernel-relevant change since 59e42cc is #680's per-brick cull band in include/clay/eval/bake_volume.h, which changes what a brick compiles, not kernel arithmetic, and the parity corpus is unchanged.
Merged
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.
Why
#649: a full brick build disagrees with the document's own raw field (
clay_eval_points) at in-band samples. #652 fixed the deformer cull and named two mechanisms that remain. This PR fixes the second one, B: squashed placements are culled as if their field were a distance.A per-axis scale on an item or on its layer makes the field a bound on the distance.
cscale_nu_distmultiplies the local value bymin(s), so the value can be short of the true distance by up toq = max(s) / min(s), and the two levels' ratios multiply. The per-brick cull drops an item once its bound is more than band + pad from the brick. That argument only holds out toq * (band + pad). A brick in between dropped an item whose field was still inside the band there.I reproduced it on origin/main 7023dd9 with the issue's probe before changing anything:
Root cause
All three cull tests compare the plain item bound: the item test and the group test in
Compiler::compile_list, and the planned-entry survive test (CullIndexentries). None of them knows the field understates under a squash.item_geometry_reach_in_documentalready refuses a squashed placement for this reason (bounds.h). The cull cannot simply refuse, because then a squashed layer would cull nothing.Fix
The bound is widened, not exempted. A point
Dfrom the geometry reads at leastD / q(less the rounding), so the cull's "more than band + pad + w from the geometry" has to becomeqtimes that. Herewis what the bound already dilates by: the rounding plus the combine support. The widening isA group takes its subtree's widest slope and reach, plus
slopetimes its own blend support. That isscene::CullSquash(item_cull_squash,node_cull_squash).item_bound_dilationis factored out ofplaced_local_bound, sowis exactly the term the bound adds. Both terms are exactly 0 for a similarity (max/min of three equal floats), so a document with no per-axis scale makes identical cull decisions. A test pins this.The band is a new input to the cull:
scene::CullRegiongainsfloat band("how far insideregionthe samples lie"). It now has a constructor, so everyCullRegion{box}keeps meaning "no band" and GCC's missing-field-initializer warning has nothing to say.try_bandclay_eval_grid/_device, which take the band from the lattice's margin inside the host's region. That margin is the bandclay_brick_cache_cull_regionadded.CullIndex::Entrycaches the squash (40 -> 48 B).Chain::widestrecords the chain's widest squash.CullIndex::plan(region, band = 0): a squashed chain's coarse scan widens the region by its widest entry's widening. The packed scan stays one box test per entry, and per-brick survival is still a subset.CullPlan::serves_band: a compile drops a plan made for a narrower band than its region carries, but only when the document holds a squashed placement. About thirtyplan(region)callers in tests and benchmarks are therefore never wrong. On a squashed document they lose the plan, not an item.Regression test (
tests/unit/test_squashed_cull.cpp)Three C ABI cases. Each refills a row of bricks whose band-dilated boxes all miss the node's unsquashed influence bound, in one batch so the plan is used, and compares every in-band sample with
clay_eval_points. Each oneREQUIREs in-band samples beyond the unsquashed band, so it cannot pass because there was nothing to drop.Three scene-level cases:
gnarly_document(layer squash, items squashed at the root, inside nested groups and in the instanced layer). Each brick is planned alone. A per-brick tape with the index and a plan for its band must be byte-identical to the plain one, and so must a tape with a plan for no band, which has to be refused. Band-clamped samples must equal the full tape exactly.gnarly_documentmust produce byte-identical tapes with and without a band.item_cull_squashreturns zero for a similarity andq - 1otherwise, and the two levels multiply.Mutation checks, each reverted afterwards:
serves_bandalways true: the gnarly case fails on byte-identity.Measured
RV_RAWCHECK=1 undo_bound_oracle_probe, Release, Apple M-series:26 documents drop out, 5743 and 6290 among them. Of the 9 left, 3 shrink sharply because B was part of their error: 5483 goes from 10,525 samples off to 14, 5907 from 4,132 to 236, and 5976 from 1,652 to 77. 6055 drops from 3,380 to 3,177 at the same worst value. The other 5 are unchanged. The plain sweep has no document this mechanism reaches.
Cost on unsquashed documents (
clay_bench, Release, Apple M-series, CPU). The two binaries were interleaved over 3 rounds of 5 repetitions at--benchmark_min_time=0.2s. Each figure is the median of the per-round medians, in µs:All three rounds agree within about 1%, so these small costs are real. They come from the extra test on a plain miss and from
CullIndex::Entrygrowing from 40 to 48 B. The first version tested the squash before the plain bound and called out of line for every dropped item: the unplanned cull was +11% and the planned one +3.5%. The plain test now runs first, and a similarity is decided inline (culled_item).check_bench.pypasses every gated case in the run. The only failures it reports are the cases outside the filter, listed as missing from the results.Not in this PR: mechanism A
Seed 978: the #335 chain-pad envelope applies its N = 75 value below 75 contributors, and that breaks the spec's relative bar. Fixing it is a trade between the pad and the #335 performance win, and it needs the iPad gate. I left it alone. The plain sweep's 21 documents and the rich sweep's remaining 9 are where it shows. This PR is Part of #649. It does not close the issue.
Verification
cmake --preset cpu-only -DCLAY_BUILD_TESTS=ON, then build andctest --preset cpu-only: 10/10 passed (four unit shards and the partition check).check_layering,check_kernel_dialect,check_licenses,check_doc_latency, andcheck_c_abi build/cpu-only/libclay_shared.dylib(hygiene + ctypes FFI) all pass.check_test_shards --binary: 3010 cases partitioned.npx @fission-ai/openspec@1.12.0 validate --all --strict: 71 passed.tools/release_check.py --skip-slowpasses every software row: version, configure, build, tests (11/11), parity, layering, dialect, licenses, task-symbols, bindings (it imported the built pyclay), kernels, abi and openspec. The rows that fail are release-time hardware records:device: the iPad gate is stale against files this PR does not touch (CMakeLists.txt, clay.h), so it fails on main too.hardware/*waivers were recorded at 59e42cc, andinclude/clay/eval/bake_volume.h(one line that passes the band) counts as a kernel-path file. They are re-run or re-waived when the release is cut.widen-the-cull-for-a-squashed-placementmodifies the scene-model "Per-brick tape culling" requirement. It repeats the four existing scenarios and adds three.docs/05states the widening.compile_listgets simpler (the group test moved intoculled_group, and the planned test is one call). The new functions are small:node_cull_squashhas two branches and a loop.Part of #649.