Skip to content

Bulk uploads ingest supplied supervoxel and root id columns - #271

Merged
fcollman merged 4 commits into
masterfrom
upload-supplied-seg-ids
Oct 30, 2026
Merged

fcollman merged 4 commits into
masterfrom
upload-supplied-seg-ids

Conversation

@fcollman

Copy link
Copy Markdown
Collaborator

Bulk uploads can now take supervoxel and root id columns from the source file, and skip the lookups those columns make unnecessary.

What changes for users

In step 2 of the upload wizard, each bound spatial point (pre_pt, post_pt on a synapse table) gets two optional rows: <point>_supervoxel_id and <point>_root_id. Plain spatial points (ctr_pt) don't get them, because they have no segmentation columns. These rows only appear on deployments that run Ray uploads; a Celery upload with these columns mapped is refused at start.

When a root column is mapped, step 3 asks when the roots were looked up: at one time, sometime between two times, or not known. Step 4's preview checks the first rows' ids against the chunkedgraph and reports how many match, which catches a column mapped to the wrong point.

How it works

  • Separate segmentation CSV: processing writes the supplied ids to a second CSV (<processed>.segmentation.csv), and a new load_segmentation stage imports it into the segmentation table with Cloud SQL.

  • Exact ids: CSV id columns are read as text, and nullable parquet integers as Python ints, so 64-bit ids aren't rounded through float64. A file that stores ids as large floats is refused.

  • Stages per case:

    Supplied Spatial lookup resolve_roots
    nothing as before skipped
    supervoxels skipped for those points looks up their roots
    supervoxels + roots skipped checks every root, looks up only the stale ones
    roots only runs for that point checks the roots against the new supervoxels
  • resolve_roots:

    • Chunking: it works through the segmentation table in ranges of about 20,000 rows. Ranges follow the ids present, so sparse ids don't make empty chunks.
    • Per chunk: it fills blank supervoxels from the point's position, checks supplied roots, and looks up missing and stale roots. Only changed rows are written.
    • Checkpoints: chunks are recorded in a new upload_root_chunks table. It is a new table only, so the schema version stays 1. Chunks also pause for frozen materialization like spatial chunks do.
  • Every supplied root is checked at the job's timestamp, whatever time the user gives. The table is published as correct at that timestamp, and the live update only fixes roots that expire after it, so a root already stale then would otherwise stay wrong. A root is current only if it's a top-layer id that existed and had no new parent before the timestamp. That is kvdbclient 0.9's is_latest_roots rule, written out here because kvdbclient ≥0.8 needs Python 3.13. Older kvdbclient counted ids that never existed as current.

  • Parallel lookups: chunkedgraph calls go in pieces of 1,000 ids, 8 at a time per task (UPLOAD_ROOT_THREADS). kvdbclient sends one Bigtable request at a time. On a 1-CPU pod, threading took get_roots from 270 to 1,548 supervoxels a second, and is_latest_roots from 1,523 to 15,136 roots a second.

  • Also fixed: the root lookup's timestamp parsing failed when the microseconds were 0.

Tested on ltv7

The test files are the 1M synapse file, with ids taken from synapses_1m_inprod6. For the root files, the roots were either current or from 90 days earlier:

Upload File Roots checked / stale Roots looked up resolve_roots Submit → publish
synapses_1m_sv3 supervoxels – 2,028,544 5m18s* 9m
synapses_1m_root_now1 + current roots 2,028,542 / 161 163 51s 4m
synapses_1m_root_90d1 + roots from 90 days earlier 2,028,542 / 40,723 40,725 53s 4m
synapses_1m_root_90d2 (final image, stricter root check) + roots from 90 days earlier 2,028,542 / 40,740 40,742 2m20s† 7m

† On a cold cluster; 90d1 ran on workers left up by the run before it.

* About 4 minutes of that was the Ray autoscaler adding workers. Once they were up, the last 48 of 51 chunks finished in about 80 seconds. For comparison, the normal path (spatial lookup) for the same file is 9m07s.

In every table:

  • the supervoxels match the file;
  • every root equals get_roots(supervoxel, job timestamp);
  • the indexes match the model;
  • no temporary upl… tables are left over.

The 90-day file's stale roots match the count built into the file (40,603), plus newer edits.

Unit and integration tests: 25 new tests in tests/test_upload_supplied_ids.py, and the full suite passes (820 passed, 2 skipped).

Notes

  • CLUSTER: a source with its own unsorted ids still gets its segmentation table clustered by finish_tables, exactly like the annotation table.
  • Rewrites: a supervoxels-only upload updates nearly every segmentation row once in resolve_roots, so the CLUSTER in finish_tables rewrites the table. With supplied roots, only stale rows change.

🤖 Generated with Claude Code

fcollman and others added 4 commits October 29, 2026 14:06
A source may carry <point>_supervoxel_id and <point>_root_id columns for its bound
spatial points. They are written to a second processed CSV and imported into the
segmentation table (load_segmentation), and the upload skips the lookups they make
unnecessary:

- points with supplied supervoxels skip the spatial lookup; their roots are looked
  up by id range of the segmentation table (resolve_roots, checkpointed in
  upload_root_chunks)
- supplied roots are all checked with is_latest_roots at the job's timestamp
  (and for being top-layer ids); stale ones are looked up again
- blank supervoxels in a supplied column are looked up from the point's position

Ids are read as text (CSV) or exact Python ints (nullable parquet) so 64-bit ids
are not rounded through float64; ids as large floats are refused.

The wizard offers the optional id columns for bound spatial points (Ray uploads
only), asks when supplied roots were looked up, and the preview checks the first
rows' ids against the chunkedgraph. Also fixes the root lookup's timestamp parse
when the microseconds are 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…chunks for 8 CPUs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…000 ids

kvdbclient reads one Bigtable request at a time, so a task mostly waited (~270
supervoxels/s). PyChunkedGraph splits reads into pieces of 1000 keys on threads; doing
the same per task gave, on a 1-CPU pod on ltv7: get_roots 270/s -> 1548/s (8 threads),
2457/s (32); is_latest_roots 1523/s -> 15136/s (8). UPLOAD_ROOT_THREADS sets it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…9's is_latest_roots

kvdbclient before 0.8 counts any id without a NewParent cell as latest, so a root-shaped
id that never existed passed the supplied-root check. 0.9 also requires Child cells
before the timestamp (PyChunkedGraph's rule), but 0.8+ needs Python 3.13 and the image
is on 3.12, so the rule is read here with the same read_nodes call. On ltv7 it agreed
with the old check on 19,769 real roots and refused a fabricated one, ~13% slower.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@fcollman
fcollman merged commit a674b80 into master Oct 30, 2026
2 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