tinyexp/examples/vit_tp_exp.py trains and evaluates DeiT-S with tensor
parallelism (TP), implemented against the official DeiT sources line by line
(对拍): only formatting-level changes are allowed, and every deviation from upstream is
listed below and proven numerically equivalent by tests. This page is the complete
plan-and-record home for the example: ported sources, sanctioned deviations, usage,
and the four-layer cross-check record (numerics, eval accuracy, full-training
accuracy, throughput).
Tensor parallelism shards attention on head boundaries, so num_heads must be
divisible by the TP size. DeiT-Ti has 3 heads — structurally unusable at TP=2. DeiT-S has
6 heads (3 per rank at TP=2), 22,050,664 parameters, and the official 300-epoch recipe
reaches 79.8% top-1 on ImageNet val, which is the accuracy cross-check target.
| Component | Source |
|---|---|
Model (VisionTransformer/Block/Attention/Mlp/PatchEmbed) |
timm vision_transformer implementation configured for timm 0.3.2 DeiT behavior; Attention is adapted locally to split QKV for TP |
| Factory hyper-parameters, checkpoint URL | facebookresearch/deit models.py @ 7e160fe4 |
Train/eval loops, recipe defaults, transforms, RASampler, MetricLogger |
deit engine.py/main.py/datasets.py/samplers.py/utils.py (same commit) |
Data/loss/optimizer helpers (Mixup, create_transform, NativeScaler, create_scheduler, …) |
timm >= 1.0 (same algorithms) |
TP itself is torch.distributed.tensor.parallel (ColwiseParallel/RowwiseParallel)
wrapped by TPAccelerator, following the classic Megatron block layout: colwise on
attn.q/k/v + mlp.fc1, rowwise on attn.proj + mlp.fc2, one all-reduce per block
pass. Attention runs per-head locally with no communication inside, using PyTorch's
scaled_dot_product_attention (FlashAttention backend on supported CUDA devices).
- D1 — the official fused
attn.qkvLinear is split intoq/k/vso colwise sharding lands on head boundaries.convert_fused_qkv_to_split/convert_split_qkv_to_fusedtranslate weights; checkpoints always store the official fused layout and load official DeiT checkpoints directly. - D2 —
Attention.forwardreshapes use-1(local-shape aware), as torch'suse_local_output=TrueTP pattern requires; at tp=1 the shapes resolve to exactly the official ones. - D3 —
mode=benchis an added capability (synthetic throughput/memory check); it does not touch the training semantics. - D4 — EMA (official default on) is disabled at tp>1: an EMA copy holds full weights and per-step gathering from DTensors is expensive.
- D5/D6 — under TP all ranks consume identical batches and share one RNG seed (TP
collectives require identical inputs; the official
seed + rankis the DDP data-sharding context). - D7 — optional Redis byte cache for the training set
(
redis_cfg.redis_cache_enabled, default off; ray-managed Redis, no external wrapper needed): the cache stores raw JPEG bytes verbatim, so samples are bit-identical todatasets.ImageFolder; the first epoch fills the cache from disk, later epochs read from memory.safe_setfailures (e.g. maxmemory full) fall back to disk reads — correctness never depends on the cache. - D8 — the train dataloader raises
ValueErrorwhen it has no batch (per-rank samples <train_batch_size_per_devicewithdrop_last=True). Upstream would silently train zero steps and then fail on an empty metric average; valid configurations are unaffected.
Full recipe (2 GPUs, TP=2, official defaults: 300 epochs, AdamW wd 0.05, cosine with 5-epoch warmup, mixup 0.8/cutmix 1.0, repeated augmentation):
python -m tinyexp.examples.vit_tp_expEval-only against the official checkpoint (accuracy cross-check, expects 79.8 ± 0.1):
python -m tinyexp.examples.vit_tp_exp mode=eval \
module_cfg.pretrained_from=https://dl.fbaipublicfiles.com/deit/deit_small_patch16_224-cd65a155.pthThroughput / memory benchmark (compare against DDP by overriding the accelerator):
python -m tinyexp.examples.vit_tp_exp mode=bench
python -m tinyexp.examples.vit_tp_exp mode=bench accelerator_cfg.accelerator=ddpCPU smoke test (no ImageNet, ray 2 workers x 2 CPUs, gloo TP=2). The batch overrides are required:
fake_data has 64 samples and the default batch of 256 with drop_last=True yields no training step:
python -m tinyexp.examples.vit_tp_exp dataloader_cfg.fake_data=true dataloader_cfg.input_size=32 \
dataloader_cfg.train_batch_size_per_device=8 dataloader_cfg.val_batch_size_per_device=8 \
module_cfg.img_size=32 module_cfg.num_classes=10 epochs=1 max_train_steps=4 \
ray_cfg.ray_num_gpus_per_worker=0.0 ray_cfg.ray_num_cpus_per_worker=2Full 300-epoch training on many GPUs (e.g. 8×H200/H800 single node, DDP — DeiT-S has 6 heads and cannot shard over TP=8), with the Redis byte cache for slow shared filesystems (D7; the ray mixin starts the Redis shards itself):
python -m tinyexp.examples.vit_tp_exp accelerator_cfg.accelerator=ddp \
ray_cfg.ray_num_worker=8 redis_cfg.redis_cache_enabled=true redis_cfg.redis_cache_max_memory=300Recipe note (global batch / lr): the official linear rule lr × global_batch / 512
is applied verbatim — the default 2×256=512 gives lr 5e-4 (the same scaling point as
the official 1024 @ 1e-3), and the 8×256=2048 H200 run below used lr 2e-3.
ImageNet-1k in the standard ImageFolder layout, located via IMAGENET_HOME or
dataloader_cfg.data_root (default ./data/imagenet/):
<root>/train/<wnid>/*.JPEG # 1.28M images, 1000 class dirs (full training)
<root>/val/<wnid>/*.JPEG # 50k images (eval-only cross-check)
Four layers, each with a hard pass criterion; all hit.
- L1 — numerical equivalence (CI, CPU+gloo): TP=2 forward logits and one-step
gradients match the single-process run at fp32
allclose(atol=1e-5)(tests/examples/test_vit_tp_exp.py), and the split-qkv forward matches the fused official one atatol=1e-6. - L2 — eval accuracy (official checkpoint, full 50k val): 79.82% top-1 / 94.95% top-5 at tp=1, 79.81% / 94.94% at tp=2 (official reference 79.8; tp=1 vs tp=2 differ by 0.01%).
- L3 — full-training accuracy (300-epoch official recipe from scratch, 8×H200
DDP via rjob, global batch 2048 / lr 2e-3, Redis byte cache on): best top-1
79.84% / top-5 94.99% in 17h56m (~3.5 min/epoch incl. full-val each epoch) —
paper reports 79.8 / ~95.0. Trajectory: ep45 58.6 → ep113 69.7 → ep181 74.1 →
ep249 78.4 → ep299 79.84. Artifacts:
output/vit_tp_l3_h200/{last,best}.ckpt(official fused-qkv layout), per-epochlog.txt. - L4 — throughput/memory (bench mode, table below).
Two engine hardenings landed with this example, each held by a regression test:
DDPAccelerator.reduce stages CPU tensors through the device so nccl
metric-sync works at epoch boundaries, and store_and_run_exp resolves the
canonical importable twin of a python -m __main__ class so ray ships it by
reference (robust against cluster agents that replace builtins.print after
import).
Redis cache behavior measured on the L3 run: RASampler(num_repeats=3) visits
~1/3 unique indices per epoch, so the cache fills over ~3 epochs to the full
1,281,167-key train set (6 shards × 213,528); once warm, data time drops to
~0.6 ms/step and steps run at ~0.25 s.
| Config | batch/rank | step latency | effective img/s | peak mem/rank |
|---|---|---|---|---|
| DDP=2 | 256 (its ceiling; 288 OOM) | 294.7 ms | 1737 | 13.44 GB |
| TP=2 | 256 | 841.0 ms | 304 | 8.61 GB |
| TP=2 | 384 (its ceiling; 512 OOM) | 1262.5 ms | 304 | 12.87 GB |
Honest reading: at DeiT-S scale TP is not a throughput win (communication dominates;
~17% of DDP). What TP buys is per-device memory — a 1.5× larger per-rank batch ceiling
(384 vs 256) on the same 16 GB cards. The correctness gate is met: evaluating the official checkpoint on the full 50k
ImageNet val yields 79.82% top-1 / 94.95% top-5 at tp=1 and 79.81% / 94.94% at
tp=2 (official reference: 79.8%); numerical equivalence of TP=2 vs single-rank
forward/backward is asserted in CI (fp32 allclose, tests/examples/test_vit_tp_exp.py).