Dinovol is an implementation of DINO-style self-supervised pretraining on 3D volumes.
The canonical source is
ScrollPrize/villa/dinovol.
Run project commands from the monorepository's dinovol/ directory.
dinov2_evais adapted from dynamic-network-architectures, with minimal changes.- RoPE is adapted from the DINOv3 implementation and extended to support 3D.
This implementation is still incomplete. Pretraining works, but finetuning has not yet been implemented.
Use the v3 preset for new runs, as selected by every example recipe. It uses
plain SwiGLU with a 2x hidden width and no query/key/value projection biases,
axial 3D RoPE, separate local/global CLS output normalization, constant LR after
warmup, constant weight decay and teacher momentum, and KoLeo groups of up to 16.
The v2 preset and configs without model_type retain their older defaults for
checkpoint compatibility. Explicit config values override preset defaults.
To select the new defaults, set model.model_type to v3:
{
"model": {
"model_type": "v3",
"embedding_type": "default",
"global_crops_size": [96, 96, 96],
"local_crops_size": [48, 48, 48]
}
}dinovol_2/configs/pretrain_8_9um.json
is a standalone pretraining config with 128³ global crops and 64³ local crops.
Volumes use pyramid level 0 or 2 to target roughly 8–9 µm voxels, as listed below.
It uses a CNN feeding a width-480 v3 backbone with 10 full-attention transformer
blocks, a 3,456-wide SwiGLU FFN, 10 attention heads, and mixed RoPE. The recipe uses
batch size 12 per GPU, two gradient-accumulation steps, gradient checkpointing,
two global views, and four local views. Pseudo-object losses are enabled; Gram
and point supervision are disabled.
The FFN uses mlp_ratio: 7.2 (3,456 / 480); its packed input projection produces
two 3,456-wide branches before gating.
Compilation is enabled for this recipe with the top-level setting:
"compile": {"enabled": true, "backend": "inductor", "mode": "max-autotune-no-cudagraphs", "dynamic": {"backbone": false}}The trainer compiles the student and teacher backbones (CNN plus transformer)
and DINO/iBOT projection heads in place using nn.Module.compile. A Gram teacher
backbone is also compiled if present. Losses, token selection, optimizer/EMA
updates, and data loading remain eager. Parameter names and optimizer references
are unchanged, so checkpoints can be resumed with compilation enabled or disabled.
Compilation does not add CNN checkpointing or change precision settings.
Compiled backward graphs are captured with autocast disabled for backward,
matching the trainer's existing AMP forward / non-autocast backward sequence.
Other configs default to compilation disabled; "compile": false disables it.
Optional settings are backend, mode, dynamic (default null, automatic
dynamic-shape handling), and fullgraph (default false, allowing graph breaks).
dynamic may also map backbone, dino_head, and ibot_head to true,
false, or null; unlisted modules use null. This recipe compiles backbones
statically, giving one graph per global/local crop shape instead of a slower
dynamic-shape recompile, while the iBOT head keeps automatic handling for its
varying number of masked tokens. With the inductor backend the trainer disables
Inductor's mix-order reduction, which in torch 2.10 returns the mask-token
gradient with the wrong shape in channels-last CNN backward graphs (8+ masked
128³ crops); disabling it had no measurable throughput cost.
Initial calls compile lazily; global/local shapes and training/evaluation paths
can each require warmup. Compilation failures are not silently suppressed.
The default mode avoids explicitly enabling CUDA-graph modes that reserve extra
workspace memory. Measure throughput and peak VRAM after warmup on the target GPU;
compiled execution can change floating-point rounding and random-number streams.
For compiler diagnostics, run with TORCH_LOGS=graph_breaks,recompiles.
The recipe also enables these throughput and memory settings. Each is off unless a config sets it:
ibot_fused_last_layer(top level): the heads return the iBOT bottleneck features and last-layer weight, and the loss forms the[n_masked, ibot_out_dim]logits, Sinkhorn-Knopp targets, and gradients one chunk ofibot_masked_loss_chunk_sizerows at a time. Sinkhorn-Knopp reduces to per-prototype scalesbwith targetssoftmax(teacher_logits / T + log b), so three passes over recomputed teacher logits replace the stored matrices. The math is unchanged; only floating-point rounding differs.model.deeper_embed_channels_last: CNN activations and weights use the channels-last 3D layout, which cuDNN's tensor-core convolutions use natively. Weights are converted after initialization; checkpoints are layout-independent.model.deeper_embed_batch_chunk_size: null: the CNN embeds the whole batch at once instead of one sample at a time (the default of 1).fused_optimizer: fused CUDA AdamW.cuda_expandable_segments: the CUDA caching allocator uses expandable segments, avoiding fragmentation-driven cache flushes near full memory.cudnn_benchmark: cuDNN autotunes each convolution's algorithm for the (fixed) crop shapes on first use. On the H100 this cut thepretrain_8_9um.jsonstep from 1.65 s to 1.53 s. Algorithm choice can change floating-point rounding.compile.mode: "max-autotune-no-cudagraphs"(used bypretrain_8_9um.json) benchmarks Triton matmul and convolution kernels against the library ones at compile time and keeps the fastest. The first launch compiles for about 13 minutes; the inductor cache makes later launches fast.
Historical optimizer-step measurements used an earlier version of
pretrain_8_9um.json with width 288, a 2,048-wide FFN, a 16-channel CNN stem,
and two local views. They do not measure the current recipe. On one H100
(batch 16, 2 accumulation steps, train_step on real batches preloaded into
memory so there is no data wait; median of 10 steps after 6 warmup steps):
| setting | s/step | samples/s |
|---|---|---|
| previous config | 1.649 | 19.4 |
+ cudnn_benchmark |
1.527 | 21.0 |
+ model.attention_backend: "cudnn" |
1.389 | 23.0 |
+ compile.mode: "max-autotune-no-cudagraphs" |
1.350 | 23.7 |
In those measurements, the GPU was the bottleneck (97–100% utilization in training; the loaders supplied about 46 samples/s). The largest cost was the CNN embedding's full-resolution layers: with 16 channels, a 16→16 3×3×3 convolution at 128³ runs at about 87 TFLOPS, against about 600 TFLOPS for 128→128 at 32³ with the same arithmetic, because the narrow channel dimension leaves tensor-core tiles mostly empty.
dataset.occupancy_level: the pyramid level (5 here, 32× coarser) whose nonzero voxels, dilated by one cell, bound each source window's nonzero fraction. Windows that cannot reachnonzero_thresholdare rejected before any read, so the accepted windows and their distribution are unchanged. Masked-out chunks are absent from the S3 stores, so these reads were otherwise wasted. The coarse masks are read once at startup and only cover the usable bounding box.dataset.object_mask(enabled,gap_um2000,z_trim_pct0.05): source windows lie entirely inside the scanned object, replacing the centralvol_trim_pctbox (which both reached past flattened scrolls and cut off interior of wide ones). The published volumes are SAM2-masked with a dilated margin of air, so at startup theoccupancy_levelcells (32× coarser; the 2.4 µm volumes mean-pool their 8× level, streamed in z slabs) are split into tissue and air by an Otsu threshold, closed overgap_umso the space between wraps and interior air chambers (open to the outside through gaps narrower than 2 mm) count as inside, hole-filled per z slice and in 3D, intersected with the SAM2 mask (so the closing cannot spread into real exterior air), with components under 1% of the largest dropped. A window may start in a cell only if a window starting anywhere in that cell covers only object cells (conservative by up to one cell), and only outside the bottom and topz_trim_pctof z slices. Objects thinner than a window (fragments, flattened pieces) setobject_mask_min_inside(e.g. 0.5) in their dataset entry: at least that fraction of the window's cells must be inside instead of all of them.object_mask_source: "sam2"in a dataset entry uses the published SAM2 mask itself (nonzero cells, eroded by one cell so windows stay off its edge, air margin included) instead of the tissue envelope; PHercMAN5, a box of fragments with large air chambers, uses it. Starts are uniform over those cells; volume weights follow their counts. Every crop is cut from inside its window, so no crop reaches outside the object. Both nonzero checks are skipped (the occupancy pre-check and the post-readnonzero_thresholdcheck): every drawn window is used. Needspixel_size_umper dataset. Measured before the change by simulating the old sampler on these masks: on average 9% of global crops were less than half inside the object (up to 23% on PHerc0826). Startup reads ~13 GB nominal (less in practice: masked-out chunks are absent from S3), most of it the twelve 2.2–2.4 µm volumes' 8× levels.dataset.read_ahead: each loader process reads this many future source windows in background threads while it augments the current sample. Window locations are still drawn in the loader process, so sampling stays deterministic, but enabling it changes the order of random draws.dataset.gaussian_blur: trueanddataset.global_crop_scale: [0.18, 0.45]/dataset.local_crop_scale: [0.023, 0.057]: views are resampled only 0.87–1.18× from the 192³ source window (global 108–147 → 128 voxels, local 55–74 → 64) with Gaussian blur enabled as part of the augmentation. The zoom range covers the 7.9–9.4 µm spread in voxel size between scans.num_workers: 24,prefetch_factor: 2,persistent_workers: true: with read-ahead, one worker produces about 1.8 samples/s (augmentation-bound), so 24 workers comfortably exceed the GPU's ~20 samples/s on an H100.
The teacher forward now runs before the student forward, so its no-grad intermediates are freed before student activations accumulate.
The model option attention_type accepts "full" (the default, used in this recipe)
or "axial". Each axial block has three sequential attention
sublayers along D, H, and W, each with its own normalization, projections, and
residual update, followed by one FFN. Patch queries attend to their spatial
line plus CLS/register tokens; CLS/register queries attend to all tokens on
each pass. Thus global crop representations remain available for DINO, and
spatial outputs remain available for iBOT. RoPE is applied using the original
grid coordinates before grouping lines. rope_type independently selects the
positional encoding and does not select the attention pattern.
Axial attention supports variable crop grids and gradient checkpointing. It
reduces attention interactions but adds two sets of attention projections per
3D block; runtime and training quality should be measured on the target GPU.
Full-attention checkpoints keep their existing parameter layout when
attention_type is omitted. Switching to axial adds parameters and requires
a matching checkpoint or a new training run.
The CNN uses the existing deeper embedding with a 32-channel full-resolution
stem: one 3×3×3 stride-1 convolution, normalization, and activation. Four residual
stages follow, with 32, 64, 128, and 256 channels, strides 1, 2, 2, and 2, and
1, 2, 2, and 1 basic residual blocks (two 3×3×3 convolutions per block).
The first stage operates entirely at full resolution; subsequent stages
downsample in the first convolution of their first block. This gives three
convolutions before the first downsampling, enough to resolve one-voxel
structure while keeping full-resolution activations small. Capacity sits in the
64³ and 32³ stages; a single 256-channel block at token resolution is followed
by a 1×1×1 projection to the 480-wide EVA tokens. Residual skip connections
retain their channel projections. Total stride is 8, giving 4,096 global
tokens and 512 local tokens, before CLS/register tokens. The CNN trains at the full learning rate
(patch_embed_lr_mult: 1.0).
The recipe uses a 192³ source sampling window, from which augmented crops are
drawn. CNN convolutions use zero padding, and the model receives exactly the
128³ global and 64³ local crops. There is no halo expansion, extra context read,
or cropping of the embedded grid. The recipe retains the historical output-directory
name dinovol_pretrain_8_9um_cnn192proj_10blocks_ffn2048_full_g128_p8_stem16/;
that name no longer describes its architecture. Since automatic resume is enabled,
use a fresh output directory for a new architecture unless its checkpoint is compatible.
The configurable CNN options under model are:
{
"embedding_type": "deeper",
"embed_dim": 480,
"num_heads": 10,
"patch_size": [8, 8, 8],
"deeper_embed_stage_channels": [32, 64, 128, 256],
"deeper_embed_stage_strides": [1, 2, 2, 2],
"deeper_embed_depth_per_level": [1, 2, 2, 1],
"deeper_embed_use_stem": true,
"deeper_embed_stem_channels": 32,
"deeper_embed_stem_type": "conv",
"deeper_embed_final_projection": true
}Without a final projection, the last stage must have embed_dim channels.
Stage channel/depth counts must match the stride list, whose per-axis product
must equal patch_size. Each stride entry can be a scalar or a per-axis list
of 1s and 2s. When omitted, strides are derived from patch_size as before.
Stem width defaults to 32, and stem type defaults to the embedding block style
(a residual stem for the standard deeper embedding); "conv" selects a single
convolution with normalization and activation.
Omitted CNN architecture options preserve the existing deeper embedding's
parameter layout. All CNN embeddings now use zero padding; older checkpoints
can load with matching architecture, but their boundary behavior changes.
Spatial CNN chunking and the separate *_view_size dataset settings have been
removed; use *_crop_size and optionally deeper_embed_batch_chunk_size.
The CNN regression tests check the recipe's architecture
on a 32³ synthetic crop, gradients, masking, and checkpoint reconstruction;
the pretraining smoke suite also exercises the projection-free path with a
smaller CNN and synthetic volume.
The volume list is pinned from the public scan metadata, retrieved on 2026-10-11. It contains one masked intensity OME-Zarr per eligible scroll sample (32 in total):
- 24 ESRF volumes at 8.640–9.366 µm and two Diamond volumes at 7.910 µm (PHerc0172 and PHerc1667, treated as approximately 8 µm), at scale 0.
- Five scrolls whose 2.4 µm ESRF scans carry fiber predictions, used at pyramid level 2 (9.6 µm): PHerc0139, PHerc0332, PHerc1451 and PHercParis4 (replacing their 7.9–9.4 µm volumes) and PHerc1299 (no other volume near this resolution). Their fiber and surface predictions are on the same grid, so no registration is needed.
sample_id, pixel_size_um, and scan_location in each entry document the
selection; the loader uses volume_path, volume_scale, and (with
object_mask) pixel_size_um. Sampling retains the
existing weighting by valid crop starts, rather than equal weight per scroll.
Occupancy cells cover 2^occupancy_level selected-scale voxels on every volume:
level-2 volumes read the same pyramid level and max-pool it.
PHerc0172 uses the newer cropped export 20241024131839. PHercMAN5, PHercMANB,
PHercMANBp, PHercParis3, PHerc0009B, PHerc0343P and PHerc0500P2 (all ESRF BM18) are
read from their 2.2–2.4 µm masked scans at scale 2 (8.9–9.6 µm), like PHerc0139,
PHerc0332, PHerc1451, PHerc1299 and PHercParis4. PHercParis3 has no surface or fiber
predictions in this format and trains without object labels. MANBp, 0343P and 0500P2
are thinner than a source window and use object_mask_min_inside: 0.5.
dinovol_2/bench_speed.py is a machine-specific launcher using the paths defined
in that file and GPU 6. On the configured benchmark host, run:
python -m dinovol_2.bench_speed launch
python -m dinovol_2.bench_speed statusEach launch creates a fresh run-* directory under BENCH_ROOT, next to the
source RUN_DIR, with its own config, checkpoints, log, and PID file. It resumes
explicitly from RUN_DIR/checkpoint_step_160000.pt; the source checkpoint is read
in place, and the source config, logs, and other checkpoints are preserved.
The seed checkpoint must exist before launch. status reads the latest launch
recorded in BENCH_ROOT/latest-run.txt. Earlier benchmark directories are kept,
including when a later launch fails. The launcher does not stop existing GPU
processes; choose a free GPU before running it.
The filesystem behavior is checked with mocked process launches (no GPU or training process is started):
python -m unittest discover -s tests -p test_bench_speed.pyTo encourage object-centric features (each papyrus sheet and each fiber a distinct object) without full supervision, the recipe adds small point and object losses on pseudo-labels derived from published predictions:
- Sheets: the binary
surface-m7recto surface prediction (29 volumes;surface_path,surface_level). Connected components are found inside 48³ blocks and kept only if they are a single layer: every line along the sheet normal crosses them once (the normal from PCA and, for thin slabs, within the y-x plane). Pieces in adjacent blocks are joined only when the joined piece is still a single layer locally and no block would hold two pieces of the group. A sheet may therefore be split into several objects, but neighbouring wraps, which touch almost everywhere, are never merged into one. - Fibers: the
fibers-20260915212757presence prediction (5 volumes; 19.2 µm, sofiber_level: 3andfiber_factor: 2relative to the 9.6 µm image), thresholded at 0.5. Neighbouring voxels are joined only if their decoded fiber directions agree (|cos| ≥ 0.85), keeping the crossing fibers of the two papyrus layers apart. - Two objects are marked as different only when that is provable: sheets whose union clearly fails the single-layer test near their closest approach (stacked layers), and fibers at a large angle. Other pairs are ignored by the losses.
dataset.object_points controls sampling. Both global crop regions are drawn
first; labels are computed on one slab_thickness-voxel (16) z slab inside
their overlap, so every labeled object is visible in both views. Up to
max_labeled_points (512) points, points_per_object (16) per object and
max_objects (64) objects, are taken from the labels, and the rest of the
points_per_sample (1024) are drawn uniformly in the overlap. Points travel into
each view through the crop/resize mapping and the spatial augmentations like
keypoints. On real data a sample carries about 110 labeled points on 7 objects,
68% of samples have at least one provably different pair, and labeling costs
about 0.3 s per sample in a loader worker.
object_losses adds, for each view, a point embedding from a small MLP over the
trilinearly interpolated ViT tokens and CNN stage outputs (32, 64 and 128
channels at full, 1/2 and 1/4 resolution), and compares the student with the EMA
teacher's other view:
point_weight: the embedding at each point should agree (1 − cosine) with the teacher's at the same physical point in the other view. This term needs no labels and has no negatives: an unlabeled point nearby may lie on the same sheet or fiber, so only the object terms push embeddings apart. (A first version contrasted every point against all others; the monitor's dense panel then showed per-voxel fingerprints, even in air.)object_weight: points on the same object are positives, points on provably different objects negatives (InfoNCE,object_temperature0.1).region_weight: the same at object level on averaged token features with a projection head (DetCon-style).
Student points in iBOT-masked tokens are skipped, since their voxels are hidden.
Each weight is 0.1 relative to the DINO loss, and all three ramp up linearly over
ramp_steps (2000) from the step the heads were added. A checkpoint without the
heads can be resumed: the heads are initialized (the teacher's copied from the
student) and the optimizer state of existing parameters is kept.
grad_accumulation_steps: 2 runs two batches of 12 per optimizer step
(effective batch 24 per GPU). The configured learning rate is 1.768e-4.
dataset.augmentation selects the view augmentation applied after the random
resized crop. "full" (the default, used by pretrain_8_9um.json together with
gaussian_blur: true) applies simulated scanner artifacts (smear, slice
illumination), right-angle rotations, flips and axis transposes, local brightness
gradients and gamma, blur, noise, sharpening, contrast and brightness, low
resolution, gamma and inversion. "dino" keeps only the rotations, flips,
transposes and the contrast and brightness jitter. Rotations and transposes use
every pair of axes with equal crop size (all three for cubic crops). The scanner
artifacts act along fixed tensor axes, so they run before the rotations: they
follow the volume's axes, as real scan artifacts do, instead of always lying along
the same axes of the augmented view. Every crop draws its own augmentation (with
the probabilities above, most intensity transforms change a 1-channel crop in
10 to 40% of crops, since they also apply per channel with probability 0.5).
Points, object points and token correspondences follow the spatial transforms.
ibot_targets: "cross_view" (used by pretrain_8_9um.json; default
"same_view") takes the teacher's iBOT target for a masked student token from the
other global view, at the same physical location (the teacher's patch tokens
interpolated there), instead of from the same token of the same unmasked view.
With same-view targets, anything in the target that depends on the token's place in
the crop can be predicted from that place alone, which the network can read from
the crop borders (zero padding, attention range). This "positional collapse" is a
known failure of masked-token objectives (CAPI, arXiv:2502.08769). Here it took
over tokens without tissue: from about 110k steps the teacher's tokens in empty
regions encoded their distance to the crop faces along y (the horizontal bars),
grew through the transformer blocks (on a blank input the per-position token
variance rose from 8.7 at 110k to 85 at 135k, 96% of it along y), and persisted
after adding rotations; RoPE was not involved (shifting the teacher's RoPE
coordinates did not move it). Centering the targets per position did not remove
it, because the code was present only in empty tokens. The two global crops have
independent offsets, scales and orientations, so a token's place in the student's
crop says almost nothing about the teacher's position in its own crop. Only masked
tokens inside the other view get a target (about 58% on average); their weights are
renormalized per view, and the kept fraction is logged as ibot_target_fraction
(ibot_tgt in the progress bar). Validation batches keep same-view targets.
Run from dinovol/ with the existing environment:
AGENTS_AGENT_MODE=1 .venv/bin/python -m dinovol_2.pretrain dinovol_2/configs/pretrain_8_9um.jsonThe URLs are public and require no credentials file. Volume selection and
scale-0 array metadata were checked; GPU memory requirements and training quality
have not been measured for this recipe. Adjust batch_size for the available GPU.
Original Dinovol code is available under the MIT License. Third-party code retains its upstream terms; see Third-Party Notices.
The following settings can be added to a training config:
{
"lr_schedule": "constant",
"warmup_steps": 1000,
"koleo_distributed": true,
"koleo_loss_group_size": 16,
"model": {
"model_type": "v3",
"rope_type": "axial",
"scale_mlp": false,
"mlp_ratio": 2.0,
"qkv_bias": false,
"untie_cls_and_patch_norms": false,
"untie_global_and_local_cls_norm": true
}
}lr_scheduledefaults toconstantforv3, andcosinefor older presets.constantuses the existing linear warmup and then holds the configuredlr, including beyondmax_iterations; it ignoresmin_lr. Last-layer freezing and parameter-group LR multipliers still apply. Weight decay, teacher momentum, and temperature retain their own schedules. Inv3, weight decay defaults to a constant0.04and teacher momentum to a constant0.994; overriding their initial values also changes their default final values. Setweight_decay_endequal toweight_decayandfinal_momentum_teacherequal tomomentum_teacherto hold those constant too.warmup_steps: 0disables LR warmup; otherwise the default is 10% of the run.scale_mlp,mlp_ratio, andqkv_biasdefault tofalse,2.0, andfalseinv3, following the DINOv3 7B pretraining block.scale_mlp: falseremoves the extra normalization inside SwiGLU; the usual pre-attention and pre-MLP norms remain. Our ratio specifies the effective hidden width directly: DINOv3'sffn_ratio: 3applies an internal two-thirds adjustment. Older presets retaintrue,8/3, andtrue. To load checkpoints from the earlierv3architecture, preserve their saved model configuration or explicitly set all three old values (mlp_ratio: 2.6666666666666665in JSON); changing these settings changes parameter shapes and state-dict keys.attention_backend(defaultnull, PyTorch's choice) names the preferred fused attention kernel:cudnn,flash,efficientormath, with the others as fallbacks. Each attention call applies it, so compiled graphs record the choice when traced (a process-wide setting would not reach graphs reloaded from the compile cache). With 4101 tokens and head dimension 48, cuDNN attention takes 7.1 ms forward plus backward per 32 global crops on the H100, against 12.4 ms for FlashAttention-2;pretrain_8_9um.jsonusescudnn.rope_typeselects the existing 3Daxialor learnable per-blockmixedimplementation. The default isaxialforv3,mixedforv2, andaxialfor the original unversioned model. Both support variable crop sizes and coordinate rescaling. The attention head dimension must be divisible by six for 3D RoPE.rope_shift_coords,rope_jitter_coordsandrope_rescale_coordsaugment the student's RoPE coordinates during training (the EMA teacher always uses the plain grid): a uniform shift in[-shift, shift]per axis (coordinates span[-1, 1]), an independent log-uniform stretch in[1/jitter, jitter]per axis, and one log-uniform isotropic scale in[1/rescale, rescale]. Only rescaling (2.0) is on by default.pretrain_8_9um.jsonuses mixed RoPE with shift1.5, jitter1.5and rescale2.5: with axial RoPE that run's teacher drifted, from about 110k steps, to patch tokens that vary along one axis only (horizontal bars in the token PCA, first in crops with little tissue), while iBOT loss dropped sharply. The run was resumed from 110k with these settings. Axial RoPE lets a head attend by the offset along a single axis, which averages whole planes of tokens. Mixed RoPE with these settings did not stop the drift (share of patch-token variance explained by y in low-tissue crops: 0.090 at 110k, 0.107 axial and 0.103 mixed at 115k), nor did adding rotations and transposes from 115k. The cause was the same-view iBOT targets (seeibot_targets); the run was resumed from 140k with cross-view targets and the full augmentation.untie_cls_and_patch_normsgives CLS/register tokens their own output norm; patch tokens keep the originalnorm.untie_global_and_local_cls_normadds a separate CLS/register output norm for local crops during student training. Evaluation and the EMA teacher use the global norm. This flag defaults totrueforv3andfalsefor older presets;untie_cls_and_patch_normsremainsfalsein all presets.koleo_distributeddefaults totrue: with DDP, KoLeo compares each view's embeddings across ranks, with gradients through gathered embeddings. This expects equal per-rank batch sizes, as supplied by the training loaders. Set it tofalsefor local-only comparisons. A comparison batch with fewer than two samples produces zero loss and gradient.koleo_loss_group_sizedefaults to16forv3, andnull(the whole comparison batch) for older presets. Groups are contiguous in rank-major order, independently for each global view. If fewer than 16 samples are available, all available samples are used. Larger comparison batches must be divisible by the requested group size; choose a divisor ornullotherwise. Grouping limits neighbour selection; the current implementation still gathers all ranks.
Model head dimensions and pretrained_weights are preserved in the resolved
training config. When enabling separate norms on an older checkpoint, missing
norm weights are initialized from its trained shared norm (or its existing CLS
norm for the new local norm). Use model.pretrained_weights to start a new run
when changing normalization options; resuming optimizer state requires the same
parameter layout. A checkpoint written with axial RoPE can be resumed with
rope_type: "mixed": each block's mixed frequencies start at the axial layout, so
outputs are unchanged at the resume step, and they then train (without weight
decay); optimizer state of all existing parameters is kept, matched by name.
Other switches between mixed and axial RoPE are architecture changes, not exact
checkpoint conversions.
Every train_monitor_every_n steps the trainer writes
monitor/monitor_step_NNNNNN.jpg from the first monitor_batch_size samples of
the current batch (one row each, at least 5; pretrain_8_9um.json uses 12). For
each global view a row shows the center slice of the input, then
monitor_pca_sets (default 4) RGB images of the student's patch tokens on that
slice from principal components 1-3, 4-6, 7-9 and 10-12, and, with the object
losses, the same for the dense point head at every voxel of the slice. PCA is fit
per panel, so colors are comparable only within one panel.
Masked student views always zero the selected voxel regions before patch
embedding, then replace the selected embeddings with the learned mask token.
Each loss-mask position covers one patch_size region on the output grid.
Masking happens on the prepared model input, before convolution. The original
crop remains unchanged for the unmasked teacher.
For embedding_type: "default", this is mathematically equivalent to the former
token-only masking, including parameter gradients. For "deeper", it prevents
neighboring embeddings from observing the hidden voxel values and changes
training behavior, including when resuming an older checkpoint. There is no
legacy masking mode or configuration switch; checkpoint parameter shapes stay
unchanged. Output/gradient equivalence and hidden-voxel isolation are covered by
tests/test_voxel_masking.py in the standard test suite.
pretrain.py now supports the three-stage DINOv3-style workflow:
- base pretraining with DINO + iBOT + KoLeo
- late dense-feature refinement with Gram anchoring
- short mixed-resolution HR adaptation with Gram anchoring kept on
The new config surface is:
- top-level
gramenabledloss_weightteacher_checkpointteacher_refresh_everyteacher_refresh_start_stepnormalizedimg_levelremove_negremove_only_teacher_neg
- dataset keys
gram_teacher_crop_sizegram_teacher_no_augmentationsvariants
When gram.enabled=true, the trainer builds a frozen Gram teacher backbone, loads it from gram.teacher_checkpoint when provided, refreshes it from the live EMA teacher on the configured cadence, and adds an image-level Gram loss on patch features. Gram-teacher crops are paired with each global crop from the exact same sampled 3D region. With gram_teacher_no_augmentations=true, they receive normalization and the same geometric transforms as the student, but no intensity/noise augmentation. Shared rotations, flips, and transpositions preserve patch correspondence even at different crop resolutions.
Mixed-resolution HR adaptation is enabled by adding dataset.variants, where each variant defines its own crop sizes and sampling ratio. The trainer builds one dataloader per variant and samples them with the configured weights. All embedding types consume the configured crop sizes directly.
dinovol_2/example_config.json remains runnable as a base-pretraining config and also includes a recipes object with three complete examples:
recipes.base_pretrainrecipes.gram_refinementrecipes.hr_adaptation
The important stage-specific overrides are:
{
"gram": {
"enabled": true,
"loss_weight": 2.0,
"teacher_checkpoint": "/path/to/previous/checkpoint.pt",
"teacher_refresh_every": 10000
},
"model": {
"pretrained_weights": "/path/to/previous/checkpoint.pt",
"pretrained_backbone_only": false
}
}For HR adaptation, add weighted crop variants:
{
"dataset": {
"variants": [
{
"ratio": 0.3,
"global_crop_size": [128, 128, 128],
"local_crop_size": [48, 48, 48],
"gram_teacher_crop_size": [160, 160, 160]
},
{
"ratio": 0.7,
"global_crop_size": [160, 160, 160],
"local_crop_size": [80, 80, 80],
"gram_teacher_crop_size": [192, 192, 192]
}
]
}
}To sanity-check one batch end to end:
uv run python -m dinovol_2.verify dinovol_2/example_config.json --no-ampTo run the synthetic smoke suite:
uv run python -m unittest discover -s tests -vWith an existing environment, run without installing or updating dependencies:
AGENTS_AGENT_MODE=1 OMP_NUM_THREADS=1 MKL_NUM_THREADS=1 .venv/bin/python -m unittest discover -s tests -vThe suite includes synthetic volume smoke tests and regressions for checkpoint initialization, output norms, both RoPE implementations, constant LR, paired Gram geometry, and two-rank KoLeo gradients. It does not establish downstream quality; validate training changes on real held-out volumes before a long run.
Pretraining can optionally reinforce patch embeddings with sparse annotated points while DINO, iBOT, KoLeo, and Gram remain active. The top-level defaults are:
{
"point_supervision": {
"enabled": true,
"sampling_probability": 0.1,
"loss_weight": 0.05,
"different_type_margin": 0.0,
"max_points_per_view": 64
}
}Attach annotations to each applicable volume (paths are intentionally omitted from the runnable example config):
{
"volume_path": "/path/to/volume.zarr",
"volume_scale": 1,
"point_collections": [
{"path": "~/points/fibers.json", "type": "fiber"},
{"path": "annotations/surface.json", "type": "surface"}
]
}Each annotation file must be version 1 and contain a collections array whose entries contain points arrays with p: [x, y, z]. The configured type applies to every point in that file; equal type strings have equal meaning across volumes. ~ is expanded, while other relative paths remain relative to the process working directory.
Coordinates are scale-0 voxel centers in XYZ order. Dinovol converts them to ZYX and maps voxel centers to the selected pyramid level using the co-registered level-shape ratio. Point-centered sampling and the point loss are inactive when the feature is disabled or no volume has point_collections. Only training samples and the two global views are supervised; validation, monitor batches, and local views keep their existing sampling behavior.
pretrain.py can optionally run small downstream segmentation trainings during pretraining.
- set
task_eval_everyto a positive step cadence to enable it - choose
eval_taskasboth,surfaces, orink - set
eval_task_train_itersto control the mini-training length, default500 - set
eval_task_decoder_typetosimpleorpatch_encode_decode
The task data is downloaded with python -m dinovol_2.eval.download_data --task both.
bothnow meanssurfacesplusinksurfacesis resized 2x before crops are drawnsurfacesandinkeach use the first 10 sorted samples as the deterministic validation setinkis not resized before crops are drawn- train and validation crops are taken from precomputed chunks that contain some foreground and at least 50% background in supervised voxels
- the saved validation image contains one row per validation sample, with image / label / prediction panels
- for
ink, voxels withsupervision_mask == 0are ignored and supervised unlabeled voxels are treated as background - for
ink, loss/metrics and saved previews use a max projection across Z to match the flat ink trainer
There is a small napari helper for checkpoint inspection at dinovol_2/eval/napari_visualizer.py.
Run it with:
python -m dinovol_2.eval.napari_visualizerWorkflow:
- open an OME-Zarr from the widget, click
Load Scales, choose the desired scale, and clickOpen Zarr - draw a rectangle in the generated
*_bboxshapes layer; this 2D YX bbox is applied across the full Z span of the selected scale - add one or more points in a
Pointslayer - choose a
pretrain.pycheckpoint, image layer, and points layer in the dock widget - click
Cache Embeddings - click
Show Feature PCAto render a 3-channel PCA view of the cached patch embeddings - optionally enable
Otsu Foreground Maskand setMask Dilationbefore creating the PCA layer - click
Similarity For Selected PointsorSimilarity For All Points
The widget rebuilds the teacher backbone from the saved checkpoint config, computes a patch embedding grid only inside the active bbox for the selected OME-Zarr scale, and limits the PCA and cosine-similarity outputs to that same crop. The dock widget opens on the bottom of the napari window.