This document describes the unit test suite for the differential-privacy
layer of the example client service at cache_model_0/services/client.py
— specifically resolve_dp_config() (manifest → DP config resolution) and
apply_dp_mechanism() (the three post-training mechanisms). Background on
the manifest's nested dp block is in the repo docs on manifest-driven
services (see CLAUDE.md, "Manifest-driven, pluggable model services").
Note the target: cache_model_0/ is a reference service implementation
(MNIST-style), not dincli framework code. In production, model owners ship
their own client.py via IPFS; this suite pins down the behavior of the
bundled example that other implementations are likely to copy.
These are pure unit tests: no chain, no IPFS, no training loop. Real
PyTorch tensors are used (the suite is skipped entirely if torch is not
installed, via pytest.importorskip), but every DP configuration is chosen
so the mechanisms are exercised in a deterministic identity regime — zero
noise scale and a clipping norm far above the tensor norms — so exact
torch.equal assertions hold without seeding RNG.
cd /path/to/devnet
pytest tests/test_cache_client_dp.py -v
# single test
pytest tests/test_cache_client_dp.py -k test_update_gaussian_preserves_weights_when_noise_is_zeroRequires torch in the environment; without it the whole module is
skipped (not failed).
| Test | Behavior pinned down |
|---|---|
test_resolve_dp_config_uses_nested_manifest |
A nested dp manifest block (enabled, mode: afterTraining, mechanism: update_gaussian, parameters.{clipping_norm, noise_multiplier, clip_scope}) resolves field-for-field into the returned DP config, read through the runtime's get_manifest_key("dp", ...) |
All three supported mechanisms are driven with zero noise
(noise_multiplier/laplace_scale = 0.0 — a deliberate no-op per
add_gaussian_noise/add_laplace_noise) and clipping_norm: 100.0 (no
tensor comes near the bound), so the output must equal the input exactly.
| Test | Behavior pinned down |
|---|---|
test_post_training_mechanisms_preserve_weights_when_noise_is_zero[post_training_gaussian] |
Clip-then-Gaussian-noise pipeline is the identity when noise is 0 and clipping is loose (clip_scope: per_layer) |
test_post_training_mechanisms_preserve_weights_when_noise_is_zero[post_training_laplace] |
Same for the clip-then-Laplace pipeline (laplace_scale: 0.0) |
test_update_gaussian_preserves_weights_when_noise_is_zero |
The delta path — compute trained−reference delta, clip (clip_scope: global), noise the delta, reconstruct reference+delta — round-trips back to the trained weights exactly |
These drive real, nonzero noise and real clipping.
| Test | Behavior pinned down |
|---|---|
test_noise_scales_with_clipping_norm[...] |
For all three mechanisms, quadrupling clipping_norm at a fixed multiplier quadruples the noise standard deviation. Weights sit well inside the bound so clipping is a no-op and the measured difference is noise alone |
test_noise_scales_with_noise_multiplier[...] |
For all three mechanisms, quadrupling noise_multiplier at a fixed clipping norm quadruples the noise. Without this, an implementation that used clipping_norm alone would still pass the test above |
test_non_positive_clipping_norm_is_rejected[...] |
A clipping_norm of zero or below is rejected by apply_dp_mechanism for all three mechanisms, rather than silently disabling clipping or producing a no-op |
test_resolve_dp_config_rejects_non_positive_clipping_norm[...] |
The same rejection happens at manifest resolution, so the bad value is caught before any training output reaches a mechanism |
test_laplace_requires_an_explicit_scale |
post_training_laplace without its own laplace_scale raises rather than inheriting noise_multiplier |
test_resolve_dp_config_requires_laplace_scale |
The same requirement is enforced when the manifest is resolved |
test_global_clip_scope_bounds_the_combined_norm |
Sixteen tensors each driven past the bound: per_layer leaves a combined L2 norm of clipping_norm * 4 (that is, sqrt(16)), global leaves exactly clipping_norm |
test_resolve_dp_config_defaults_clip_scope_to_global |
A manifest with no clip_scope resolves to global |
All three zero-noise tests also include a non-floating tensor (counter, dtype
long) in the state_dict and assert it survives untouched, pinning down
that clipping/noising apply only to floating-point tensors; integer
buffers are cloned through unchanged (see clip_state_dict /
add_noise_to_state_dict / reconstruct_state_dict_from_delta).
Conventions to reuse when adding tests to this suite:
-
Skip, don't fail, without torch —
torch = pytest.importorskip("torch")at module top level; keeps the suite runnable in environments where the ML stack isn't installed (dincli itself does not depend on torch). -
Dynamic module load —
cache_model_0/is not an importable package;load_client_module()loadsservices/client.pyviaimportlib.util.spec_from_file_location. This mirrors howdincliactually consumes model-owner services at runtime (DinContext.load_custom_fndynamically loads IPFS-fetched files), so the tests exercise the module the way production does. -
DummyRuntime— a two-line stand-in forServiceRuntimeContextexposing onlyget_manifest_key(key, default)backed by a plain dict. Reuse it for any test of manifest-driven service behavior. -
Identity regime instead of seeded RNG — rather than seeding
torch.manual_seedand asserting statistics, configs are chosen so the mechanism is provably a no-op (noise ≤ 0short-circuits, loose clipping norm). Assertions can then use exacttorch.equal.
Behavior of the DP layer this suite does not yet exercise:
- Noise distribution shape — the sensitivity tests measure standard deviation only. Nothing asserts that Gaussian noise is Gaussian or that Laplace noise has Laplace tails, so a mechanism swapped for the wrong distribution at the same scale would pass.
resolve_dp_configbeyond the happy path — untested: absent/emptydpblock → compact disabled config;enabled: false/ disabled mode spellings (DISABLED_DP_MODES:none,off,false, …);enabled: truewith no mode coercing toafterTraining; parameter defaults injected bysetdefault(clipping_norm1.0,noise_multiplier0.5,laplace_scaledefaulting tonoise_multiplier;clip_scopenow covered above); theValueErrorfor unsupported mechanisms.- Alias normalization —
normalize_dp_mechanism(gaussian→post_training_gaussian,update→update_gaussian, …) andnormalize_dp_mode(after_training→afterTraining) have no direct tests; the manifest test uses canonical names only. - Error paths in
apply_dp_mechanism—update_gaussianwithout areference_state_dict(ValueError), invalidclip_scope, and unsupported mechanism names are unasserted. - Helper primitives in isolation —
clip_weights(incl. the non-finite-norm guard),compute_state_dict_delta,reconstruct_state_dict_from_delta, andclone_state_dict's mapping-type preservation are only covered indirectly. - Non-tensor state_dict entries — the copy-through branches for non-tensor values are never hit (only tensors appear in test inputs).
- Integration with training —
train_client_modelinvokingresolve_dp_config/apply_dp_mechanism(theafterTraininghook point, and passing the genesis model asreference_state_dictforupdate_gaussian) is out of scope here and untested.