Skip to content

Auditing API

auditing

Empirical privacy auditing façade — canary-based with pluggable attacks.

Quick start::

import opake.auditing as auditing
from opake.random import key

cf = auditing.coin_flip(dataset, num_canaries=1000, key=key(42))
train_data = dataset.select(cf.train_indices(len(dataset)))

# ... DP-SGD training loop ...

scores = auditing.loss_scores(loss_fn, params,
                               batch_argnums=(1,),
                               coin_flip=cf, dataset=dataset,
                               batch_size=32, collate_fn=canary_collate)
estimate = auditing.one_run(scores, coin_flip=cf)
print(f"ε (empirical): {estimate.eps_delta().epsilon_at(delta=1e-5):.4f}")

Scoring with coin_flip= + dataset= pairs every score with the dataset index of its canary; one_run joins scores to coin-flip labels by those identifiers, so however the scoring pipeline orders its batches, the audit cannot silently misalign. A collate_fn that reorders examples within a batch is the one exception — identifiers are attached before collation, so keep it order-preserving.

Result data classes (CanaryScores, CoinFlip, OneRunEstimate) live in :mod:opake.auditing.types.

References
  • Xiang et al. (2025), https://arxiv.org/abs/2509.08704
  • Steinke, Nasr, Jagielski (2023), https://arxiv.org/abs/2305.08846
  • Carlini et al. (2022), https://arxiv.org/abs/2112.03570

canary_scores(scores, *, canary_indices)

Attest which canary each externally computed score belongs to.

:func:~opake.auditing.loss_scores and :func:~opake.auditing.gradient_scores already return :class:CanaryScores in verified mode; use this factory for scores computed by some other pipeline. Pass the identifiers in whatever order the scores were computed — :meth:CoinFlip.split_scores joins on them rather than assuming a position.

Parameters:

Name Type Description Default
scores Any

Membership scores, shape (num_canaries,), float.

required
canary_indices Any

Dataset index of the canary behind each score, in the same order as scores.

required

Returns:

Name Type Description
A CanaryScores

class:CanaryScores pairing each score with its identifier.

Raises:

Type Description
ValueError

If either array is not 1-D, the identifiers are not integers, the lengths disagree, or an identifier repeats.

Example::

scores = auditing.canary_scores(values, canary_indices=ids)
estimate = auditing.one_run(scores, coin_flip=cf)
Source code in packages/opake-auditing/src/opake/api/auditing/_coin_flip.py
def canary_scores(scores: Any, *, canary_indices: Any) -> CanaryScores:
    """Attest which canary each externally computed score belongs to.

    :func:`~opake.auditing.loss_scores` and
    :func:`~opake.auditing.gradient_scores` already return
    :class:`CanaryScores` in verified mode; use this factory for scores
    computed by some other pipeline.  Pass the identifiers in whatever
    order the scores were computed — :meth:`CoinFlip.split_scores` joins
    on them rather than assuming a position.

    Args:
        scores: Membership scores, shape ``(num_canaries,)``, float.
        canary_indices: Dataset index of the canary behind each score, in
            the same order as ``scores``.

    Returns:
        A :class:`CanaryScores` pairing each score with its identifier.

    Raises:
        ValueError: If either array is not 1-D, the identifiers are not
            integers, the lengths disagree, or an identifier repeats.

    Example::

        scores = auditing.canary_scores(values, canary_indices=ids)
        estimate = auditing.one_run(scores, coin_flip=cf)
    """
    return CanaryScores(scores, canary_indices=canary_indices)

coin_flip(dataset, *, num_canaries, key, candidate_indices=None)

Create a coin-flip partition for canary-based auditing.

Selects num_canaries examples uniformly without replacement and flips a fair coin for each. Selection uses the whole dataset unless candidate_indices supplies an eligible pool. Fix that pool before target training, independently of the target run's coins and outputs.

Parameters:

Name Type Description Default
dataset Any

Any dataset with len() (HuggingFace or PyTorch).

required
num_canaries int

Number of canary examples to designate.

required
key RngKey

RNG key for reproducible canary selection and coin flips.

required
candidate_indices Any | None

Optional 1-D collection of unique, in-range integer indices eligible to become canaries. Passing exactly num_canaries indices designates them all.

None

Returns:

Name Type Description
A CoinFlip

class:CoinFlip with the canary partition.

Raises:

Type Description
ValueError

If the candidate pool is invalid or too small.

Example::

