Skip to main content
This example walks through fine-tuning an autonomous vehicle (AV) perception model on targeted failure-mode slices of BDD100K (riders, nighttime pedestrians, and distant pedestrians) using LanceDB as a single multimodal table from raw JPEG bytes through to the PyTorch training loop. The full pipeline lives in the lancedb/training repository. This page focuses on the parts most relevant to training: defining curated splits as materialized views, loading them through the Permutation API, and pinning checkpoints to an exact data version.

What you get

Fine-tuning Faster R-CNN ResNet50 FPN v2 for 10 epochs on each curated slice (batch size 64, AMP, A100), starting from the same COCO-pretrained checkpoint and evaluating on the matching validation view: No external data added, only training-distribution correction via SQL filters over a single Lance table. Each panel below shows the same frame with three overlaid predictions: green = ground truth · red = pretrained COCO baseline · blue = fine-tuned model. Rider detection: ground truth vs baseline vs fine-tuned Nighttime pedestrian detection: ground truth vs baseline vs fine-tuned Distant pedestrian detection: ground truth vs baseline vs fine-tuned The rest of the page walks through the pipeline that produced these checkpoints.

The failure modes

A perception model fine-tuned on a generic dataset typically misses the long-tail scenarios that matter most in deployment. Three common failure modes drive this example: Each curated slice becomes a Geneva materialized view (a named, refreshable SQL filter over the source table), and the training script loads it by name. New footage flows in through add() → backfill() → refresh(); no manifests, no exports, no reshuffling on disk.

1. Schema

The source table holds raw image bytes alongside structured annotations. Bounding boxes are stored as a parallel list (one element per box) rather than a nested struct so they remain directly queryable with SQL.
Ingestion streams pa.RecordBatches of raw frames + annotations directly into a Lance table, with no intermediate preprocessing job. The table can live on local disk, S3, GCS, or Azure; everything downstream (backfills, views, the training loader) opens it in place via lancedb.connect("s3://...") with no local copy step.

2. Backfill curation features with Geneva

Curation signals are added as columns on the same table using Geneva backfilling. Backfills are incremental and checkpointed: re-running the command after new footage arrives only computes the new rows.
Run the backfill against the live table:
Because the curation features are flat scalar columns on the same table, all four retrieval modes (SQL, full-text search, vector search, and SQL-filtered vector search) work directly without joins or exports. See Geneva backfilling for how a backfill fills only the rows that lack values.

3. Define training splits as materialized views

A training split is a named SQL filter, not a CSV manifest. Each view stays in sync with the source table and bumps its version on every refresh. That version is the link between a checkpoint and the exact data that produced it.

4. PyTorch DataLoader via the Permutation API

The training script doesn’t know about the filter: it opens a view by name and reads through the Permutation API. Each DataLoader worker reopens its own connection lazily, reads Arrow batches directly from Lance (zero-copy, no intermediate file format), and the collate function decodes the whole batch in one pass. Permutation provides random-access indexing over the table, so shuffling is a cheap pointer rewrite rather than a full-dataset shuffle on disk.
The collate function decodes JPEG bytes and converts BDD category strings into COCO class IDs (so the comparison against the pretrained checkpoint is valid):
Wire it into a standard torch.utils.data.DataLoader:
with_format("arrow") keeps batches as zero-copy pa.RecordBatches: no per-row Python boxing, no pickling between worker and main. Each DataLoader worker reopens its own Permutation after fork (the Rust async handle is cleared in __getstate__), so reads scale with num_workers and stream straight from the underlying object store. JPEG decode overlaps with GPU compute via pin_memory + prefetch_factor, which is what keeps the loader from becoming the bottleneck on a fast GPU.

5. Fine-tune Faster R-CNN

The training loop is plain PyTorch: the Lance integration ends at the loader. Mixed precision is enabled on CUDA for ~2× speedup on Ampere GPUs.

6. Pin the checkpoint to a data version

Every Lance table, including a materialized view, exposes a monotonically increasing version. Logging it next to the weights gives a permanent, deterministic link between a checkpoint and the exact data snapshot that produced it.
To reproduce a run, time-travel the view to the recorded version before opening the loader:

7. Continuous updates

When new footage arrives, the same three calls update every downstream view (no view definitions change, no training-script edits required):
The next training run picks up the new data automatically and pins itself to the new version.

Full source

The complete code, including a synthetic-data mode for pipeline verification (--synthetic 500), GPU UDFs for CLIP embeddings and dHash deduplication, and the EDA notebook, is in this GitHub repository.