recall_guard
recall_guard
recall_guard: measured inference-without-recall.
The public entry point. A consumer typically needs only::
from recall_guard import MemoryGuardedScorer
scorer = MemoryGuardedScorer.calibrate(
api_key=..., # NVIDIA NIM key
model="meta/llama-3.1-8b-instruct",
is_memorized=[...], # prompts dated before the model's cutoff
oos_control=[...], # prompts dated after it
)
guarded = scorer.score("<your prompt>")
guarded.signal, guarded.p_memorized, guarded.memguard_confidence
This module re-exports the façade plus a curated set of core and mia
primitives. It deliberately re-exports no plotting symbol and nothing from the
portfolio (backtest) layer, so import recall_guard never pulls in
matplotlib or vectorbt (Req 4.1, 4.3). The plotting helpers remain available, on
demand, via recall_guard.harness (lazy) and the backtest engine via
recall_guard.portfolio.backtest (requires the backtest extra).
LOGPROB_FLOOR
module-attribute
LOGPROB_FLOOR = -30.0
Lower bound for individual logprob values, applied before averaging.
Prevents a single -inf (or extremely negative) per-token logprob from
poisoning loss / min_k / zlib_ratio / ref_delta.
CompletionResult
dataclass
Single chat-completion response with logprobs.
Attributes:
| Name | Type | Description |
|---|---|---|
content |
str
|
Assistant message content. |
logprobs |
list[TokenLogprob]
|
Per-token logprob entries. |
raw_temperature_observed |
float | None
|
The temperature the API reported as honoured, when exposed. |
Source code in recall_guard/core/nvidia_lm.py
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | |
CostEstimate
dataclass
What an ensemble would cost, computed without issuing anything.
Source code in recall_guard/core/ensemble.py
90 91 92 93 94 95 | |
CutoffViolation
Bases: Exception
Raised when shortlisted models post-date the eval set's cutoff.
Source code in recall_guard/core/loader.py
44 45 | |
EnsembleResult
dataclass
One ensemble's reduced answer plus the evidence behind it.
component_verdicts carries the separated-cluster check for every
component, not only the flagged ones. A verdict of separated=False with
masses near the threshold is a very different situation from one with no
mass on either side, and only the caller can judge which matters -- so the
result reports what the test saw rather than only its boolean conclusion. A
None verdict means the check did not run at all.
max_tokens and temperature record the settings the draws were taken
under. An ensemble is an audit artifact, and "under what generation settings"
belongs next to the draw-set digest: a consensus sampled at a different token
budget than production is not measuring the production decision.
sampled_at records when the draws were taken, and is None for a
result produced by replaying a stored draw set -- which is the honest
answer, because a replay was not sampled. It exists because the sampled
distribution moves between sessions as well as within one: the same prompt
against the same model id has been observed to shift a component's median
materially over two days. So a consensus has a shelf life, draws_sha256
pins which draws produced it but nothing else pins when, and a stored
corpus is not ground truth against which to judge a fresh ensemble.
Source code in recall_guard/core/ensemble.py
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | |
EnsembleSpec
dataclass
Opt-in ensemble configuration.
Every default here is provisional: all of them were calibrated against a single measurement date at a crisis onset, chosen because it was the hard case. Whether they generalise to calmer regimes is unmeasured, which is why each threshold is a field rather than a literal.
max_tokens and temperature default to None, meaning the client's
own defaults. Set them to whatever production uses. An ensemble drawn at
a different token budget is not measuring the production decision -- and on a
reasoning model the budget is not a detail, because the chain of thought
consumes it and truncates the reply before the payload a caller parses.
Measured on one such model, dropping from a 2048-token production budget to
the 512-token client default took the parse rate from 95% to 48%.
draws is sized for agreement precision, not for component-split
detection; those are different numbers and the second is larger. See
:func:~recall_guard.core.consensus.smallest_detectable_split_n.
Source code in recall_guard/core/ensemble.py
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 | |
smallest_certifiable_n
property
smallest_certifiable_n
Draws needed to certify agreement_target, or None if unset.
Unanimity is the best case, so this is a hard floor -- below it no observed agreement can clear the target, whatever the model returns.
EvalRow
dataclass
One evaluation row: prompt + ground-truth direction + opaque metadata.
Source code in recall_guard/core/loader.py
48 49 50 51 52 53 54 | |
EvalSet
dataclass
A loaded JSONL eval set plus its cutoff header and content hash.
Source code in recall_guard/core/loader.py
57 58 59 60 61 62 63 | |
LocationMode
Bases: StrEnum
Which location estimator to apply to an unflagged component.
Source code in recall_guard/core/ensemble.py
63 64 65 66 67 68 | |
Manifest
dataclass
Per-run reproducibility manifest written to <out_dir>/manifest.json.
Fields mirror the design's core.manifest Service Interface verbatim.
The dataclass is frozen so callers cannot mutate a manifest after it has
been hashed/written, which keeps the persisted manifest.json faithful
to whatever the runner actually saw.
Source code in recall_guard/core/manifest.py
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 | |
MultimodalAction
Bases: StrEnum
What to do with a component that holds separated clusters.
Silently averaging across one is the single behaviour that must never be available: it launders a real disagreement into false precision, returning a value the model effectively never emitted.
Source code in recall_guard/core/ensemble.py
71 72 73 74 75 76 77 78 79 80 | |
MultimodalVerdict
dataclass
Outcome of the separated-cluster check for one component.
separated is the gate: when it is true the component holds two clusters
with a genuine gap between them, there is no single location to estimate,
and a location estimator must not be run at all.
Source code in recall_guard/core/consensus.py
330 331 332 333 334 335 336 337 338 339 340 341 342 343 | |
NvidiaLM
Thin HTTP client around the NVIDIA OpenAI-compatible chat endpoint.
Always sends logprobs=True and top_logprobs=20. The default
temperature is 0.0 (per Req 10.3) and can be overridden per call.
Source code in recall_guard/core/nvidia_lm.py
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 | |
generate
generate(prompt, temperature=0.0, max_tokens=512)
Send a single chat completion and return parsed logprobs.
Caps response length at max_tokens (default 512) so reasoning
models (gpt-oss-, nemotron-nano-) have enough budget to finish
their reasoning chain AND emit the final Direction: /
Confidence: lines. Non-reasoning models stop early on EOS so
the higher cap costs nothing for them.
Raises:
| Type | Description |
|---|---|
TimeoutError
|
If the underlying HTTP call times out. |
RuntimeError
|
If the response body lacks |
Source code in recall_guard/core/nvidia_lm.py
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | |
ReferenceMode
Bases: StrEnum
Whether the optional reference draw varies per ensemble draw.
Source code in recall_guard/core/ensemble.py
83 84 85 86 87 | |
Tail
Bases: StrEnum
Which tail an interval's confidence level refers to.
This must be declared rather than assumed: the draw count needed to certify an agreement target differs substantially between the two conventions, so a feasibility check evaluated against the wrong one is meaningless.
Source code in recall_guard/core/consensus.py
48 49 50 51 52 53 54 55 56 57 | |
TokenLogprob
dataclass
Per-token logprob record returned by the NVIDIA OpenAI-compatible API.
Source code in recall_guard/core/nvidia_lm.py
63 64 65 66 67 68 69 | |
ConfigurationError
Bases: RuntimeError
Raised when the NIM credential is absent, empty, or rejected (Req 3.7).
Source code in recall_guard/harness/scorer.py
82 83 | |
EnsembledScore
dataclass
One prompt scored over many draws.
p_memorized_point is the exposure multiplier. consensus.p_memorized
is the score of one actually-observed draw and is evidence only -- the two
can differ, because the representative draw is selected by rank while the
point estimate is a reduction over all draws. Using the consensus draw's
score to scale exposure would silently substitute a single draw for the
ensemble, which is the thing this feature exists to stop.
sampled_at records when the draws were taken. The sampled distribution
moves between sessions as well as within one, so a consensus has a shelf
life and a cached one is not the same as a fresh one.
p_memorized_point is None exactly when the ensemble failed, and it is
never 0.0 in that case: zero would mean "pass 100% of exposure through",
the opposite of what an unusable measurement should imply.
Source code in recall_guard/harness/scorer.py
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | |
GuardedScore
dataclass
One guarded inference result.
Attributes:
| Name | Type | Description |
|---|---|---|
prompt_hash |
str
|
First 16 hex chars of |
parse_ok |
bool
|
|
signal |
int | None
|
Parsed direction in |
raw_confidence |
float | None
|
Parsed confidence in |
p_memorized |
float | None
|
Calibrated |
memguard_confidence |
float | None
|
|
features |
MiaFeatures | None
|
The raw :class: |
fail_reason |
str | None
|
One of |
Source code in recall_guard/harness/scorer.py
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | |
MemoryGuardedScorer
Calibrated, per-model inference-without-recall scorer.
Construct via :meth:calibrate (which performs the model calls and training),
then call :meth:score / :meth:score_many.
Source code in recall_guard/harness/scorer.py
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 | |
holdout_auc
property
holdout_auc
Held-out IS/OOS separation of the trained calibrator (Req 3.4).
is_weak
property
is_weak
True when holdout_auc is below the calibration gate (Req 3.4).
calibrate
classmethod
calibrate(
*,
api_key,
model,
is_memorized,
oos_control,
reference_model=None,
min_auc=0.6,
min_valid=50,
seed=0,
max_workers=8,
timeout_s=45.0,
min_call_interval_s=0.0,
lm_factory=None,
)
Build the control baseline and train the MCS calibrator for model.
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If |
ValueError
|
If a class has too few usable rows to calibrate / train (Req 3.4). |
Source code in recall_guard/harness/scorer.py
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 | |
score
score(prompt)
Score one prompt into a :class:GuardedScore.
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If the NIM endpoint rejects the credential while scoring (Req 3.7). |
Source code in recall_guard/harness/scorer.py
296 297 298 299 300 301 302 303 304 305 306 | |
score_many
score_many(prompts, *, max_workers=8)
Score many prompts (parallel LM calls); preserves input order.
Source code in recall_guard/harness/scorer.py
308 309 310 311 312 313 314 315 316 317 318 319 | |
score_ensemble
score_ensemble(prompt, *, spec, conservative_quantile=None)
Score one prompt over spec.draws draws and reduce the results.
A single scoring is close to uninformative as an exposure multiplier: measured on one identical prompt, its 95% band spans two thirds of the unit interval. Ensembling narrows that, and reports what is left.
Each draw is scored through the unchanged single-draw path and the resulting scores are then reduced. Averaging the intermediate features and scoring once would be a different quantity -- the calibrator is a sigmoid, so the score of the mean is not the mean of the scores.
The point estimate is the mean, because attenuation is linear in the score and the mean is therefore unbiased for expected attenuation. No symmetric trimming is applied: the upper tail of this distribution is the contamination evidence the score exists to report, so trimming it away would discard the signal and shift the estimate toward the risk-increasing side.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
EnsembleSpec
|
Explicit configuration. There is no default instance. |
required |
conservative_quantile
|
float | None
|
Optional upper quantile of the score, for a caller who would rather withhold more exposure than risk withholding too little. |
None
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If the endpoint rejects the credential while drawing. |
Source code in recall_guard/harness/scorer.py
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 | |
ControlBaseline
dataclass
Per-model baseline distribution of every MIA feature on the OOS control corpus.
Attributes:
| Name | Type | Description |
|---|---|---|
model |
str
|
The NVIDIA model ID this baseline was built for. |
n_valid |
int
|
Number of control rows where |
feature_means |
dict[str, float | None]
|
Per-feature mean across the valid rows. Keys are the five MIA feature
names. |
feature_stds |
dict[str, float | None]
|
Per-feature standard deviation across the valid rows, floored at
|
is_calibrated |
bool
|
|
min_valid |
int
|
The threshold used (default 50, per the Open Defaults in requirements.md). |
Source code in recall_guard/mia/control.py
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 | |
MCSCalibrator
dataclass
Per-model logistic-regression calibrator for p(memorized | features).
Attributes:
| Name | Type | Description |
|---|---|---|
model |
str
|
The NVIDIA model ID this calibrator was trained for. |
classifier |
LogisticRegression
|
The fitted |
feature_order |
list[str]
|
Canonical order used to flatten the standardised feature dict
into the classifier's input vector. Populated at train time and
consumed verbatim by :meth: |
holdout_auc |
float
|
ROC-AUC score of the trained classifier on the 25% held-out portion of the labelled IS/OOS corpus. Reported in the manifest and the per-model evaluation result (Req 5.2). |
is_weak |
bool
|
|
Source code in recall_guard/mia/mcs.py
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | |
predict_proba
predict_proba(features, baseline)
Return the calibrated probability of "memorized" for one record.
Standardises features against the model's baseline and
feeds the resulting vector to the trained classifier in
self.feature_order.
Returns:
| Type | Description |
|---|---|
float
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If any of the four core features standardises to |
Source code in recall_guard/mia/mcs.py
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | |
MiaFeatures
dataclass
Five MIA features for one (model, prompt, response) record.
Attributes:
| Name | Type | Description |
|---|---|---|
loss |
float
|
Mean negative logprob of the realised tokens (clipped at floor). Low loss means the model found the text easy to predict, which is what stored text looks like. |
min_k |
float
|
Mean of the bottom |
min_k_pp |
float
|
Mean of the bottom-K per-position z-scores (Min-K%++). Same idea as
|
zlib_ratio |
float
|
|
ref_delta |
float | None
|
|
Source code in recall_guard/mia/features.py
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 | |
assert_cutoff_safe
assert_cutoff_safe(eval_set, models, cutoffs)
Fail-fast guard: every shortlisted model has a cutoff <= eval cutoff.
Raises:
CutoffViolation: if any model is missing from cutoffs or, when
eval_set.cutoff_date is set, post-dates it.
Source code in recall_guard/core/loader.py
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | |
bootstrap_ci
bootstrap_ci(
samples,
statistic,
n_resamples=1000,
confidence=0.95,
seed=0,
)
Compute a percentile bootstrap CI for statistic over samples.
Args:
samples: Sequence of arbitrary objects (may be ints, floats, tuples,
dicts, dataclasses, etc.). Resampling uses index sampling, so the
element type does not need to be numpy-friendly.
statistic: Callable taking a resampled sequence and returning a float.
n_resamples: Number of bootstrap resamples (>=1). Default 1000 matches
the harness's Req 6.1 minimum.
confidence: Two-sided confidence level in (0, 1). Default 0.95.
seed: Seed for numpy.random.default_rng; same seed -> same output.
Returns:
Tuple (point, lo, hi):
- ``point = statistic(samples)`` (computed once on the original).
- ``lo``, ``hi`` are the lower / upper percentile bounds.
- Postcondition: ``lo <= point <= hi`` (clamped on tiny float drift).
Raises:
ValueError: if samples is empty, n_resamples < 1, or
confidence is outside (0, 1).
Source code in recall_guard/core/bootstrap.py
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 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | |
canonical_draw_hash
canonical_draw_hash(contents)
SHA-256 over the draw set's reply text, independent of arrival order.
Covers reply text only. Logprob structures are excluded because their key ordering comes from the provider's JSON and is not stable across servers or library versions, and timing and thread identity are excluded because they are not properties of the answer.
Sorting before hashing is what makes the digest a property of the draw set; the tie-break rules elsewhere in this module recover a deterministic order from content alone, so nothing depends on how the draws arrived.
Source code in recall_guard/core/ensemble.py
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 | |
compute_file_hash
compute_file_hash(path)
Return the sha256 hex digest of the bytes at path.
Reads in 8KB chunks via hashlib.sha256().update(chunk) so the function
can hash files larger than fit comfortably in memory. The chunked read is
semantically equivalent to hashlib.sha256(path.read_bytes()).hexdigest()
for any file size; the dedicated test exercises a >16KB payload to make
sure the chunk boundary does not corrupt the digest.
Source code in recall_guard/core/manifest.py
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | |
detect_multimodal
detect_multimodal(
values,
*,
grid,
mass_min=0.25,
trough_steps=3,
density_ratio=10.0,
min_draws=8,
min_cluster_density=1.5,
)
Detect two clusters separated by a sparse gap.
Returns None when the check could not run -- never a silent "found
nothing". There are three such cases, and a caller who needs to tell them
apart should read the lattice adherence alongside this:
- no lattice was declared (a continuous quantity has none);
- fewer than
min_drawsdraws, which cannot evidence two clusters; - the clusters are too thinly populated for a gap between them to mean anything (see below).
The rule is defined directly on the lattice rather than by a classical unimodality test. Those assume a continuous distribution, and on heavily tied lattice data they measure tie mass instead of modality -- badly enough that on the measured corpus the most sharply converged component scores as more multimodal than the genuinely split one.
The sparsity guard matters. trough_steps counts lattice steps, not
draws, so on a lattice much finer than the sampling, empty runs occur
everywhere by chance; the density test is vacuous there because an empty gap
has no peak to compare against. Measured, a unimodal normal at a 0.001
lattice flagged on every single subsample.
What separates the two regimes is not how wide the gap is but how dense the
clusters are: a real cluster stacks many draws onto few lattice positions,
while a spurious one is a scatter of singletons whose gaps are ordinary
spacing. So each side of the split must average at least
min_cluster_density draws per occupied position.
Every threshold is a parameter because all of them were tuned against a single measurement date. Detects separated clusters only: two overlapping modes with no gap between them are invisible to it, a known and accepted false-negative class.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mass_min
|
float
|
Minimum share of draws each cluster must hold. |
0.25
|
trough_steps
|
int
|
Minimum width of the gap, in lattice steps. |
3
|
density_ratio
|
float
|
How much denser the taller cluster peak must be than the busiest bin inside the gap. Note this binds only when the gap is non-empty; at realistic draw counts most detections win on a completely empty gap. |
10.0
|
min_draws
|
int
|
Below this many draws the check does not run. |
8
|
min_cluster_density
|
float
|
Minimum draws per occupied lattice position within each cluster. |
1.5
|
Source code in recall_guard/core/consensus.py
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 | |
estimate_cost
estimate_cost(
spec,
*,
max_retries,
has_reference,
seconds_per_request=None,
)
Worst-case request count and duration, without issuing any request.
The nominal draw count is the floor, not the worst case: each logical draw
can become max_retries + 1 requests, and a configured reference model
doubles the whole thing.
Source code in recall_guard/core/ensemble.py
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | |
generate_ensemble
generate_ensemble(
lm, prompt, spec, *, decide, components=None
)
Draw spec.draws replies to prompt and reduce them.
Raises:
| Type | Description |
|---|---|
ValueError
|
If a component holds separated clusters and the spec asks to raise. |
RuntimeError
|
If the request budget is exhausted, too few draws are usable, or transport failures exceed the configured share. Each of these is a refusal to report a confident answer computed from survivors. |
Source code in recall_guard/core/ensemble.py
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 | |
grid_adherence
grid_adherence(values, grid, *, tolerance=1e-09)
Fraction of values that actually lie on the declared lattice.
Reported so a caller who declares a lattice the data does not follow finds out, instead of silently receiving mis-snapped results. A continuous quantity scores near zero here at any lattice.
A non-finite draw counts as off-lattice rather than raising: this reports on data quality, so it has to survive the bad data it exists to describe.
Source code in recall_guard/core/consensus.py
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 | |
lag_dependence
lag_dependence(labels, groups)
How much more alike draws are within a collection group than across them.
Returns None when there are fewer than two groups, or too few pairs to
compare -- never a misleading zero.
The statistic is the probability that two draws from the same group carry the same label, minus the probability for two draws from different groups. Zero means the grouping carries no information, which is what independence looks like; positive means draws collected together agree more than draws collected apart.
This exists because the reported agreement interval assumes independent draws, and that is precisely the assumption a serving stack violates -- batching, cache reuse, and node affinity all couple requests issued together. Positive dependence makes every interval narrower than its label, in the one direction that matters. Measuring it does not correct the interval; it makes the assumption falsifiable instead of merely disclaimed.
Depends only on the stored labels and group tags, never on arrival order, so it replays identically from a persisted draw set.
Source code in recall_guard/core/consensus.py
548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 | |
load_cutoffs
load_cutoffs(path)
Parse the cutoffs YAML registry into {model_id: cutoff_date}.
Source code in recall_guard/core/loader.py
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | |
load_eval_set
load_eval_set(path)
Parse a JSONL eval file into an EvalSet.
The first line may optionally be a header object containing
{"_cutoff_date": "YYYY-MM-DD"}. All other lines must be row objects
matching the input contract (Req 2.1). Logs WARNING records for low-N
and class-imbalance conditions (Req 2.2, 2.3); never raises for those.
Returns the entire set as a single list with no train/dev split (Req 2.4).
Source code in recall_guard/core/loader.py
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | |
read_manifest
read_manifest(path)
Load manifest.json from path and reconstruct the dataclass.
Validates the top-level shape: the JSON object must have exactly the same
keys as Manifest's fields. Missing or extra keys raise ValueError
naming the offending key(s) so manifest drift is caught immediately rather
than silently dropped on round-trip.
Source code in recall_guard/core/manifest.py
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | |
reduce_draws
reduce_draws(
draws,
spec,
*,
decide,
components=None,
waves=None,
n_requested=None,
fail_counts=None,
sampled_at=None,
)
Reduce a draw set to one answer. Pure: no I/O, no randomness, no clock.
Separated from execution so a stored draw set can be replayed into a
bit-identical result without contacting a model, which is what makes an
ensemble auditable after the fact. No clock is read here: sampled_at
stays None unless the caller passes through what execution recorded.
Source code in recall_guard/core/ensemble.py
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 | |
robust_location
robust_location(values, *, mode='median', trim=0.25)
Reduce values to one location, independent of their order.
mode is one of "mean", "median", or "trimmed".
Note what this cannot do: on a component whose draws form two separated clusters there is no single location to estimate, and no symmetric trim fraction escapes the gap between them -- trimming converges toward the median, not toward a mode. Callers must run the multimodality check first and skip this entirely for a flagged component.
Source code in recall_guard/core/consensus.py
597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 | |
scale_floor
scale_floor(values, *, grid=None)
Robust scale estimate, floored at the lattice's resolution limit.
The floor exists because a concentrated lattice-valued component can drive the median absolute deviation to exactly zero, leaving the usual robust scale undefined. It is an identifiability floor, not an estimate of quantization noise: any true dispersion far below one lattice step produces observations on one or two lattice points and is indistinguishable from zero, so an estimate below that level carries no information.
Two caveats worth knowing before relying on it. It binds whenever
1.4826 * MAD < grid / sqrt(12) -- that is, for every MAD below
roughly 0.195 * grid, not only when the deviation is exactly zero. So on
a sharply concentrated component it can inflate a small but perfectly
well-defined estimate. And it only helps at all because the declared lattice
is coarser than the emitted one; declare the true lattice and the undefined
case returns.
Source code in recall_guard/core/consensus.py
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 | |
smallest_certifiable_n
smallest_certifiable_n(
target,
*,
confidence=0.95,
tail=Tail.TWO_SIDED,
limit=100000,
)
Fewest unanimous draws whose interval's lower bound reaches target.
Unanimity is the best case, so this is a hard floor: below it no observed agreement can certify the target, and a configuration requesting fewer draws can never succeed no matter what the model returns. Surfacing it at construction turns a silently-unreachable setting into an error.
Source code in recall_guard/core/consensus.py
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | |
smallest_detectable_split_n
smallest_detectable_split_n(
*,
cluster_positions,
min_cluster_draws=8,
min_cluster_density=1.5,
)
Fewest draws at which :func:detect_multimodal can flag a split.
The companion to :func:smallest_certifiable_n, and the reason both exist:
agreement precision and split detection need different sample sizes, and
sizing for the first silently under-sizes for the second. Agreement is where
the reported confidence lives; component splits are where a silently wrong
answer lives.
This is a necessary condition, not a sufficient one -- exactly as its companion is a floor under unanimity rather than a promise. Below the value returned here the density guard cannot be satisfied at all, so a split of that shape is undetectable no matter how clean the data. Above it, detection becomes possible; whether it fires still depends on sampling noise in the trough and in the cluster masses.
Measured on a corpus whose split is unambiguous at full size, detection still missed ~3.6% of 64-draw bootstrap resamples -- the closer analogue to a fresh ensemble, which draws independently rather than from a fixed pool. Reaching 99% detection on that corpus took ~128 draws.
The figure tracks parsed draws, not the configured count. The test
only ever sees replies that survived transport, parsing, and the caller's
projection. A prompt with a 5% failure rate configured at draws=64 is
really operating at n=61, where the same measurement gives 5.5% rather than
3.6% -- so size against n_parsed, not against draws.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cluster_positions
|
int
|
How many lattice positions the two clusters together occupy. Wider clusters need more draws to reach the same density. |
required |
Source code in recall_guard/core/consensus.py
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | |
snap_to_grid
snap_to_grid(value, grid)
Round value onto a lattice of step grid, half away from zero.
Deliberately does not divide by grid. That division is inexact in
binary in a value-dependent way -- 0.85 / 0.1 is exactly 8.5 while
0.95 / 0.1 is 9.499999999999998 -- so the tie direction ends up
depending on the value rather than on the rule, and the obvious spellings
disagree with one another. Working in integer lattice units avoids it.
Source code in recall_guard/core/consensus.py
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | |
wilson_interval
wilson_interval(
k,
n,
*,
confidence=0.95,
tail=Tail.TWO_SIDED,
continuity=False,
)
Score interval for a binomial proportion, or None when n is zero.
Inverting the score test rather than the Wald test keeps the interval inside
[0, 1] and, critically, non-degenerate at k == n. The Wald interval
collapses to zero width exactly there, which at high agreement is the
typical case -- it would report certainty from a couple of dozen draws.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
k
|
int
|
Successes observed. Must satisfy |
required |
n
|
int
|
Draws observed. |
required |
continuity
|
bool
|
Apply the Newcombe continuity correction, widening the interval. |
False
|
Source code in recall_guard/core/consensus.py
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | |
write_manifest
write_manifest(out_dir, manifest)
Serialise manifest to <out_dir>/manifest.json and return that path.
out_dir is created (with parents) if it does not yet exist; this lets
the runner pin the output directory at run start before any per-run
artifact has been produced. JSON is indented and key-sorted so the file is
diff-friendly across runs that differ only in metadata order.
Source code in recall_guard/core/manifest.py
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | |
build_baseline
build_baseline(
model_lm,
control_rows,
ref_lm,
min_valid=50,
max_workers=1,
)
Build a per-model control-corpus baseline.
For each row in control_rows:
- Call
model_lm.generate(row.prompt). OnTimeoutErrororRuntimeError(e.g., missing logprobs) the row is dropped and a WARNING is logged with the row index. - When
ref_lmis provided, also callref_lm.generate(row.prompt). A reference-side failure does not invalidate the row; it merely setsref_logprobs = Nonefor that row, so the four other features still contribute to the baseline. - Compute :class:
MiaFeaturesvia :func:compute_mia_features.
Per-feature mean and std are aggregated with numpy.mean and
numpy.std(ddof=0). Std is floored at _STD_FLOOR to avoid div-by-zero
in :func:standardise. When every valid row has ref_delta = None
(because ref_lm is None or every reference call failed), the
ref_delta mean and std are stored as None.
is_calibrated is set to n_valid >= min_valid.
Source code in recall_guard/mia/control.py
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | |
compute_mia_features
compute_mia_features(
response, logprobs, ref_logprobs, k=0.2
)
Compute the five MIA features for one record.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
response
|
str
|
The model's emitted text. Used only for the zlib-ratio denominator. |
required |
logprobs
|
list[TokenLogprob]
|
Per-token logprob entries from |
required |
ref_logprobs
|
list[TokenLogprob] | None
|
Per-token logprobs from a reference model on the same prompt; or
|
required |
k
|
float
|
Fraction of tokens used for the bottom-K slice in Min-K% and Min-K%++. Defaults to 0.2 (the paper's setting). |
0.2
|
Returns:
| Type | Description |
|---|---|
MiaFeatures
|
Frozen dataclass with all five features. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in recall_guard/mia/features.py
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | |
standardise
standardise(features, baseline)
Standardise eval-time MIA features against the model's control baseline.
For each of the four always-present features loss, min_k,
min_k_pp, zlib_ratio returns (value - mean) / max(std, _STD_FLOOR).
For ref_delta returns None whenever either the baseline or the
eval-time features have no reference value to standardise, i.e., the
field stays "off" rather than being silently coerced to 0.0.
Source code in recall_guard/mia/control.py
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | |
train_mcs
train_mcs(
model_lm,
is_memorized,
oos_control,
baseline,
ref_lm,
min_auc=0.6,
seed=0,
max_workers=1,
)
Train the MCS classifier for one model.
Drives the LM over both labelled corpora (in parallel when
max_workers > 1), fits a logistic regression on the standardised
features, and reports a held-out AUC. Raises ValueError if either
class ends up empty after per-row skips.
Source code in recall_guard/mia/mcs.py
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 | |