Skip to content

fix: zero-init TGS empty_trigrid_node_ + AOS Point/AOSNode (root-cause CI flakes) - #28

Merged
LimHyungTae merged 1 commit into
mainfrom
fix/tgs-uninit-memory
May 9, 2026
Merged

LimHyungTae merged 1 commit into
mainfrom
fix/tgs-uninit-memory

Conversation

@LimHyungTae

@LimHyungTae LimHyungTae commented May 9, 2026 •

Copy link
Copy Markdown
Contributor

Two related uninit-memory bugs that caused 1-1.5% run-to-run mismatches in the Python smoke tests on the GitHub Ubuntu runners. Same shape in both: a class member declared without an initializer, copied into a working buffer that the algorithm then assumes is zeroed.

TGS

initTriGridField had four no-op expression statements where it clearly intended to zero-initialize the Eigen members of empty_trigrid_node_:

empty_trigrid_node_.normal;            // evaluates, drops the lvalue
empty_trigrid_node_.mean_pt;           // same
empty_trigrid_node_.singular_values;   // same
empty_trigrid_node_.eigen_vectors;     // same

Eigen vectors/matrices don't zero-init by default. empty_trigrid_node_ then got copied into every cell of trigrid_field_, seeding the whole field with stack garbage.

Fix: replace with explicit Eigen::Vector3f::Zero() / Eigen::Matrix3f::Zero().

AOS

class Point declared int label; (and float x,y,z;, uint idx;) without in-class initializers. range_mat_ entries got assigned fresh Point copies in sphericalProjection, leaking the uninit label field into the working set. class AOSNode had the same problem on its start / end members. Combined, they made cluster filtering decisions in labelPointcloud run-to-run nondeterministic — about 1-1.5% of points flipped between "in some cluster" and "no cluster" on consecutive segment_objects calls with identical input.

Fix: brace-init (label{0}, start = 0, etc.).

Why this is safe under the "never break the pipeline" rule

This is a correctness fix, not a behavior change. The existing code's output was already nondeterministic, so there's no single "original behavior" to preserve. What we want to preserve is the deterministic majority result, which is exactly what the algorithm produces in the common (non-uninit-read) path.

Verified on macOS arm64:

$ ./build/tests/regression_kitti kitti00_000000.bin /tmp/run.bin cpp/tests/data/kitti00_000000_gold.bin
REGRESSION PASS: bit-identical to cpp/tests/data/kitti00_000000_gold.bin (374008 bytes)

So the algorithm's output on KITTI sequence 00 frame 0 (with AOS seed=42) is byte-for-byte the same as the gold dump captured pre-fix. The fix moves the previously-stochastic minority into the same canonical answer the deterministic majority was already producing.

test_xyz_and_xyzi_inputs_both_accepted and test_class_api_matches_convenience_function ran 30 times back-to-back on macOS — 30/30 passed for both (each was flaking ~1-1.5% on the Ubuntu runner before this commit).

Full pytest python/tests/ -> 7/7 passed.

Test plan

  • CI green across all 6 jobs (KITTI bit-compare in particular).
  • Two CI runs back-to-back (workflow re-run from this PR's HEAD) both green — confirms the AOS-side flake is gone, not just rerun-lucky.

… Point/AOSNode

Two related uninit-memory bugs that caused 1-1.5% run-to-run mismatches
in the Python smoke tests on the GitHub Ubuntu runners. Both have the
same shape: a class member declared without an initializer, copied into
a working buffer that the algorithm then assumes is zeroed.

(1) TGS — initTriGridField had four no-op expression statements where
    it intended to zero-initialize the Eigen members of empty_trigrid_node_:
        empty_trigrid_node_.normal;            // evaluates, drops the lvalue
        empty_trigrid_node_.mean_pt;
        empty_trigrid_node_.singular_values;
        empty_trigrid_node_.eigen_vectors;
    Eigen vectors/matrices do not zero-init by default; the four members
    held stack garbage. empty_trigrid_node_ was then copied into every
    cell of trigrid_field_, seeding the whole field with that garbage.
    Replaced with explicit Eigen::Vector3f::Zero() / Matrix3f::Zero()
    assignments.

(2) AOS — `class Point` declared `int label;` (and `float x,y,z; uint idx;`)
    without in-class initializers. range_mat_ entries got assigned
    fresh Point copies in sphericalProjection, leaking uninit `label`
    fields into the working set. `class AOSNode` had the same problem
    on its `start` / `end` members. Combined, they made cluster
    filtering decisions in labelPointcloud run-to-run nondeterministic
    (~1-1.5% of points flipped between "in some cluster" and "no cluster"
    on consecutive segment_objects calls with identical input).
    Fixed with brace-init (`label{0}`, `start = 0`, etc.).

Verified:

* cpp/tests/regression_kitti against cpp/tests/data/kitti00_000000_gold.bin
  -> REGRESSION PASS: bit-identical (374008 bytes). The KITTI sequence 00
  frame 0 output with seed=42 is byte-for-byte the same as the gold dump
  captured pre-fix. Both fixes therefore *only* remove nondeterminism;
  the deterministic majority result that was already produced is
  preserved.

* python/tests/test_smoke.py::test_xyz_and_xyzi_inputs_both_accepted
  and python/tests/test_smoke.py::test_class_api_matches_convenience_function
  ran 30 times back-to-back on macOS arm64 — 30/30 passed (the two had
  been flaking ~1-1.5% on the Ubuntu CI runners).

* Full pytest python/tests/ -> 7/7 passed.
@LimHyungTae
LimHyungTae force-pushed the fix/tgs-uninit-memory branch from 118604d to 9fb65e1 Compare May 9, 2026 09:44
@LimHyungTae LimHyungTae changed the title fix(tgs): zero-init TriGridNode Eigen members in initTriGridField fix: zero-init TGS empty_trigrid_node_ + AOS Point/AOSNode (root-cause CI flakes) May 9, 2026
@LimHyungTae
LimHyungTae merged commit 19bbe67 into main May 9, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant