Azure ML Training Deployment
Summary
scripts/azure/promote_checkpoint.sh); experiment tracking stays
TensorBoard-only — no MLflow — served live off blob storage via
adlfs (scripts/azure/tb_azure.sh, in-job sidecar copies tfevents to
the output mount every 60 s); spot/low-priority is not currently possible
(both clusters dedicated, TotalLowPriorityCores quota = 0 — needs a quota request
+ a new cluster). The iolabs-common resolution risk is cleared — it's published
on Nexus (0.3.2).
scripts/train.py --config configs/*.yaml)
as GPU command jobs on the existing AI3D Azure ML workspace
(ai3d-lf-mlw-pc-01), using the already-provisioned
Nvidia-TeslaT4-2x-GPU-Compute cluster (Standard_NC4as_T4_v3, scales 0→12).
The ~5 GB tile/label dataset is mirrored once from DVC into the workspace blob store and
mounted read-only; runs/ (TensorBoard events, checkpoints, overlays) becomes the
job output and is downloaded for local TensorBoard comparison against existing local runs.
The deployment lives entirely in this repo (env spec, job YAML, submit script) — it borrows
the orchestrator repo's proven conventions (conda env on the MPI base image, T4-compatible
torch pin, az ml job create submission) without adopting its
component/pipeline machinery, which is overkill for single-node training experiments.
Two small code changes are needed in train.py: a --data-root and a
--log-dir override so mounted paths replace the repo-relative ones in configs.
Context
- What runs today: Lightning harness, any smp architecture; configs like
unetpp_r34_recipe2.yamltrain on 5 Abschnitt tile dirs (data/00_external/260611_topdown_tiles+ masks indata/01_interim), 512-px crops, batch 8, early stopping onval/loss. Data is DVC-tracked (Google Drive remote), ~2.9 GB images + ~2.0 GB masks + review sidecars. - Azure workspace (sub
ba81…, RGAI3D-rg): GPU clusterNvidia-TeslaT4-2x-GPU-Compute= NC4as_T4_v3 (1× T4 16 GB, 4 vCPU, 28 GB RAM), autoscale 0–12; CPU clusterCPU-Compute-4x(F4s_v2, 0–16). - Orchestrator conventions worth reusing
(from
3dai.iolabs.orchestrator): per-step env dir withenvironment.yml(AML asset, manual integer version bump) +conda.yml; base imagemcr.microsoft.com/azureml/openmpi4.1.0-ubuntu22.04everywhere — GPU via piptorch==2.6.0+cu124(documented T4 driver ceiling: nothing newer than cu129/torch 2.11 runs); xorg/libgl conda packages for cv2;.amlignoreto keep thecode: ..snapshot small; submission is plainaz ml job create --set name=…$(date); data flows asuri_folderURIs onworkspaceblobstore, no registered data assets. The emptyconfigs/jobs/dir in this repo was already stubbed for job specs. - Not reused: parallel components, prepare-data steps, Kibana logging
wiring, and the orchestrator's habit of embedding Nexus pip credentials in committed
conda.ymlfiles (we keep the credentialed file out of git instead).
Key decisions
| Decision | Choice | Why |
|---|---|---|
| Where the deployment lives | This repo: environments/train/, configs/jobs/, scripts/azure/ | Training experiments iterate with the harness code; user confirmed no need to follow the orchestrator structure. configs/jobs/ already exists (empty). |
| Job shape | Single-node type: command job per experiment config | One config = one run maps 1:1 to how experiments run locally; cluster autoscale gives parallelism across configs for free (up to 12 concurrent). |
| Compute | Existing Nvidia-TeslaT4-2x-GPU-Compute, no new provisioning | T4 16 GB fits bs 8 @ 512 px U-Net++/r34 with 16-mixed; zero-min autoscale means no idle cost. |
| Data path | One-time mirror of the DVC dirs to workspaceblobstore under linebitmapsegmentation/data/…, mounted ro_mount | DVC (Google Drive) stays SSOT; jobs can't pull the gdrive remote (service-account JSON + slow). Blob mirror follows orchestrator's datastore-URI convention. |
| Path remapping | Add --data-root + --log-dir CLI overrides to train.py | Configs keep repo-relative data/… paths and stay valid locally; on Azure the same YAMLs run unmodified with re-rooted paths. Smallest possible diff. |
| Environment | AML env: MPI base image + pip torch==2.6.0+cu124, smp, lightning, albumentations, inference pkg from Nexus | Copies the exact torch/CUDA pin the orchestrator proved on these T4s; Nexus egress from AML compute already proven by orchestrator envs. |
| Nexus credentials | conda.yml generated from a committed conda.template.yml + env vars; real file gitignored | Improves on orchestrator's committed-credentials pattern without inventing new infra (Key Vault for pip index isn't supported by AML env builds anyway). |
| Experiment tracking | (Updated) TensorBoard only, live from blob: job writes events to node-local disk, a sidecar copies them to the output mount every 60 s (blobfuse only uploads on file close, so direct writes wouldn't be visible mid-run); tb_azure.sh serves TB straight off az://…/azureml/<job>/runs via adlfs — locally, or on an AML compute instance for an AAD-protected https URL. No MLflow. | Miro: "if TensorBoard can be served from Azure, forget AML metrics." It can. |
| Checkpoint home | (Resolved) Best checkpoints promoted back into DVC under data/03_experiments/azure/<job>/ via promote_checkpoint.sh | Keeps DVC the single experiment archive, same as the local sweep convention. |
| Spot / low-priority | (Resolved) Not available today: both clusters are dedicated tier and TotalLowPriorityCores quota is 0 in polandcentral. Enable later via quota request + new --tier low_priority cluster + resume-from-last.ckpt wiring. | Verified against the workspace 2026-07-07; spot T4 ≈ 75% cheaper when wanted. |
| Who writes the code | Codex (or Sonnet) writes; Fable reviews | Per Miro's delegation preference; changes are small and mechanical. |
Phases
Phase 1 — Stage the dataset in blob storage S
One-time upload of the training data to the workspace default datastore, matching the
repo-relative layout so --data-root is a pure prefix swap:
AZ="--account-name ai3dlfmlwpc011354919387 \
--container-name azureml-blobstore-919dd9b5-… --auth-mode key"
az storage blob upload-batch $AZ \
--destination-path linebitmapsegmentation/data/00_external/260611_topdown_tiles \
--source data/00_external/260611_topdown_tiles
# same for data/01_interim/260611_topdown_tiles_markings
# and data/02_processed/260611_confirmed_good (review sidecars + adjusted vectors)
Write a small scripts/azure/sync_data.sh wrapping this (idempotent re-sync
when DVC data changes) and drop a MANIFEST.txt (dvc hash of each mirrored
.dvc file) next to the blobs so it's always clear which DVC revision the mirror
reflects. ~5 GB upload, done once.
Exit check: az storage blob list --prefix linebitmapsegmentation/data/
shows the expected tile counts.
Phase 2 — Training environment on AML M
New environments/train/ following the orchestrator two-file convention:
# environments/train/environment.yml
name: linebitmapseg_train
version: 1
image: mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu22.04
conda_file: conda.yml # generated, gitignored
# environments/train/conda.template.yml (committed)
name: linebitmapseg-train
channels: [conda-forge]
dependencies:
- python=3.12
- pip
- xorg-libx11, xorg-libxext, xorg-libxrender, libglib, libgl # cv2 runtime
- pip:
- --extra-index-url https://download.pytorch.org/whl/cu124
- --extra-index-url https://${NEXUS_USER}:${NEXUS_PASS}@nexus.iolabs.ch/repository/pypi-private/simple/
- torch==2.6.0+cu124 # T4 driver ceiling — proven in orchestrator s4/s7
- segmentation-models-pytorch, albumentations, lightning>=2.2
- tensorboard, torchmetrics, pyyaml, opencv-python-headless
- iolabs-image-analyzer-line-bitmap-inference>=0.1.1
scripts/azure/render_env.sh substitutes the creds from env vars into the
gitignored conda.yml, then:
az ml environment create --file environments/train/environment.yml \
-g AI3D-rg -w ai3d-lf-mlw-pc-01
Verify first that iolabs-common resolves from Nexus via pip
alone (locally it's a path dep ../3dai.common; the inference package pulls it
transitively — orchestrator envs already install Nexus packages this way, but confirm this
specific chain with a local pip install --dry-run against the index before
burning an AML image build).
Exit check: environment build succeeds in AML (check the build log in
Studio); a trivial job on the GPU cluster prints torch.cuda.is_available() == True.
Phase 3 — train.py overrides for mounted paths S
Two new optional CLI args (Codex writes, Fable reviews; TDD per house rules):
--data-root DIR— re-roots every relativedata/…path indata.pairs / val_pairs / test_pairsontoDIR(pure prefix join inHarnessConfigpost-load; absolute paths left untouched).--log-dir DIR— overridestrain.log_dir(defaultruns) so outputs land on the AML output mount.--num-workers N— overridedata.num_workers; configs say 8 but NC4as_T4_v3 has only 4 vCPUs.
Exit check: local
uv run --extra ml python scripts/train.py --config configs/unet_baseline.yaml
--data-root . --log-dir /tmp/x --fast-dev-run passes; existing behaviour without the
flags is byte-identical (unit test on config re-rooting).
Phase 4 — Job spec + submit script + smoke run M
configs/jobs/train.yaml (command job, parameterised by experiment config):
$schema: https://azuremlschemas.azureedge.net/latest/commandJob.schema.json
type: command
code: ../.. # repo snapshot (see .amlignore)
compute: azureml:Nvidia-TeslaT4-2x-GPU-Compute
environment: azureml:linebitmapseg_train:1
inputs:
data:
type: uri_folder
mode: ro_mount
path: azureml://datastores/workspaceblobstore/paths/linebitmapsegmentation/data
config: configs/unetpp_r34_recipe2.yaml
outputs:
runs: { type: uri_folder, mode: rw_mount }
command: >-
python scripts/train.py --config ${{inputs.config}}
--data-root ${{inputs.data}} --log-dir ${{outputs.runs}}
--num-workers 4
experiment_name: linebitmapseg_training
Add .amlignore (exclude data/, runs/,
.venv/, .dvc/cache, docs/, previews) so the code
snapshot is a few MB, and scripts/azure/submit_train.sh <config> [overrides…]
wrapping az ml job create --file configs/jobs/train.yaml
--set inputs.config=… name=…$(date -u +%Y%m%d%H%M%S).
Exit check: a --fast-dev-run job completes green on the GPU
cluster; downloaded named-outputs/runs/ contains the TensorBoard event file.
Phase 5 — Real experiments + monitoring loop S
Submit the current experiment queue (e.g. unetpp_r34_recipe2, the
confirmed-good splits arm, seed-variance arms) — each as its own job; the cluster runs up
to 12 concurrently. Daily loop:
az ml job list --query "[?experiment_name=='linebitmapseg_training']…" -o table
az ml job stream --name <job> # live stdout
az ml job download --name <job> --download-path … --all
tensorboard --logdir runs # merged with local baselines
Downloaded run dirs drop into the local runs/ tree (same experiment naming),
so Azure and local arms compare in one TensorBoard. Best checkpoints that matter long-term
get dvc add-ed under data/03_experiments/ like the existing sweeps.
Exit check: one full config trains to early-stop on Azure with val/f1 within noise of the local reference run.
Phase 6 (optional, later) — Niceties S
- MLflow logger alongside TensorBoard — AML tracks metrics natively in
Studio, enabling
az ml job-level metric queries without downloading events. - AML sweep jobs for hyper-parameter search (lr, tversky α, stroke px)
instead of hand-written config fans like
sweep2d_*. - Low-priority tier on a second cluster for cheap long sweeps
(checkpoint/resume already works via Lightning's
last.ckpt). - Bigger SKU quota (NC8as_T4_v3 or A100) if the 4-vCPU dataloader measurably starves the T4.
Risks
| Risk | Severity | Mitigation |
|---|---|---|
iolabs-common may not resolve from Nexus via pip | cleared | Verified by querying the index directly. |
data/02_processed split dirs are symlink trees — symlinks don't survive the blob mirror, so unet_confirmed_good_splits-style configs break on Azure | med | Materialize the split dirs (copy instead of symlink) before mirroring, or generate Azure variants of split configs that reference the real dirs. |
4 vCPUs on NC4as_T4_v3 starve the dataloader (configs assume num_workers: 8); GPU sits idle | med | --num-workers 4 default in the job; watch step-time vs. local; escalate to a bigger SKU (Phase 6) only if measured. |
| Blob mirror silently drifts from DVC data (label repairs are ongoing — AI3D-226) | med | sync_data.sh is the only write path and stamps MANIFEST.txt with the DVC hashes; job stdout logs the manifest at start. |
Nexus credentials leak via committed conda.yml (orchestrator's existing anti-pattern) | med | Template + gitignore from day one; note the rendered file still lands in the AML environment asset (visible to workspace users — same exposure as today's orchestrator envs). |
| T4 driver ceiling breaks a future torch upgrade | low | Pin torch==2.6.0+cu124 with the why-comment copied from orchestrator conda.ymls; upgrade only with the documented cu129/2.11 ceiling in mind. |
Checkpoint upload bloat: rw_mount output writes top-2 + last ckpt per run (~100–300 MB each) | low | Fine at this scale; if sweeps grow, set save_top_k=1 for Azure arms via config. |
Cost runaway from early-stop failure (max_epochs: -1) | low | Early stopping (patience 4) is already the local convention; add a job-level timeout (e.g. 12 h) in the job YAML as backstop. NC4as_T4_v3 ≈ $0.53/h — a full run costs a few dollars. |
Open questions
- Which configs form the first real Azure batch once the repaired labels land — rerun the
sweep2d_*fan for validation against local results, or straight to new experiments? (The 2-epoch smoke run on current data validates the pipeline meanwhile.) - Request low-priority quota now (so a spot cluster is ready for the first big sweep), or wait until dedicated throughput actually becomes the bottleneck?
Checkpoints → DVC or blob?Resolved: DVC viapromote_checkpoint.sh.MLflow as system of record?Resolved: TensorBoard only, served from blob.