cf = auditing.coin_flip(dataset, num_canaries=1000, key=key(42))
train_data = dataset.select(cf.train_indices(len(dataset)))
Source code in packages/opake-auditing/src/opake/api/auditing/_coin_flip.py
def coin_flip(
    dataset: Any,
    *,
    num_canaries: int,
    key: RngKey,
    candidate_indices: Any | None = None,
) -> CoinFlip:
    """Create a coin-flip partition for canary-based auditing.

    Selects ``num_canaries`` examples uniformly without replacement and
    flips a fair coin for each. Selection uses the whole dataset unless
    ``candidate_indices`` supplies an eligible pool. Fix that pool before
    target training, independently of the target run's coins and outputs.

    Args:
        dataset: Any dataset with ``len()`` (HuggingFace or PyTorch).
        num_canaries: Number of canary examples to designate.
        key: RNG key for reproducible canary selection and coin flips.
        candidate_indices: Optional 1-D collection of unique, in-range
            integer indices eligible to become canaries. Passing exactly
            ``num_canaries`` indices designates them all.

    Returns:
        A :class:`CoinFlip` with the canary partition.

    Raises:
        ValueError: If the candidate pool is invalid or too small.

    Example::

        cf = auditing.coin_flip(dataset, num_canaries=1000, key=key(42))
        train_data = dataset.select(cf.train_indices(len(dataset)))
    """
    dataset_size = len(dataset)
    if num_canaries < 0:
        raise ConfigurationError(
            *(f"num_canaries must be non-negative, got {num_canaries}",)
        )
    if candidate_indices is None:
        population: int | np.ndarray = dataset_size
        pool_size = dataset_size
        pool_name = "dataset size"
    else:
        population = _candidate_pool(candidate_indices, dataset_size)
        pool_size = population.size
        pool_name = "candidate pool size"
    if num_canaries > pool_size:
        raise ConfigurationError(
            *(f"num_canaries ({num_canaries}) exceeds {pool_name} ({pool_size})",)
        )

    rng = np.random.default_rng(fold_in(key, _CANARY_SELECTION_DOMAIN).seed)
    canary_indices = rng.choice(population, size=num_canaries, replace=False)
    coin_rng = np.random.default_rng(fold_in(key, _COIN_FLIP_DOMAIN).seed)
    in_mask = np.asarray(
        coin_rng.random(num_canaries) < _INCLUSION_PROBABILITY, dtype=bool
    )

    return CoinFlip(
        num_canaries=num_canaries,
        canary_indices=canary_indices,
        _in_mask=in_mask,
        in_indices=canary_indices[in_mask],
        out_indices=canary_indices[~in_mask],
        dataset_size=dataset_size,
    )

gradient_scores(loss_fn, *args, batch_argnums, coin_flip, dataset, reference_scores=None, batch_size=32, collate_fn=None)

Compute membership scores as negative squared per-example gradient norm.

Higher score = smaller gradient norm = more likely a training member. The model parameters must be the first positional argument to loss_fn (position 0), matching the default argnums=0 of :func:~opake.clipped_grad.

Scores the partition's canaries over an internal identifier-carrying loader and returns :class:~opake.auditing.types.CanaryScores, which :func:~opake.auditing.one_run requires. Identifiers are attached per batch before collation, so collate_fn must emit one row per example in the order it received them; reordering within a batch misaligns the pairing undetectably. To audit scores computed by some other pipeline, attest their identifiers with :func:~opake.auditing.canary_scores instead.

Processes one sample at a time to keep peak GPU memory at 2× model size regardless of model or batch size. torch.func.grad is a functional transform that operates independently of the standard autograd context, so the loop runs inside torch.no_grad() for efficiency (same as :func:~opake.api.auditing.attacks._loss.loss_scores).

When reference_scores are provided, the returned scores are current - reference, which equals ||∇loss(θ₀)||² - ||∇loss(θₜ)||² (positive when the current model has smaller gradients, i.e. is more converged). This matches the calibration pattern of :func:~opake.api.auditing.attacks._loss.loss_scores. The reference is aligned by identifier before subtraction.

Parameters:

Name Type Description Default
loss_fn Callable

Per-example loss function whose first argument is the model parameters (a tensor or PyTree). Must return a scalar.

required
*args Any

Non-batched arguments to loss_fn (e.g., model parameters). The first element is differentiated.

()
batch_argnums tuple[int, ...]

Indices of loss_fn positional arguments that come from dataset batches. Must not include 0 (reserved for params). Must be sorted, unique, non-negative.

required
coin_flip CoinFlip

The audit partition to score against.

required
dataset Any

The full dataset the partition was created from; canaries are selected internally by identifier.

required
reference_scores CanaryScores | None

Baseline scores from an untrained model, over the same partition. When provided, returned scores are scores - reference_scores. Typically obtained by calling gradient_scores on the untrained model before training.

None
batch_size int

Batch size of the internal loader. Defaults to 32.

32
collate_fn Callable | None

Collation for the internal loader. Defaults to torch.utils.data.default_collate.

None

