Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,29 @@ This example is self-contained and doesn't require external data. Use it to unde

---

### [target_coverage.ipynb](target_coverage.ipynb)
**Duration**: <1 min | **GPU**: Not required

**What it demonstrates**:
- Why a fixed `base_window` in `logarithmic_windows()` makes analysis coverage balloon across a
batch-size sweep at a fixed token budget, and how `target_coverage` holds it roughly constant
- How to reproduce the exact before/after coverage numbers from a real packed-corpus sweep
- How many training tokens a single measurement window actually costs, and how that scales
(or doesn't) with batch size under `target_coverage` on vs. off
- How `tokens_per_step`/`tokens_per_window` holds tokens-per-window *exactly* constant instead, by
deriving `base_window` straight from a token budget rather than via `max_steps`

**Key Insight**: `target_coverage` derives `base_window` from `max_steps` instead of fixing it, which
keeps both the realized coverage *and* the token cost of one measurement roughly constant across a
batch-size sweep -- trading window width for batch size as the budget shifts between them.
`tokens_per_step`/`tokens_per_window` goes further: it derives `base_window` directly from a token
budget, holding tokens-per-window constant exactly (up to step rounding) rather than roughly.

This example only imports `perspic.logger`, `pandas`, and `matplotlib` -- no model, training loop, or
GPU needed.

---

### [core/hutchinson_convergence.py](core/hutchinson_convergence.py)
**Duration**: ~2 min | **GPU**: Not required

Expand Down Expand Up @@ -155,6 +178,7 @@ examples/
│ ├── mlps.py
│ └── utils.py
├── batch_size_scaling_analysis.ipynb # Theory + implementation
├── target_coverage.ipynb # target_coverage vs. fixed base_window
├── cifar10.ipynb # Core workflow
├── logging_scheduler.ipynb # Efficient logging
├── cross10_cross_response.ipynb # Response analysis
Expand Down
368 changes: 368 additions & 0 deletions examples/target_coverage.ipynb

Large diffs are not rendered by default.

78 changes: 77 additions & 1 deletion perspic/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ def logarithmic_windows(
points_per_decade: int = 10,
base_window: int = 5,
adaptive_scale: float = 0.0,
target_coverage: Optional[float] = None,
tokens_per_step: Optional[float] = None,
tokens_per_window: Optional[float] = None,
) -> LogarithmicWindowSchedule:
"""Generate logarithmically spaced measurement windows.

Expand All @@ -69,13 +72,52 @@ def logarithmic_windows(
points_per_decade: Number of log points per 10x increase in steps.
Higher values give denser coverage. Default is 10.
base_window: Minimum window size (number of consecutive measurements
per log point). Default is 5.
per log point). Default is 5. Ignored if target_coverage or
tokens_per_window is set.
adaptive_scale: Controls how window size grows with step number.
- 0.0: Fixed window size (always base_window)
- 1.0: Window grows by ~1 step per decade
- 2.0: Window grows by ~2 steps per decade
The formula is: window_size = base_window + adaptive_scale * log10(step)
Default is 0.0 (fixed windows).
target_coverage: If set, overrides base_window with a value derived
from max_steps so that num_points * base_window / ( max_steps + 1 )
approximates this fraction (clamped to at least 1; the *actual*
coverage -- len(schedule.steps) / (max_steps + 1) -- will still differ
somewhat, since overlapping/truncated windows collapse below this
nominal estimate). Use this instead of a hand-picked base_window
when comparing schedules across runs whose max_steps varies for
reasons unrelated to how much you want measured -- e.g. a
batch-size sweep at a fixed token budget, where max_steps shrinks
linearly with batch size. With a fixed base_window, window
*count* (num_points above) only grows with log10(max_steps), so
realized coverage balloons as max_steps shrinks; deriving
base_window from target_coverage instead keeps it roughly
constant across such a sweep. None (default) preserves the
original fixed-base_window behavior exactly.
tokens_per_step: Tokens consumed by one training step (e.g.
batch_size * seq_len, computed by the caller -- this function
stays agnostic of both). Must be set together with
tokens_per_window (both or neither); mutually exclusive with
target_coverage.
tokens_per_window: If set (together with tokens_per_step), overrides
base_window with round(tokens_per_window / tokens_per_step)
(clamped to at least 1), making the token cost of one
measurement window equal to tokens_per_window exactly, up to
that rounding. Use this instead of target_coverage when you need
tokens-per-window held constant *exactly* rather than roughly
across a batch-size sweep at a fixed token budget: holding
tokens_per_window fixed while tokens_per_step grows with batch
size shrinks window width in steps by the same factor,
canceling the growth exactly (subject to rounding and the
1-step floor) -- unlike target_coverage, which derives
base_window via max_steps/num_points as a proxy and only
approximates this. None (default) leaves base_window and
target_coverage's behavior unaffected. Note: combining
tokens_per_window with adaptive_scale > 0 reintroduces drift,
since the adaptive growth added on top of base_window depends
on log10(center) rather than tokens_per_step -- the exact
guarantee applies to the base_window component only.

Returns:
LogarithmicWindowSchedule containing:
Expand All @@ -96,6 +138,22 @@ def logarithmic_windows(
adaptive_scale=2.0
)

# Batch-size sweep: hold ~11% coverage constant across runs whose
# max_steps varies with batch size, instead of a fixed base_window
# that would make coverage balloon as batch size grows and max_steps
# shrinks.
schedule = logarithmic_windows(max_steps=max_steps, target_coverage=0.11)

# Batch-size sweep: hold tokens-per-window exactly constant (up to
# rounding) as batch size varies, instead of target_coverage's
# roughly-constant behavior.
tokens_per_step = batch_size * seq_len # computed by the caller
schedule = logarithmic_windows(
max_steps=max_steps,
tokens_per_step=tokens_per_step,
tokens_per_window=2_000_000,
)

# Use with analyzer
model = analyzer(MyModule, analysis_schedule=schedule, ...)
"""
Expand All @@ -109,6 +167,24 @@ def logarithmic_windows(
# Generate log-spaced center points in the range [10^0, 10^log10(max_steps)]
# Step 0 will be added separately below
num_points = int(points_per_decade * math.log10(max_steps)) + 1
if (tokens_per_step is None) != (tokens_per_window is None):
raise ValueError(
"tokens_per_step and tokens_per_window must both be set, or both left as None"
)
if tokens_per_window is not None:
if target_coverage is not None:
raise ValueError(
"tokens_per_window is mutually exclusive with target_coverage; set only one"
)
if not math.isfinite(tokens_per_step) or tokens_per_step <= 0:
raise ValueError("tokens_per_step must be a finite positive float")
if not math.isfinite(tokens_per_window) or tokens_per_window <= 0:
raise ValueError("tokens_per_window must be a finite positive float")
base_window = max(1, round(tokens_per_window / tokens_per_step))
if target_coverage is not None:
if not math.isfinite(target_coverage) or target_coverage <= 0:
raise ValueError("target_coverage must be a finite positive float")
base_window = max(1, round(target_coverage * (max_steps + 1) / num_points))
# Generate logspace without numpy: 10^(start + i * step) for i in range(num_points)
log_start, log_end = 0, math.log10(max_steps)
log_step = (log_end - log_start) / (num_points - 1) if num_points > 1 else 0
Expand Down
30 changes: 29 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,35 @@
[build-system]
requires = ["setuptools>=80"]
build-backend = "setuptools.build_meta"

[tool.black]
line-length = 88
preview = true

[tool.isort]
profile = 'black'
multi_line_output = 3
multi_line_output = 3

[tool.uv.sources]
perspic = { workspace = true }

[project]
name = "perspic"
version = "0.0.1"
requires-python = ">=3.11"
dependencies = [
"ipykernel>=7.3.0",
"matplotlib>=3.11.1",
"nngeometry==0.3.2",
"opacus==1.5.4",
"pandas>=3.0.5",
"psutil==7.1.3",
"pytorch-lightning==2.5.5",
"setuptools==80.9.0",
"torch==2.8.0",
]

[dependency-groups]
dev = [
"perspic",
]
24 changes: 0 additions & 24 deletions setup.py

This file was deleted.

Loading
Loading