This document is relevant for: Trn2, Trn3
Vision Encoder Parallelism#
Overview#
The vision encoder supports independent TP and DP configurations, decoupled from the text model’s parallelism. Vision TP shards encoder weights across ranks; Vision DP scatters vision blocks across ranks for independent processing, then all-gathers after the merger.
Prefill-only. Vision encoding runs during prefill. Decode steps do not invoke the vision encoder.
Block-level DP. Unlike text model DP which shards by batch, vision DP shards by the num_blocks dimension of the block-packed vision inputs, where each block contains at least one image. Each DP rank processes num_blocks / dp_size blocks independently, then results are gathered.
Configuration#
VisionNeuronConfig(
tp_size=1, # Vision TP degree (default: 1, no weight sharding)
dp_size=4, # Vision DP degree (default: inferred as world_size // tp_size)
)
Parameter |
What it controls |
Default |
|---|---|---|
|
Weight sharding across TP ranks |
1 |
|
Block scatter/gather across DP ranks |
Inferred: |
Constraints:
tp_sizemust be a positive integer that dividesworld_sizetp_size * dp_size == world_size(all ranks must participate — no redundant compute)num_blocksis padded to be divisible bydp_sizefor even scatter
Resolution rules (resolve_tp_dp):
Only
tp_sizeset →dp_sizeinferred asworld_size // tp_sizeNeither set (defaults
tp_size=1, dp_size=1) → inferred totp_size=1, dp_size=world_size(full DP)Both set explicitly → validated that
tp_size * dp_size == world_size
resolve_tp_dp is called both at worker init (for process group creation) and at model runner init (for warmup and runtime block padding). It writes back the resolved dp_size to the config instance so downstream callers see the correct value.
Modes#
User config |
Resolved (world_size=16) |
Behavior |
|---|---|---|
(default) |
tp=1, dp=16 |
Full DP — each rank holds full weights, processes |
|
tp=4, dp=4 |
Combined — weights sharded across 4 TP ranks, blocks scattered across 4 DP ranks |
|
tp=16, dp=1 |
Full TP — weights sharded across 16 ranks, all ranks process all blocks |
|
tp=4, dp=4 |
Same as specifying only |
Process Groups#
Vision TP group#
Created when vision_tp_size != text_tp_size. Each group contains vision_tp_size contiguous ranks.
Example: world_size=16, vision_tp=4
Vision TP groups (4 groups of 4):
[0,1,2,3] [4,5,6,7] [8,9,10,11] [12,13,14,15]
When vision_tp_size == text_tp_size, the text TP group is reused (no new group created).
Vision DP group#
Created when vision_dp_size > 1. Column groups built via _build_dp_column_group_ranks(world_size, vision_tp_size, vision_dp_size) — ranks at the same TP position across DP replicas.
Example: world_size=16, vision_tp=4, vision_dp=4
Vision TP groups: [0,1,2,3] [4,5,6,7] [8,9,10,11] [12,13,14,15]
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
Vision DP columns: [0,4,8,12] [1,5,9,13] [2,6,10,14] [3,7,11,15]
Example: world_size=8, vision_tp=1, vision_dp=4 (full DP, no weight sharding)
Vision TP groups: [0] [1] [2] [3] [4] [5] [6] [7] (each rank = own TP group)
Vision DP columns: [0,1,2,3] [4,5,6,7] (4 ranks per DP group)
Constraint: tp_size * dp_size == world_size#
Vision TP and DP must fully utilize all ranks — no redundant compute. At runtime, resolve_tp_dp enforces tp_size * dp_size == world_size. If the user specifies only tp_size, dp_size is inferred as world_size // tp_size. If neither is specified, defaults to full DP (tp_size=1, dp_size=world_size).
Forward Flow#
Input: pixel_values [num_blocks, block_size, patch_dim]
(+ pos_emb_idx, pos_emb_weight, cos, sin, bound_min, bound_max, unpack_indices)
Step 0 — DP scatter (if dp_size > 1):
blocks_per_rank = num_blocks // dp_size
Each rank slices its portion: [start:end] along dim 0
pixel_values: [num_blocks, ...] → [blocks_per_rank, ...]
Step 1 — Patch embedding + position embedding:
pixel_values → hidden_states [blocks_per_rank, block_size, hidden]
Step 2 — Transformer blocks + deepstack extraction:
For each layer: attention + MLP (TP collectives within vision TP group)
At deepstack layers: extract merged features
Step 3 — Final merger:
hidden_states → main_merged [blocks_per_rank, merged_bs, D]
Step 4 — DP gather (if dp_size > 1):
all_gather(main_merged, dim=0) across vision DP group
main_merged: [blocks_per_rank, ...] → [num_blocks, merged_bs, D]
Same for deepstack features.
Step 5 — Unpack to per-image order:
index_select with unpack_indices → [merged_seq_len, D]
(Operates on full gathered tensor — same on all ranks)
Step 6 — Construct fat tensor (main + deepstack):
Concatenate main and deepstack features → final output
Block Padding#
num_blocks is padded to be divisible by dp_size at three points:
Config
__post_init__:num_total_vision_attention_blockspadded for compiled graph shapesselect_vision_bucket: runtime bucket selection padsnum_blocksVision warmup (
neuron_worker.py): warmup shapes padded
Padding blocks contain zeros and produce zero outputs. The unpack_indices tensor ensures only valid merged tokens are selected in the final output.
Weight Loading#
Vision DP does not affect weight loading — each DP rank holds a full copy of the vision encoder weights (dp_size > 1 reduces activation memory, not weight memory).
Vision TP shards weights across the vision TP group using the same column/row parallel patterns as the text model, but scoped to the vision TP group.
When to Use Block-Level DP#
Large images / many images per request: High
num_blocksmakes the per-rank activation memory the bottleneck. DP reduces activation memory linearly withdp_size.Small vision encoder: When the encoder fits in a single device’s weight memory, TP provides no benefit. Full DP (
tp_size=1) avoids TP communication overhead entirely.Combined TP+DP: For very large encoders that don’t fit on one device AND have high block counts, use both.
Block-Level DP vs. Model-Level DP#
The current implementation is block-level DP: blocks from a single embed_multimodal call are scattered across ranks within one compiled graph. All ranks participate in every encoder invocation and the result is all-gathered before returning.
Model-level DP is a different approach: multiple independent encoder replicas, each handling separate requests (or subsets of images) concurrently with no communication between replicas.
Aspect |
Block-level DP (current) |
Model-level DP (future) |
|---|---|---|
Scope |
Within one |
Across concurrent calls |
What’s distributed |
Blocks from one batched image set |
Entire requests to independent replicas |
Communication |
All-gather after compute |
None between replicas |
Compiled bucket |
One bucket per call (all images share it) |
Each replica selects its own bucket |
Latency |
Reduces single-call latency |
Does not reduce single-call latency |
Throughput |
Increases via cross-request batching |
Increases via parallel independent calls |
Cross-request batching#
The scheduler already batches images from multiple requests into a single embed_multimodal call (via group_and_batch_mm_kwargs). This means block-level DP naturally benefits from concurrent request load — more pending images → more blocks → better DP utilization.
Where model-level DP adds unique value#
Block-level DP has a fundamental constraint: all images in one call must use the same compiled bucket. When a small image (256 tokens → bucket 1024) and a large image (4096 tokens → bucket 4096) are batched together, the small image pays the latency cost of the larger bucket.
Model-level DP solves this by routing images to replicas with different optimal buckets:
Block-level DP (current):
Small + Large images → both use bucket 4096 → wasted compute on small image
Model-level DP (future):
Replica 0: Small image → bucket 1024 (fast)
Replica 1: Large image → bucket 4096 (parallel)
This document is relevant for: Trn2, Trn3