Returns:

Name Type Description
A CanaryScores

class:~opake.auditing.types.CanaryScores, shape (n,):

CanaryScores

negated squared gradient norms (higher = more likely member),

CanaryScores

optionally adjusted by reference scores, each carrying its

CanaryScores

canary's dataset index.

Raises:

Type Description
ValueError

If 0 in batch_argnums (params must be at position 0), batch_size is not positive, or collate_fn changes the batch row count.

TypeError

If batch_size is not an int, or reference_scores does not carry identifiers.

Example::

ref = auditing.gradient_scores(
    loss_fn, initial_params,
    batch_argnums=(1,),
    coin_flip=cf, dataset=dataset, collate_fn=canary_collate,
)
scores = auditing.gradient_scores(
    loss_fn, trained_params,
    batch_argnums=(1,),
    coin_flip=cf, dataset=dataset, collate_fn=canary_collate,
    reference_scores=ref,
)
Source code in packages/opake-auditing/src/opake/api/auditing/attacks/_gradient.py
def gradient_scores(
    loss_fn: Callable,
    *args: Any,
    batch_argnums: tuple[int, ...],
    coin_flip: CoinFlip,
    dataset: Any,
    reference_scores: CanaryScores | None = None,
    batch_size: int = 32,
    collate_fn: Callable | None = None,
) -> CanaryScores:
    """Compute membership scores as negative squared per-example gradient norm.

    Higher score = smaller gradient norm = more likely a training member.
    The model parameters must be the first positional argument to
    ``loss_fn`` (position 0), matching the default ``argnums=0`` of
    :func:`~opake.clipped_grad`.

    Scores the partition's canaries over an internal identifier-carrying
    loader and returns :class:`~opake.auditing.types.CanaryScores`, which
    :func:`~opake.auditing.one_run` requires.  Identifiers are attached
    per batch before collation, so ``collate_fn`` must emit one row per
    example in the order it received them; reordering within a batch
    misaligns the pairing undetectably.  To audit scores computed by some
    other pipeline, attest their identifiers with
    :func:`~opake.auditing.canary_scores` instead.

    Processes one sample at a time to keep peak GPU memory at 2× model
    size regardless of model or batch size.  ``torch.func.grad`` is a
    functional transform that operates independently of the standard
    autograd context, so the loop runs inside ``torch.no_grad()`` for
    efficiency (same as :func:`~opake.api.auditing.attacks._loss.loss_scores`).

    When ``reference_scores`` are provided, the returned scores are
    ``current - reference``, which equals
    ``||∇loss(θ₀)||² - ||∇loss(θₜ)||²`` (positive when the current model
    has smaller gradients, i.e. is more converged).  This matches the calibration pattern
    of :func:`~opake.api.auditing.attacks._loss.loss_scores`.  The
    reference is aligned by identifier before subtraction.

    Args:
        loss_fn: Per-example loss function whose first argument is the
            model parameters (a tensor or PyTree).  Must return a scalar.
        *args: Non-batched arguments to ``loss_fn`` (e.g., model parameters).
            The first element is differentiated.
        batch_argnums: Indices of ``loss_fn`` positional arguments that come
            from dataset batches.  Must not include 0 (reserved for params).
            Must be sorted, unique, non-negative.
        coin_flip: The audit partition to score against.
        dataset: The full dataset the partition was created from; canaries
            are selected internally by identifier.
        reference_scores: Baseline scores from an untrained model, over the
            same partition.  When provided, returned scores are
            ``scores - reference_scores``.  Typically obtained by calling
            ``gradient_scores`` on the untrained model before training.
        batch_size: Batch size of the internal loader. Defaults to 32.
        collate_fn: Collation for the internal loader. Defaults to
            ``torch.utils.data.default_collate``.

    Returns:
        A :class:`~opake.auditing.types.CanaryScores`, shape ``(n,)``:
        negated squared gradient norms (higher = more likely member),
        optionally adjusted by reference scores, each carrying its
        canary's dataset index.

    Raises:
        ValueError: If ``0 in batch_argnums`` (params must be at position
            0), ``batch_size`` is not positive, or ``collate_fn`` changes
            the batch row count.
        TypeError: If ``batch_size`` is not an int, or ``reference_scores``
            does not carry identifiers.

    Example::

        ref = auditing.gradient_scores(
            loss_fn, initial_params,
            batch_argnums=(1,),
            coin_flip=cf, dataset=dataset, collate_fn=canary_collate,
        )
        scores = auditing.gradient_scores(
            loss_fn, trained_params,
            batch_argnums=(1,),
            coin_flip=cf, dataset=dataset, collate_fn=canary_collate,
            reference_scores=ref,
        )
    """
    import torch

    from opake.api.engine.pytree import global_norm

    _validate_batch_argnums(batch_argnums, len(args))

    if 0 in batch_argnums:
        raise ConfigurationError(
            *(
                "gradient_scores differentiates w.r.t. the first argument "
                "(position 0), which must not be in batch_argnums. "
                f"Got batch_argnums={batch_argnums}.",
            )
        )

    loader = _scoring_loader(
        dataset=dataset,
        coin_flip=coin_flip,
        batch_size=batch_size,
        collate_fn=collate_fn,
        reference_scores=reference_scores,
    )

    grad_fn = torch.func.grad(loss_fn)

    all_positions: list[int] = []
    all_scores: list[float] = []
    with torch.no_grad():
        for positions, batch in loader:
            all_positions.extend(int(position) for position in positions)
            batch_tensors = _extract_batch_tensors(batch, batch_argnums)
            n_in_batch = batch_tensors[0].shape[0]

            for j in range(n_in_batch):
                single_tensors = tuple(t[j] for t in batch_tensors)
                full_args = _merge_args(args, single_tensors, batch_argnums)

                grad_pytree = grad_fn(*full_args)
                norm = global_norm(grad_pytree)
                all_scores.append(-norm.pow(2).item())
                del grad_pytree

    scores = np.array(all_scores)

    return _bind_scores(
        scores,
        all_positions,
        coin_flip=coin_flip,
        reference_scores=reference_scores,
    )

