Repository navigation
Bulk upload: parallel CSV processing, cloud-location sources, faster spatial lookup - #257
Merged
Merged
Conversation
process_chunk built every spatial point through chunk.iterrows() and a Shapely Point, three points per row: about a billion Python calls for the 337M-row minnie synapse upload, which took 21.5 h in process_csv. encode_points_wkb_hex writes the EWKB POINT Z hex for a whole column with NumPy. The CSV output is byte-identical to the per-row path (49x faster on 1M rows of the real file), including rows dropped for missing coordinates. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
process_csv ran a whole upload in one task, one chunk at a time; the 337M-row minnie synapse file took 21.5 h, outlived the 6 h visibility timeout and was redelivered and imported twice. process_csv now plans ~128 MB byte ranges that each start on a line (UPLOAD_CSV_PART_SIZE_MB), runs process:process_csv_part for each on the process queue, and joins the parts in order with GCS compose into the CSV upload_to_database imports. Each part runs for about a minute and overwrites its own output, so a retry or redelivery is safe. Generated ids continue across parts from per-part row counts (process:count_csv_part). A file of one part, or one that cannot be split by lines (a quoted field holding a newline, blank lines; each part compares rows read with lines), still goes through the single-stream path. Cancelling revokes waiting parts; a failed or cancelled run deletes the parts it wrote. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The wizard's step 1 can take a cloud location instead of a browser upload. POST /api/source/inspect/<datastack> checks that the file can be read and returns its size and first rows for the column mapping; /api/process/start takes the location as sourceUri. All CSV reads go through upload/source.py: a gs:// object is read with the deployment's storage client (any object its service account can read, so every public one), and an s3:// object with unsigned requests to the bucket's region. Processed output is still written to the upload bucket. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…oss tasks On ltv7 (2026-10-24) a 4.8M-row synapse upload spent an hour in the spatial lookup, ~1.5 s per 50-point sub-batch: - Supervoxels (39%): scattered_points downloads and gunzips the whole 256x256x32 chunk again for every point, and a sub-batch's points were spread over a 4201-unit grid cell, so 48-50 of 50 hit different chunks: 9.6M downloads (~2.9 TB) for 1.3M distinct chunks. lookup_supervoxel_ids fetches each chunk once with CloudFiles and reads every point in it (compressed_segmentation/compresso/crackle read the voxel directly), and process_chunk now splits a spatial chunk's points, pre and post together, by storage chunk, so no chunk is split across sub-batches. Same labels as scattered_points; ~11x less CPU per point on real minnie chunks. The target is now 500 points per sub-batch (~45 chunks). - Root-id setup (51%): every celery task runs in an app context whose teardown disposed all engines, so each sub-batch opened new connections: ~60/s, holding the 2-vCPU Cloud SQL instance at 100% CPU, and the existing-roots query took 0.77 s instead of ~15 ms. Tasks declared with keep_db_connections=True (the spatial lookup tasks, which only use the staging and aligned-volume databases) now keep their pools. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fcollman
added a commit
that referenced
this pull request
Oct 25, 2026
The module replaced sys.modules["cloudvolume"] (and chunkedgraph_gateway) with a MagicMock at import time and never restored it, so every test module collected after it got the mock. test_spatial_supervoxel_lookup (#257) builds real CloudVolumes and failed 7 tests on master CI. The mocks now cover only the import of ingest_new_annotations. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Oct 25, 2026
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.
Makes the bulk upload workflow practical for very large files. On 2026-10-22 a 337M-row (25.8 GB) synapse CSV for minnie spent 21.5 h in
process_csv. That ran past the 6 h visibility timeout, so the task was redelivered and imported twice; this helped fill the minnie Cloud SQL disk on Oct 24. The spatial lookup that follows was the next bottleneck.Tested end to end on ltv7 (
vFastCsv1–vFastCsv3) with a 4,795,306-row slice of the same file. CSV processing took 2 min, against an estimated ~25 min single-threaded at the old rate. The spatial lookup took 15 min, against 62 min before this change.Changes
1. Vectorized point geometry (
processor.py)SchemaProcessor.process_chunkbuilt every point throughiterrows()and a ShapelyPoint, three points per row.encode_points_wkb_hexnow writes the EWKB POINT Z hex for a whole column with NumPy. The output is byte-identical to the old path, including rows dropped for missing coordinates. It is 49× faster on 1M real rows.2. Parallel byte-range processing (
gcs_processor.py,tasks.py)process_csvsplits the source into ~128 MB byte ranges that each start on a line (UPLOAD_CSV_PART_SIZE_MB).process:process_csv_part, about a minute per part, well inside the process queue's 300 s visibility timeout. A part overwrites its own output, so a retry or redelivery is safe.upload_to_databaseimports.process:count_csv_part.3. Read the CSV in place from
gs://or publics3://(source.py,api.py, wizard step 1)POST /materialize/upload/api/source/inspect/<datastack>checks that the file can be read and returns its size and first rows for the column mapping. It needs edit permission on the datastack, the same as the browser upload./api/process/starttakessourceUri.4. Spatial lookup: one download per storage chunk, and connection pools kept between tasks (
spatial_lookup.py,database.py,app.py,celery_worker.py)Profiled on ltv7 from the logs of 806 sub-batches:
get_rootsget_rootsscattered_pointsdownloads and gunzips the whole 256×256×32 chunk again for every point. A 50-point sub-batch was spread over a 4201-unit grid cell, so 48–50 of its 50 points hit different chunks: 9.6M downloads (~2.9 TB) for 1.3M distinct chunks. Decompression itself is cheap (gunzip 1.5 ms, voxel read 0.03 ms per chunk).process_chunknow orders a spatial chunk's points, pre and post together, by storage chunk, and builds sub-batches only from whole chunks: a target of 500 points, about 45 chunks.lookup_supervoxel_idsfetches each chunk once and reads every point in it. The labels are identical toscattered_points.scattered_points.teardown_appcontextdisposed all engines. So every ~1.5 s sub-batch opened new connections: ~60 per second, which held the 2-vCPU ltv Cloud SQL instance at ~100% CPU, and the existing-roots query took 0.77 s instead of ~15 ms.keep_db_connections=Truenow keep their pools. These are the two spatial lookup tasks, which only use the staging and aligned-volume databases.DROP DATABASEwhen the version expires, so this is deliberately narrow.Verification on ltv7
gs://cave-ltv-materialization-upload/...using ltv7's service account.vFastCsv3run's segmentation table was compared with thevFastCsv2run's in 364 buckets of 20,000 IDs. Row counts and supervoxel ID sums are identical, and the rows with no root ID match (18 pre, 12 post).Tests
New:
test_upload_processor.py: byte-identical geometry, NaN and string edge cases, the combined[x,y,z]format.test_upload_csv_parts.py: range planning, compose order beyond 32 parts, parts against one stream with explicit and generated IDs, the fallback, and cancel and failure handling.test_upload_source.py: URI parsing, S3 and GCS sources, the inspect endpoint, request validation.test_spatial_supervoxel_lookup.py: on local volumes incompressed_segmentationandrawencodings, labels matchscattered_points, a missing chunk reads as 0, each chunk is downloaded once, chunks are never split across sub-batches, and thekeep_db_connectionsflag works.127 spatial and upload tests pass, and ruff is clean.
Defaults changed
SUPERVOXEL_BATCH_SIZEdefault: 50 → 500. It's now a target number of points; a sub-batch takes whole storage chunks.UPLOAD_CSV_PART_SIZE_MB(default 128).Known follow-ups (not in this PR)
rows_processedcounts rows touched per upsert. Now that a synapse's pre and post points usually share a sub-batch, it stops near half of the point estimate (5.46M of 9.59M when this run finished). It should count points.drop_upload_tables: it fails on staging databases withoutcombined_table_metadata(seen on minnie).🤖 Generated with Claude Code