Skip to content
Merged
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
75 changes: 75 additions & 0 deletions tests/unit/utilities/test_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,81 @@ def test_bootstrap_ci_matches_per_cell_loop_and_median():
bootstrap_ci(x, confidence=1.0)


@pytest.mark.parametrize("statistic", ["mean", "median"])
def test_bootstrap_ci_preserves_float64_values(statistic: str):
x = torch.full((2, 8), 1.00000001, dtype=torch.float64)
low, high = bootstrap_ci(x, statistic=statistic, n_resamples=32)
torch.testing.assert_close(low, x[:, 0], rtol=0, atol=0)
torch.testing.assert_close(high, x[:, 0], rtol=0, atol=0)


@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32, torch.float64])
def test_statistics_support_floating_dtypes(dtype: torch.dtype):
x = torch.tensor([1.0, 2.0], dtype=dtype)
low, high = bootstrap_ci(torch.ones(8, dtype=dtype), n_resamples=32)
assert low.dtype == high.dtype == dtype
assert low.item() == high.item() == 1.0
pvalue = sign_flip_permutation_pvalue(x)
assert pvalue.dtype == torch.promote_types(dtype, torch.float32)
assert pvalue.item() == 0.5


@pytest.mark.parametrize("exact_max_n", [0, 12])
def test_sign_flip_float64_is_invariant_to_positive_scale(exact_max_n: int):
effects = torch.tensor([1.0, 2.0], dtype=torch.float64)
expected = sign_flip_permutation_pvalue(effects, exact_max_n=exact_max_n)
actual = sign_flip_permutation_pvalue(effects * 1e-50, exact_max_n=exact_max_n)
assert expected.item() < 1.0
torch.testing.assert_close(actual, expected, rtol=0, atol=0)


def test_sign_flip_preserves_small_float64_effects():
effects = torch.tensor([1.0, 1e-8], dtype=torch.float64)
p = sign_flip_permutation_pvalue(effects)
assert p.dtype == torch.float64
assert p.item() == 0.5


def test_sign_flip_counts_float64_ties_after_rounding():
effects = torch.tensor([-1.0, 0.4, -0.5, 0.6], dtype=torch.float64)
p = sign_flip_permutation_pvalue(effects)
assert p.dtype == torch.float64
assert p.item() == 0.875

scaled_p = sign_flip_permutation_pvalue(effects * 1e-50)
assert scaled_p.dtype == torch.float64
assert scaled_p.item() == 0.875


@pytest.mark.parametrize("alternative, expected", [("greater", 11 / 16), ("less", 7 / 16)])
def test_sign_flip_counts_float64_ties_for_one_sided_alternatives(
alternative: str, expected: float
):
effects = torch.tensor([-1.0, 0.4, -0.5, 0.6], dtype=torch.float64)
p = sign_flip_permutation_pvalue(effects, alternative=alternative)
assert p.dtype == torch.float64
assert p.item() == expected


def test_sign_flip_does_not_count_distinct_means_as_ties_near_zero():
effects = torch.tensor([1.0, -1e-8], dtype=torch.float64)
for scale in (1.0, 1e-50):
scaled_effects = effects * scale
greater = sign_flip_permutation_pvalue(scaled_effects, alternative="greater")
less = sign_flip_permutation_pvalue(scaled_effects, alternative="less")
assert greater.dtype == less.dtype == torch.float64
assert greater.item() == 0.5
assert less.item() == 0.75


def test_sign_flip_monte_carlo_preserves_float64_pvalue_dtype():
effects = torch.ones(13, dtype=torch.float64)
p = sign_flip_permutation_pvalue(
effects, n_permutations=64, generator=derive_generator(23), exact_max_n=12
)
assert p.dtype == torch.float64


def test_sign_flip_exact_enumeration_matches_hand_count():
effects = torch.tensor([[1.0, 2.0, 3.0, -0.5]])
observed = effects.mean().abs()
Expand Down
12 changes: 7 additions & 5 deletions transformer_lens/utilities/statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def bootstrap_ci(
idx = torch.randint(0, n, (count, n), generator=gen).to(moved.device)
resampled = moved[..., idx] # [..., count, n]
stats.append(reduce(resampled, dim=-1))
draws = torch.cat(stats, dim=-1).float() # [..., n_resamples]
draws = torch.cat(stats, dim=-1).to(torch.promote_types(x.dtype, torch.float32))
alpha = (1.0 - confidence) / 2.0
low = torch.quantile(draws, alpha, dim=-1)
high = torch.quantile(draws, 1.0 - alpha, dim=-1)
Expand Down Expand Up @@ -118,7 +118,7 @@ def sign_flip_permutation_pvalue(
raise ValueError(f"alternative must be one of {_ALTERNATIVES}, got {alternative!r}")
if n_permutations < 1:
raise ValueError("n_permutations must be >= 1")
moved = _to_last(effects, dim).float()
moved = _to_last(effects, dim).to(torch.promote_types(effects.dtype, torch.float32))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Float64 narrows the rounding problem, but doesn't resolve it. _count_hits still compares with bare >=, and the observed and null means are summed in different orders. So sign patterns that tie exactly can still miss by an ulp and give an anti-conservative p-value, e.g. for effects rounded to one decimal. Could you make the comparison tolerant of floating-point ties, the way scipy's permutation_test does, and add a tie case to the tests?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, both findings are addressed in 6a83322.

I reproduced the tie undercount with float64 effects [-1.0, 0.4, -0.5, 0.6]: the previous comparator returned 0.75 instead of the exact 0.875. _count_hits now uses SciPy's directional relative threshold, 100 * eps * abs(observed), without an absolute tolerance floor. Regressions cover two-sided/one-sided ties, scaling by 1e-50, and genuinely distinct tiny means. P-value dtype assertions now cover all supported floating inputs and the float64 Monte Carlo path.

With normal repository pytest options and runtime instrumentation: 21 statistics tests, 106 sparse-probing tests, and the real patching statistics consumer passed. Independent verification passed 45 numerical cases; full mypy passed all 393 source files. Formatting and whitespace checks passed; the existing Makefile isort-path warning and separate exact changed-file checks are documented in the updated description. The repair merges cleanly with current dev in a local merge-tree check. Hosted checks for this new head remain separate from these local results; no GPU/full acceptance qualification is claimed.

AI-assisted follow-up. I have left the review threads open for your assessment.

n = moved.shape[-1]
if n < 1:
raise ValueError("cannot permute an empty axis")
Expand All @@ -144,10 +144,12 @@ def sign_flip_permutation_pvalue(


def _count_hits(null: torch.Tensor, observed: torch.Tensor, alternative: str) -> torch.Tensor:
# Relative to the observed statistic: tolerate reduction-order ties without an absolute floor.
tolerance = observed.abs() * (100 * torch.finfo(null.dtype).eps)
if alternative == "two-sided":
hit = null.abs() >= observed.abs()
hit = null.abs() >= observed.abs() - tolerance
elif alternative == "greater":
hit = null >= observed
hit = null >= observed - tolerance
else:
hit = null <= observed
hit = null <= observed + tolerance
return hit.sum(dim=-1).to(null.dtype)
Loading