loss_scores(loss_fn, *args, batch_argnums, coin_flip, dataset, reference_scores=None, batch_size=32, collate_fn=None)

Compute membership scores as negative per-example loss.

Higher score = lower loss = more likely a training member. Uses torch.func.vmap for per-example loss computation, following the same batch_argnums convention as :func:~opake.clipped_grad.

The scorer builds its own loader over the partition's canaries and pairs every score with the dataset index of the example that produced it, returning :class:~opake.auditing.types.CanaryScores — the form :func:~opake.auditing.one_run requires. The pairing is joined by identifier, so no iteration order over the batches can misalign it. collate_fn receives the raw canary examples and must return one row per example in the order it received them; identifiers are attached per batch before collation, so a collate_fn that reorders within a batch does misalign the pairing and cannot be detected.

To audit scores computed by some other pipeline, attest their identifiers with :func:~opake.auditing.canary_scores instead.

When reference_scores are provided, the returned scores are current - reference, which equals loss(w0) - loss(wt) (the loss reduction from initial to current model). This matches Algorithm 3 of Steinke et al. (2023). The reference is aligned by identifier before subtraction.

Parameters:

Name Type Description Default
loss_fn Callable

Per-example loss function, same as used with :func:~opake.clipped_grad. Must be vmap-compatible.

required
*args Any

Non-batched arguments to loss_fn (e.g., model parameters).

()
batch_argnums tuple[int, ...]

Indices of loss_fn positional arguments that come from dataset batches. Must be sorted, unique, non-negative.

required
coin_flip CoinFlip

The audit partition to score against.

required
dataset Any

The full dataset the partition was created from; canaries are selected internally by identifier.

required
reference_scores CanaryScores | None

Baseline scores from an untrained model, over the same partition. When provided, returned scores are the loss reduction scores - reference_scores. Typically obtained by calling loss_scores on the untrained model before training.

None
batch_size int

Batch size of the internal loader. Defaults to 32.

32
collate_fn Callable | None

Collation for the internal loader. Defaults to torch.utils.data.default_collate.

None

Returns:

Name Type Description
A CanaryScores

class:~opake.auditing.types.CanaryScores, shape (n,):

CanaryScores

negated losses (higher = more likely member), optionally adjusted

CanaryScores

by reference scores, each carrying its canary's dataset index.

Raises:

Type Description
ValueError

If batch_argnums is malformed, batch_size is not positive, or collate_fn changes the batch row count.

TypeError

If batch_size is not an int, or reference_scores does not carry identifiers.

Example (HuggingFace pattern)::

def canary_collate(examples):
    batch = data_collator(examples)
    return (batch["input_ids"].to(device),)

