summaryrefslogtreecommitdiff
path: root/src/zbp_scaling/zbp/probes.py
blob: a8e7fe6638f958add8ba4d828ab4e101063789b1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
"""Random probe directions with E[u u^T] = I_d per sample unit.

All families return entries of O(1) magnitude so that `x + eps * u` moves every
coordinate by ~eps.  Divide by sqrt(d) for the fixed-Euclidean-norm variant.
"""
import math
import torch

_HADAMARD_CACHE = {}


def _hadamard(n_pow2, device, dtype):
    key = (n_pow2, str(device), dtype)
    H = _HADAMARD_CACHE.get(key)
    if H is None:
        H = torch.ones(1, 1, dtype=dtype, device=device)
        while H.shape[0] < n_pow2:
            H = torch.cat([torch.cat([H, H], 1), torch.cat([H, -H], 1)], 0)
        _HADAMARD_CACHE[key] = H
    return H


def sample_probes(n, batch_shape, d, family, gen, device, dtype, cycle=None):
    """Return u of shape [n, *batch_shape, d].

    For every sample unit (index into batch_shape) the n probes satisfy
    E[u_i u_i^T] = I_d.  'orthogonal' and 'hadamard' additionally make the n probes of
    one sample unit mutually orthogonal (n <= d).
    """
    B = math.prod(batch_shape) if len(batch_shape) else 1
    if family == "rademacher":
        u = torch.randint(0, 2, (n, B, d), generator=gen, device=device).to(dtype) * 2 - 1
    elif family == "gaussian":
        u = torch.randn((n, B, d), generator=gen, device=device, dtype=dtype)
    elif family == "orthogonal":
        if n > d:
            raise ValueError(f"orthogonal probes need n <= d (n={n}, d={d})")
        A = torch.randn((B, d, n), generator=gen, device=device, dtype=dtype)
        Q, R = torch.linalg.qr(A)                       # [B, d, n], orthonormal columns
        s = torch.sign(torch.diagonal(R, dim1=-2, dim2=-1))  # make the distribution Haar
        s[s == 0] = 1
        Q = Q * s[:, None, :]
        u = Q.permute(2, 0, 1) * math.sqrt(d)           # columns are unit vectors -> scale by sqrt(d)
    elif family == "hadamard":
        d2 = 1 << (d - 1).bit_length()
        if n > d2:
            raise ValueError(f"hadamard probes need n <= {d2}")
        H = _hadamard(d2, device, dtype)                # +-1 entries, orthogonal rows
        signs = torch.randint(0, 2, (B, d2), generator=gen, device=device).to(dtype) * 2 - 1
        rows = torch.argsort(torch.rand((B, d2), generator=gen, device=device), dim=1)[:, :n]
        u = H[rows] * signs[:, None, :]                 # [B, n, d2]
        u = u[:, :, :d].permute(1, 0, 2).contiguous()
    elif family == "hadamard_cycle":
        # Temporal QMC: every unit of a step shares the same n Hadamard rows (advancing by n per step) and the same
        # sign vector (fixed for a whole cycle of d2/n steps), so the projection noise cancels exactly over a cycle
        # for a slowly varying gradient -- at the price of no cross-unit averaging within a step.
        d2 = 1 << (d - 1).bit_length()
        H = _hadamard(d2, device, dtype)
        step = int(cycle or 0)
        start, cyc = (step * n) % d2, (step * n) // d2
        rows = (start + torch.arange(n, device=device)) % d2
        g2 = torch.Generator(device=device)
        g2.manual_seed(1000003 * cyc + 17)
        signs = torch.randint(0, 2, (1, d2), generator=g2, device=device).to(dtype) * 2 - 1
        u = (H[rows] * signs)[:, None, :d].expand(n, B, d).contiguous()
    elif family == "coordinate_all":
        # deterministic sweep of every coordinate (requires n == d): u_i = sqrt(d) e_i, so (1/n) sum u_i u_i^T g = g
        # exactly -- the coordinate-finite-difference estimator of HZO / BOND at its full budget of 2d queries per unit
        if n != d:
            raise ValueError(f"coordinate_all needs n == d (n={n}, d={d})")
        u = (math.sqrt(d) * torch.eye(d, device=device, dtype=dtype))[:, None, :].expand(n, B, d).contiguous()
    elif family == "coordinate":
        idx = torch.randint(0, d, (n, B), generator=gen, device=device)
        u = torch.zeros((n, B, d), device=device, dtype=dtype)
        u.scatter_(2, idx[..., None], math.sqrt(d))
    else:
        raise ValueError(f"unknown probe family {family!r}")
    return u.reshape(n, *batch_shape, d)