Files
lancedb/docs/web/training/distributed-training.mdx
T
Jack Ye ddb318567a docs: complete SQL reference, enrich Functions drafts, rename OSS to Embedded (#4732)
## Summary

- Complete the SQL reference: `ATTACH DATABASE`, the `CHANGES` clause,
and
`dedupe()`/`duplicate_pairs()` (verified against current sophon source;
these were recently renamed from
`vector_dedup`/`vector_duplicate_pairs`).
  Fix the blob-column default-projection claim and add the previously
  undocumented `blob_descriptor()`.
- Enrich the gated Functions draft (still `.mintignore`'d, pre-release):
  chained Functions over blob outputs, GPU-produced blobs, Secrets,
  multi-input/output binding, `code=[module]` packaging.
- Upgrade the materialized-view and computed-column auto-refresh drafts
with
  precise, verified catch-up semantics, and add the missing `ALTER
  MATERIALIZED VIEW` / `CREATE ... WITH(...)` sections (also gated — the
  automatic-refresh scheduler is still dev/staging-only).
- Thread one running example (video frames → GPU extract/caption → dedup
→
auto-refreshing materialized view) through Functions, materialized
views,
and the LeRobotDataset integration guide instead of a separate tutorial.
- Rename "LanceDB OSS"/"OSS" to "LanceDB Embedded"/"Embedded" throughout
  (52 files); activate the existing availability-comparison mechanism on
  `basics/offerings.mdx` for a full feature-by-feature comparison table.
2026-10-29 10:16:03 -07:00

217 lines
9.4 KiB
Plaintext

---
title: "Distributed Training"
sidebarTitle: "Distributed Training"
description: "Shard StreamingDataset across ranks and workers, with elastic determinism and checkpointing."
icon: server
availability:
embedded: available
enterprise: available
---
To understand the wide range of scenarios handled by [`StreamingDataset`](/training/data-loading) across a
distributed job, consider a model trained with stochastic gradient descent (SGD) and distributed data parallelism
(DDP). In this example, we need to load batches onto multiple GPUs across multiple servers. After each batch is
processed, the GPUs exchange weights and the next batch is loaded. We fix the following terminology:
- **World size** - The number of GPUs being loaded simultaneously. For example, if we have $2$ servers with $4$
GPUs each, the world size is $8$.
- **Rank** - The identifier of an individual GPU. It is an integer in the range `[0, world_size)`.
Each rank is allocated a disjoint portion of the source data.
- **Batch size** - The number of rows processed by one GPU in a single training step. For example, $128$.
- **Global batch size** - The total number of rows processed across all GPUs in a training step (sometimes called a "minibatch" in machine learning literature). For
example, if we have $8$ GPUs with batch size $128$, the global batch size is $1024$.
Other concepts, such as read batch size and `num_workers`, are introduced in [PyTorch Data Loader](/training/data-loading).
## Parallelism & Elasticity {#parallelism-&-elasticity}
When loading data, `StreamingDataset` partitions the source dataset among disjoint parallel threads called **splits**. Each rank is allocated a contiguous group of splits, and each DataLoader worker associated to a rank gets a contiguous subgroup of its rank's splits.
If using `shuffle=False`, data is allocated to splits naively in ordered contiguous blocks of equal size. With `shuffle=True`
(the default), the rows of the source dataset are shuffled and distributed among splits such that each split receives the same
number of rows; with `shuffle_clump_size` set, they are shuffled in clumps of that many contiguous rows.
The number of splits defaults to `world_size` if `num_splits` is omitted during initialization. This is not recommended, as a later run using a different world size will then construct a different layout and structure of global batches.
To get **elastic determinism**, choose a fixed `num_splits` that is compatible with every topology you plan to use.
For example, a run with `num_splits=8` and $4$ GPUs assigns $2$ splits to each rank. The ranks pull from their splits in
round-robin order, producing the same global batches as a run with $8$ GPUs and the same `num_splits`, `shuffle_seed`,
and `epoch`.
The following constraints apply:
- `num_splits` must be divisible by `world_size`.
- With DataLoader worker processes, `num_splits` must be divisible by `world_size * num_workers`.
- For the same samples to form each global training step across topologies, `global_batch_size` must be a multiple of
`num_splits`.
- The filtered row count must be at least `num_splits`. If it is not evenly divisible by `num_splits`, up to
`num_splits - 1` surplus rows are dropped so that every split has the same length.
For example, to switch between $8$ and $6$ GPUs, use a common multiple such as $24$ for `num_splits`. Highly composite values can support many layouts; for example, `num_splits = 48` is compatible with $10$ distinct possible GPU counts. Remember to include `num_workers` when checking divisibility.
<CodeGroup>
```py Python icon=Python
import torch.distributed as dist
dist.init_process_group("nccl")
rank = dist.get_rank()
world_size = dist.get_world_size()
# num_splits=48 is divisible by many integers,
# so this dataset works unchanged as you add or remove GPUs.
ds = StreamingDataset(
table,
num_splits=48,
shuffle_seed=42,
epoch=current_epoch,
rank=rank,
world_size=world_size,
)
for sample in ds:
train_step(sample)
```
</CodeGroup>
## Checkpointing {#checkpointing}
`StreamingDataset` provides checkpointing in case your model training fails partway through a run. Use `StreamingDataset.state_dict()` to capture the current number of samples consumed from each split in a Python dictionary, and `load_state_dict()` to restore the last saved position.
A stored `StreamingDataset` state reflects only round-robin cycles that have been completed across all splits; samples completed partway through a cycle at the time of saving can be replayed. To ensure efficient saving, make sure `global_batch_size` is a multiple of `num_splits`, as in the example below.
<CodeGroup>
```py Python icon=Python
import torch
global_batch_size = 48 # one sample per split in each global step
batch_size = global_batch_size // world_size
dataset = StreamingDataset(
table,
num_splits=48,
shuffle_seed=42,
epoch=current_epoch,
rank=rank,
world_size=world_size,
)
dataloader = torch.utils.data.DataLoader(
dataset,
batch_size=batch_size,
num_workers=0,
)
for step, batch in enumerate(dataloader):
train_step(batch)
if (step + 1) % checkpoint_interval == 0:
torch.save(
{"model": model.state_dict(), "dataset": dataset.state_dict()},
f"checkpoint_{step + 1}.pt",
)
# --- resuming after a crash ---
checkpoint = torch.load("checkpoint_100.pt")
model.load_state_dict(checkpoint["model"])
dataset_state = checkpoint["dataset"]
dataset = StreamingDataset(
table,
num_splits=dataset_state["num_splits"],
shuffle_seed=dataset_state["shuffle_seed"],
epoch=dataset_state["epoch"],
rank=rank,
world_size=world_size, # may differ from the run that saved the checkpoint
)
dataset.load_state_dict(dataset_state)
dataloader = torch.utils.data.DataLoader(
dataset,
batch_size=global_batch_size // world_size,
num_workers=0,
)
for batch in dataloader:
train_step(batch)
```
</CodeGroup>
`load_state_dict()` rejects a checkpoint whose `num_splits` or `shuffle_seed` differs from the new dataset. For an
exact resume, also use the same table snapshot, `epoch`, `shuffle`, `filter`, and other data-selection settings. The
`world_size` and number of DataLoader workers may change as long as the split and batch-size divisibility constraints
still hold.
### Checkpointing with Multiple DataLoader Workers {#checkpointing-with-multiple-dataloader-workers}
Python's built-in `torch.utils.data.DataLoader` produces a safe checkpoint when `num_workers = 0`. With `num_workers > 0`, calling `state_dict()` often raises `RuntimeError`. In this case, use `StreamingDataLoader` for multi-worker prefetch and exact resumability. It carries a state snapshot
alongside every internal batch and commits it to the parent `StreamingDataset` only when that batch is returned to the
trainer. The trainer receives the same collated batch it would receive from a standard `DataLoader`.
<CodeGroup>
```py Python icon=Python
from lancedb.streaming import StreamingDataset, StreamingDataLoader
dataset = StreamingDataset(
table,
num_splits=48,
shuffle_seed=42,
epoch=current_epoch,
rank=rank,
world_size=world_size,
)
dataloader = StreamingDataLoader(
dataset,
batch_size=batch_size,
num_workers=4,
)
for step, batch in enumerate(dataloader):
train_step(batch)
if (step + 1) % checkpoint_interval == 0:
torch.save(
{"model": model.state_dict(), "dataset": dataset.state_dict()},
f"checkpoint_{step + 1}.pt",
)
```
</CodeGroup>
`StreamingDataLoader` accepts the same arguments as `torch.utils.data.DataLoader`, with a few restrictions:
- `dataset` must be a `StreamingDataset` (subclasses that override `__iter__` are not supported).
- `in_order = True` is required so that consumer-committed checkpoints stay deterministic.
- `persistent_workers = True` is not supported, because prefetched worker copies cannot be restored from parent-committed state.
- `drop_last = True` is not supported, because incomplete tails discarded by worker replicas cannot be checkpointed topology-independently.
With more than one worker, call `state_dict()` at a complete logical step boundary where every split assigned to the
rank has the same consumed-sample count. Calling it mid-step raises `RuntimeError` asking you to consume more batches
first.
### Resuming Across Different Topologies {#resuming-across-different-topologies}
When training across ranks, each rank owns its own subset of splits and only its own splits have exact progress. To
resume on a different `world_size`, collect the `state_dict()` from every rank of the previous run and merge them with
`StreamingDataset.merge_state_dicts` before calling `load_state_dict()` on the new run. The merge is topology-agnostic:
pass the full list of per-rank states in, and hand the identical merged dict to every rank of the resumed job,
regardless of whether the topology grew, shrank, or stayed the same.
<CodeGroup>
```py Python icon=Python
# On each rank of the previous run:
state = dataset.state_dict()
# ...gather `state` from every rank into a single list `states` (for example
# via torch.distributed all-gather or by saving one file per rank).
merged = StreamingDataset.merge_state_dicts(states)
# On each rank of the resumed run (world_size may differ):
dataset = StreamingDataset(
table,
num_splits=merged["num_splits"],
shuffle_seed=merged["shuffle_seed"],
epoch=merged["epoch"],
rank=rank,
world_size=new_world_size,
)
dataset.load_state_dict(merged)
```
</CodeGroup>