ref = auditing.loss_scores(
    loss_fn, initial_params,
    batch_argnums=(1,),
    coin_flip=cf, dataset=dataset,
    batch_size=32, collate_fn=canary_collate,
)
scores = auditing.loss_scores(
    loss_fn, trained_params,
    batch_argnums=(1,),
    coin_flip=cf, dataset=dataset,
    batch_size=32, collate_fn=canary_collate,
    reference_scores=ref,
)
estimate = auditing.one_run(scores, coin_flip=cf)
Source code in packages/opake-auditing/src/opake/api/auditing/attacks/_loss.py
def loss_scores(
    loss_fn: Callable,
    *args: Any,
    batch_argnums: tuple[int, ...],
    coin_flip: CoinFlip,
    dataset: Any,
    reference_scores: CanaryScores | None = None,
    batch_size: int = 32,
    collate_fn: Callable | None = None,
) -> CanaryScores:
    """Compute membership scores as negative per-example loss.

    Higher score = lower loss = more likely a training member.
    Uses ``torch.func.vmap`` for per-example loss computation, following
    the same ``batch_argnums`` convention as :func:`~opake.clipped_grad`.

    The scorer builds its own loader over the partition's canaries and
    pairs every score with the dataset index of the example that produced
    it, returning :class:`~opake.auditing.types.CanaryScores` — the form
    :func:`~opake.auditing.one_run` requires.  The pairing is joined by
    identifier, so no iteration order over the batches can misalign it.
    ``collate_fn`` receives the raw canary examples and must return one
    row per example in the order it received them; identifiers are
    attached per batch before collation, so a ``collate_fn`` that reorders
    within a batch does misalign the pairing and cannot be detected.

    To audit scores computed by some other pipeline, attest their
    identifiers with :func:`~opake.auditing.canary_scores` instead.

    When ``reference_scores`` are provided, the returned scores are
    ``current - reference``, which equals ``loss(w0) - loss(wt)``
    (the loss reduction from initial to current model). This matches
    Algorithm 3 of Steinke et al. (2023).  The reference is aligned by
    identifier before subtraction.

    Args:
        loss_fn: Per-example loss function, same as used with
            :func:`~opake.clipped_grad`. Must be vmap-compatible.
        *args: Non-batched arguments to ``loss_fn`` (e.g., model parameters).
        batch_argnums: Indices of ``loss_fn`` positional arguments that come
            from dataset batches. Must be sorted, unique, non-negative.
        coin_flip: The audit partition to score against.
        dataset: The full dataset the partition was created from; canaries
            are selected internally by identifier.
        reference_scores: Baseline scores from an untrained model, over the
            same partition. When provided, returned scores are the loss
            reduction ``scores - reference_scores``. Typically obtained by
            calling ``loss_scores`` on the untrained model before training.
        batch_size: Batch size of the internal loader. Defaults to 32.
        collate_fn: Collation for the internal loader. Defaults to
            ``torch.utils.data.default_collate``.

    Returns:
        A :class:`~opake.auditing.types.CanaryScores`, shape ``(n,)``:
        negated losses (higher = more likely member), optionally adjusted
        by reference scores, each carrying its canary's dataset index.

    Raises:
        ValueError: If ``batch_argnums`` is malformed, ``batch_size`` is
            not positive, or ``collate_fn`` changes the batch row count.
        TypeError: If ``batch_size`` is not an int, or ``reference_scores``
            does not carry identifiers.

    Example (HuggingFace pattern)::

        def canary_collate(examples):
            batch = data_collator(examples)
            return (batch["input_ids"].to(device),)

        ref = auditing.loss_scores(
            loss_fn, initial_params,
            batch_argnums=(1,),
            coin_flip=cf, dataset=dataset,
            batch_size=32, collate_fn=canary_collate,
        )
        scores = auditing.loss_scores(
            loss_fn, trained_params,
            batch_argnums=(1,),
            coin_flip=cf, dataset=dataset,
            batch_size=32, collate_fn=canary_collate,
            reference_scores=ref,
        )
        estimate = auditing.one_run(scores, coin_flip=cf)
    """
    import torch

    _validate_batch_argnums(batch_argnums, len(args))
    loader = _scoring_loader(
        dataset=dataset,
        coin_flip=coin_flip,
        batch_size=batch_size,
        collate_fn=collate_fn,
        reference_scores=reference_scores,
    )

    # Build in_dims for vmap: None for non-batch args, 0 for batch args
    n_args = len(args) + len(batch_argnums)
    in_dims = tuple(0 if i in batch_argnums else None for i in range(n_args))
    per_example_fn = torch.func.vmap(loss_fn, in_dims=in_dims)

    all_positions: list[int] = []
    all_scores: list[np.ndarray] = []
    with torch.no_grad():
        for positions, batch in loader:
            all_positions.extend(int(position) for position in positions)
            batch_tensors = _extract_batch_tensors(batch, batch_argnums)

            full_args = _merge_args(args, batch_tensors, batch_argnums)

            losses = per_example_fn(*full_args)
            all_scores.append(-losses.detach().cpu().numpy())

    scores = np.concatenate(all_scores) if all_scores else np.empty(0)

    return _bind_scores(
        scores,
        all_positions,
        coin_flip=coin_flip,
        reference_scores=reference_scores,
    )

Module functions

coin_flip

auditing.coin_flip(
    dataset, *, num_canaries, key,
    candidate_indices=None,
) -> CoinFlip

