Direct answer
Strict dependency pinning (exact versions, lockfiles) maximizes build reproducibility and stability at the cost of manually managing security-patch updates; flexible version ranges get security patches automatically but risk an unreviewed, unexpected dependency update silently changing behavior; the right policy is strict pinning as the default, with an automated, TESTED update process (not manual, ad hoc bumps) so security patches still flow through without sacrificing reproducibility.
Structured elaboration
- Reproducibility: exact pinned versions (a lockfile capturing the FULL resolved dependency tree, not just top-level ranges) guarantee that a build today and the same build next month use IDENTICAL dependency code, which matters enormously for ML specifically, since a subtly different version of a numerical library can change model behavior in ways that are hard to detect without exact reproducibility as a baseline to compare against.
- Security patches: a flexible range (
numpy>=1.20,<2.0) picks up patch releases automatically on the next install, including security fixes, without any explicit action; strict pinning means a security patch requires an explicit, deliberate version bump, which can lag if the process isn't automated and monitored.
- Build stability: flexible ranges risk a NEW patch release (even a supposedly 'safe' patch bump) introducing an unexpected behavior change or a new bug, breaking a build that was working fine yesterday for no code change of your own; strict pinning eliminates this specific risk entirely, at the cost of the manual-update burden above.
- A policy that gets both: pin exactly by default (lockfile-based), but run an AUTOMATED dependency-update bot (Dependabot, Renovate, or similar) that opens a PR for each available update, which then runs through the FULL test suite (including, for ML, a golden-set accuracy/performance regression check per the companion CI survivor) before a human merges it; this gets the security-patch velocity of automatic updates while keeping every single update explicitly reviewed and tested before it's live, rather than either silently picking up an update (flexible ranges) or manually, irregularly bumping versions (unautomated strict pinning).
Worked example
A project pins torch==2.1.0 exactly in its lockfile; a security patch (torch==2.1.1) is released; an automated bot opens a PR bumping the pin, which triggers the full CI suite including the golden-set model-evaluation gate; the PR shows the new version passes all tests with no accuracy regression, and a human reviewer merges it within a day of the patch's release, achieving both reproducibility (every commit has an exact, known dependency set) and reasonably fast security-patch adoption (not manual, ad hoc, and easy to neglect).
Trade-offs and pitfalls
An automated update bot that opens PRs but that NOBODY actually reviews and merges in a timely way provides none of the security benefit while still adding review-queue noise; the policy only works if merging dependency-update PRs is treated as routine, prioritized work, not something that piles up indefinitely, which is as much a team-process commitment as a tooling choice.
Separately: reproducibility across dev workstation, CI, and a multi-GPU cluster
Making an ML experiment reproducible across environments requires controlling every source of randomness AND every source of environment drift, not just pinning code:
- Seeding every source of randomness: set the seed for Python's own
random, numpy, and the ML framework's own RNG, which are all SEPARATE generators; seeding only one leaves the others uncontrolled. On a multi-GPU/multi-worker setup, seed each worker deterministically (for example base_seed + worker_rank) rather than letting each worker seed itself from system entropy, which would make even a single-node-vs-cluster comparison non-reproducible.
- Deterministic-ops flags: enable the framework's deterministic-algorithms mode (for example
torch.use_deterministic_algorithms(True), torch.backends.cudnn.deterministic = True), since some GPU kernels have a faster but non-deterministic default implementation that produces a different, though equally valid, numeric result run to run even with every seed fixed.
- Containerization: run dev, CI, and the cluster from the SAME container image, built from the same lockfile and Dockerfile described above, so a library-version or OS-level difference between where you develop and where the cluster actually trains isn't a hidden variable.
- A reproducibility checklist/test: run the SAME short training config (a handful of steps, small enough to be fast) twice, once on a dev workstation and once in CI/on the cluster, and assert the resulting loss values (or a checkpoint checksum) match within a defined, documented numeric tolerance; a divergence beyond that tolerance fails the check and should block trusting a full training run's results until the source of the drift (an unseeded RNG, a differing deterministic-ops flag, a genuine environment mismatch) is identified.
Worked example: a CI check runs a 50-step training config with a fixed seed on both a CPU-only dev machine and a GPU CI runner, comparing the resulting loss curve; a difference beyond the documented tolerance (accounting for expected CPU-versus-GPU floating-point variation) fails the check, catching an accidentally-unseeded data-shuffling step that was silently making every run non-reproducible even on the SAME hardware, before it wastes a multi-day cluster run whose results can't be trusted or reproduced.