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 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.
Python
Ingestion streams pa.RecordBatches of raw frames + annotations directly into a Lance table — 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 UDFs. Backfills are incremental and checkpointed: re-running the command after new footage arrives only computes the new rows.
Python
Run the backfill against the live table:
Python
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 the Geneva end-to-end example for more on the backfill pattern.

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 — the link between a checkpoint and the exact data that produced it.
Python

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.
Python
The collate function decodes JPEG bytes and converts BDD category strings into COCO class IDs (so the comparison against the pretrained checkpoint is valid):
Python
Wire it into a standard torch.utils.data.DataLoader:
Python
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.
Python

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.
Python
To reproduce a run, time-travel the view to the recorded version before opening the loader:
Python

7. Continuous updates

When new footage arrives, the same three calls update every downstream view — no view definitions change, no training-script edits required:
Python
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.