Create a coin-flip partition. Randomly selects num_canaries examples and flips a fair coin for each to decide inclusion/exclusion. Selection uses the whole dataset unless candidate_indices restricts it to a pool.

Parameter Type Default Description
dataset any with len() required Full training dataset
num_canaries int required Number of canaries to designate
key RngKey required RNG key for reproducibility
candidate_indices array-like | None None Unique, in-range integer indices eligible to become canaries; order is ignored

Raises ConfigurationError if the candidate pool is malformed or contains fewer than num_canaries indices.

cf = auditing.coin_flip(dataset, num_canaries=1000, key=key(42))
train_data = dataset.select(cf.train_indices(len(dataset)))

To designate a precommitted block of constructed canaries:

pool = range(len(dataset), len(audited_dataset))
cf = auditing.coin_flip(
    audited_dataset,
    num_canaries=len(pool),
    key=key(42),
    candidate_indices=pool,
)

See Stronger canaries for the statistical and accounting requirements.


loss_scores

auditing.loss_scores(
    loss_fn, *args, *,
    batch_argnums, coin_flip, dataset,
    reference_scores=None,
    batch_size=32, collate_fn=None,
) -> CanaryScores

Compute per-example membership scores as negative loss. Higher score = lower loss = more likely a training member.

Builds an internal loader over the partition's canaries and pairs every score with the dataset index of the example that produced it. Returns CanaryScores — the form one_run requires. The pairing is joined by identifier, so no iteration order over the batches can misalign it. Identifiers are attached per batch before collation, so a collate_fn that reorders examples within a batch misaligns the pairing and cannot be detected; a collate_fn that drops or adds rows raises.

To audit scores computed by some other pipeline, attest their identifiers with canary_scores instead.

Parameter Type Default Description
loss_fn Callable required Per-example loss function (vmap-compatible)
*args any — Non-batched arguments (e.g., model parameters)
batch_argnums tuple[int, ...] required Which loss_fn args are batched (same as clipped_grad)
coin_flip CoinFlip required The audit partition to score against
dataset any required The full dataset the partition was created from
reference_scores CanaryScores None Baseline scores to subtract (e.g., from untrained model), aligned by identifier
batch_size int 32 Batch size of the internal loader
collate_fn Callable default collate Collates raw canary examples into a batch for loss_fn; must emit one row per example, in the order received

Returns CanaryScores of shape (n,), one score per canary. Higher = more likely member.

Raises ConfigurationError if batch_argnums is malformed, batch_size is not positive, or collate_fn changes the batch row count; InputTypeError if reference_scores does not carry identifiers.

def canary_collate(examples):
    batch = data_collator(examples)
    return (batch["input_ids"].to(device),)

scores = auditing.loss_scores(
    loss_fn, params,
    batch_argnums=(1,),
    coin_flip=cf, dataset=dataset,
    batch_size=32, collate_fn=canary_collate,
)
estimate = auditing.one_run(scores, coin_flip=cf)

gradient_scores

auditing.gradient_scores(
    loss_fn, *args, *,
    batch_argnums, coin_flip, dataset,
    reference_scores=None,
    batch_size=32, collate_fn=None,
) -> CanaryScores

Compute per-example membership scores as negative squared gradient norm. Higher score = smaller gradient norm = more likely a training member.

This is a white-box attack that differentiates with respect to the first loss_fn argument (model parameters). Therefore 0 must not appear in batch_argnums.

Scores the partition's canaries over an internal identifier-carrying loader, with the same pairing guarantees and collate_fn obligation as loss_scores.

Parameter Type Default Description
loss_fn Callable required Per-example scalar loss function
*args any — Non-batched arguments; first arg is differentiated
batch_argnums tuple[int, ...] required Batched argument indices; must exclude 0
coin_flip CoinFlip required The audit partition to score against
dataset any required The full dataset the partition was created from
reference_scores CanaryScores None Baseline scores to subtract, aligned by identifier
batch_size int 32 Batch size of the internal loader
collate_fn Callable default collate Must emit one row per example, in the order received

Returns CanaryScores of shape (n,), one score per canary. Higher = more likely member.

Raises ConfigurationError if 0 in batch_argnums, batch_size is not positive, or collate_fn changes the batch row count; InputTypeError if reference_scores does not carry identifiers.

ref = auditing.gradient_scores(
    loss_fn, initial_params,
    batch_argnums=(1,),
    coin_flip=cf, dataset=dataset,
)
scores = auditing.gradient_scores(
    loss_fn, trained_params,
    batch_argnums=(1,),
    coin_flip=cf, dataset=dataset,
    reference_scores=ref,
)

The scorer is also available from the attacks namespace:

from opake.auditing.attacks import gradient_scores

canary_scores

auditing.canary_scores(
    scores, *, canary_indices,
) -> CanaryScores

Attest which canary each externally computed score belongs to. The built-in scorers already return CanaryScores in verified mode; use this for scores produced by another pipeline. Identifiers may be in any order — one_run joins on them rather than assuming a position.

Parameter Type Default Description
scores array-like required Membership scores, shape (n,), float
canary_indices array-like required Dataset index behind each score, same order as scores

Raises ConfigurationError if either array is not 1-D, the identifiers are not integers, the lengths disagree, or an identifier repeats.

scores = auditing.canary_scores(values, canary_indices=ids)
estimate = auditing.one_run(scores, coin_flip=cf)

one_run

auditing.one_run(scores, *, coin_flip) -> OneRunEstimate

Build a one-run privacy estimate from canary scores. Joins scores to the coin-flip partition by canary identifier, precomputes the empirical ROC, and returns a frozen estimate. The join makes scoring order irrelevant; identifiers that do not cover the partition's canaries one-to-one raise instead of silently producing a meaningless estimate.

Parameter Type Default Description
scores CanaryScores required Per-canary membership scores with identifiers, from verified scoring or explicit attestation
coin_flip CoinFlip required The coin-flip partition

Raises InputTypeError for bare arrays without identifiers; ConfigurationError if identifiers are unexpected, duplicated, or missing.

estimate = auditing.one_run(scores, coin_flip=cf)
print(estimate.epsilon_at(delta=1e-5))

CoinFlip

Coin-flip partitioning for canary-based privacy auditing. Each canary is independently included or excluded with probability 0.5.

Create instances with coin_flip. To control which records are eligible, pass candidate_indices to that factory rather than constructing the state carrier directly.

Attribute Type Description
num_canaries int Total canary count
canary_indices np.ndarray All canary dataset indices
in_indices np.ndarray Included in training (heads)
out_indices np.ndarray Excluded from training (tails)

train_indices

cf.train_indices(dataset_size) -> list[int]

All indices except held-out canaries. Returns list[int] for HuggingFace dataset.select().

split_scores

cf.split_scores(scores) -> tuple[np.ndarray, np.ndarray]

Split per-canary scores into (in_scores, out_scores), joining CanaryScores to the partition by identifier. Bare arrays raise InputTypeError; identifiers that do not join one-to-one onto canary_indices raise ConfigurationError.


CanaryScores

class CanaryScores(scores, canary_indices)  # frozen dataclass

Membership scores paired with stable canary identifiers: scores[k] was computed for dataset example canary_indices[k]. Produced by the scoring functions in verified mode; build one with canary_scores to attest identifiers for scores computed elsewhere (any order — one_run joins by identifier). Arrays are defensively copied, validated (1-D, equal length, unique integer identifiers), and marked read-only.

Attribute Type Description
scores np.ndarray Membership scores, shape (n,), float, read-only
canary_indices np.ndarray Dataset index of the canary behind each score

Supports len() and np.asarray() (yields the score values).


OneRunEstimate

class OneRunEstimate  # frozen dataclass

Precomputed one-run audit estimate, returned by auditing.one_run(). Holds the empirical ROC counts and exposes:

  • a default audit-method surface (epsilon_at, delta_at, beta_at, advantage) that dispatches to μ-GDP — the paper-recommended default for Gaussian-DP mechanisms;
  • two method factories (eps_delta, gdp) for explicit override;
  • attack-side empirical metrics (attack_auc, attack_beta_at).
Attribute Type Description
n_in int Number of held-in canaries
n_out int Number of held-out canaries
canary_indices np.ndarray \| None Stable example identifiers: dataset indices of the audited canaries, in partition order (always populated by one_run)

epsilon_at

estimate.epsilon_at(*, delta, significance=0.05, threshold=None) -> float

Epsilon lower bound from the default μ-GDP audit method. Equivalent to self.gdp().epsilon_at(...). Requires delta > 0. For non-Gaussian-DP mechanisms use self.eps_delta().epsilon_at(...) explicitly.

delta_at

estimate.delta_at(*, epsilon, significance=0.05, threshold=None) -> float

δ(ε) from the default μ-GDP audit method. Equivalent to self.gdp().delta_at(...).

beta_at

estimate.beta_at(*, alpha, significance=0.05, threshold=None) -> float

Theoretical f-DP β at α under the inferred μ̂-GDP. Equivalent to self.gdp().beta_at(...). For the empirical attack ROC β, see attack_beta_at.

advantage

estimate.advantage(*, significance=0.05, threshold=None) -> float

Total-variation advantage at the inferred μ̂-GDP. Equivalent to self.gdp().advantage().

eps_delta

estimate.eps_delta() -> EpsDeltaMethod

Mechanism-agnostic (ε, δ)-DP order-statistics audit method (Xiang et al. 2025). Use for non-Gaussian-DP mechanisms or pure ε-DP auditing. See EpsDeltaMethod below.

gdp

estimate.gdp(*, grid_size=10_000) -> GdpMethod

μ-GDP order-statistics audit method (Xiang et al. 2025) with a tunable integration grid. grid_size must be at least 1_000: coarser grids can resolve a detectable leak as epsilon=0. The top-level epsilon_at / delta_at / beta_at / advantage are shortcuts for gdp() with the default grid_size. See GdpMethod below.

attack_auc

estimate.attack_auc(*, confidence=None, num_samples=1000, key=None) -> float | tuple[float, float]

Empirical ROC AUC of the membership inference attack. Returns point estimate by default, or (lower, upper) CI tuple when confidence is provided. A CI requires an explicit key so bootstrap resampling is reproducible and isolated from global NumPy state. Independent of the audit method.

attack_beta_at

estimate.attack_beta_at(*, alpha) -> float | np.ndarray

Empirical attack β at given FPR: 1 − TPR interpolated from the empirical ROC. Distinct from beta_at which is the theoretical f-DP β at the inferred μ̂-GDP.


EpsDeltaMethod

Returned by estimate.eps_delta(). Exposes the two query directions along the audit's (ε, δ) boundary.

epsilon_at

method.epsilon_at(*, delta=0.0, significance=0.05, threshold=None) -> float

Largest ε the audit can certify at the given δ. delta=0 is allowed (pure ε-DP). When threshold is provided, accuracy is evaluated at that specific score threshold; otherwise the Pareto-optimal threshold maximising TP + TN is used.

delta_at

method.delta_at(*, epsilon, significance=0.05, threshold=None) -> float

Largest δ at which the audit certifies ε ≥ epsilon. Returns 0.0 when epsilon is unreachable even at δ=0.

EpsDeltaMethod deliberately does not expose beta_at / advantage: the (ε, δ)-DP trade-off function is a family envelope, so those metrics would be worst-case across the family rather than instance-specific — use GdpMethod for sharp f-DP queries on Gaussian-DP mechanisms.


GdpMethod

Returned by estimate.gdp(grid_size=). Mirrors the full Pld metric surface: all four methods derive from a single inferred μ̂.

Attribute Type Default Description
grid_size int 10_000 Grid points for the order-statistics integration; minimum 1_000

epsilon_at

method.epsilon_at(*, delta, significance=0.05, threshold=None) -> float

Largest ε the audit can certify at the given δ. Requires delta > 0 (μ-GDP is incompatible with pure ε-DP).

delta_at

method.delta_at(*, epsilon, significance=0.05, threshold=None) -> float

δ(ε) under the inferred μ̂-GDP guarantee, via the closed-form GDP relation.

beta_at

method.beta_at(*, alpha, significance=0.05, threshold=None) -> float

f-DP Type-II error at α under the inferred μ̂-GDP: β(α; μ) = Φ(Φ⁻¹(1 − α) − μ̂). Theoretical β of the post-audit guarantee, distinct from OneRunEstimate.attack_beta_at (empirical attack ROC).

advantage

method.advantage(*, significance=0.05, threshold=None) -> float

Total-variation advantage at the inferred μ̂-GDP: TV(μ) = 2·Φ(μ̂/2) − 1.


Quick reference

auditing.coin_flip(dataset, ...) Coin-flip partition → CoinFlip
auditing.coin_flip(dataset, ..., candidate_indices=pool) Partition a precommitted canary pool
auditing.loss_scores(loss_fn, ..., coin_flip=cf, dataset=ds) Verified membership scores → CanaryScores
auditing.gradient_scores(loss_fn, ..., coin_flip=cf, dataset=ds) Verified white-box scores → CanaryScores
auditing.one_run(scores, coin_flip=cf) Estimate privacy → OneRunEstimate
auditing.canary_scores(values, canary_indices=...) Attest identifiers for externally computed scores
cf.train_indices(len(dataset)) Training indices for dataset.select()
estimate.epsilon_at(delta=) ε bound (μ-GDP default)
estimate.delta_at(epsilon=) δ at given ε
estimate.beta_at(alpha=) Theoretical β at α (μ-GDP)
estimate.advantage() TV advantage at inferred μ̂
estimate.eps_delta() Mechanism-agnostic (ε, δ)-DP method (override)
estimate.gdp(grid_size=) μ-GDP method with tunable grid
estimate.attack_auc() Empirical attack AUC
estimate.attack_auc(confidence=, key=) Empirical AUC with CI
estimate.attack_beta_at(alpha=) Empirical attack β at given FPR