Skip to content

Commit 5219119

Browse files
author
Han Wang
committed
fix(dpmodel): gate the phantom denominator floor to the negative-count regime
Two review findings on the exponential-tail floor (OutisLi): 1. The floor fired for phantom_count >= 0, where no pole exists (the denominator is a plain positive softmax sum), breaking the documented term-for-term dense parity by orders of magnitude when every logit sits below phantom_logit ([0.5, 0.5] became [0.0009, 0.0009]). 2. Float32 autodiff produced NaN gradients for ordinary finite logits: with a tiny delta the backward of -denom/delta forms denom/delta**2, which overflows before being multiplied by the underflowed-to-zero exponential (0 * inf -> NaN), in both torch and jax. Fix: apply the floor only where (a) the count is negative and (b) denom < 40*delta -- beyond that the correction is < 1e-19 relative, sub-ulp in float32 and float64, so the gate boundary is bit-invisible and every in-design segment (and ALL count >= 0 segments) is now BIT-exact with the floor-free formula. The inactive branch substitutes (0, 1) for (denom, delta) BEFORE the division (safe-where), so the pathological ratio is never constructed under autodiff. The exponent caps move to 80 (finite exp in float32 too), including a new cap on ph_term itself that kills a latent inf - inf NaN for logits > 80 below phantom_logit. Tests: phantom_count == 0 below-phantom exact-dense (bitwise vs the phantom-free primitive), positive-count below-phantom exact-dense, torch + jax float32 backward finite-and-correct in both floor regimes, and LocalAtten / Atten2Map below-phantom graph-vs-dense parity at the reviewer's construction.
1 parent 3006793 commit 5219119

3 files changed

Lines changed: 241 additions & 30 deletions

File tree

deepmd/dpmodel/utils/neighbor_graph/segment.py

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,14 @@ def segment_softmax(
113113
term-for-term equal to the dense softmax. Real logits are NOT bounded
114114
below by ``phantom_logit`` (the smooth envelope maps pre-shift logits
115115
``raw < -attnw_shift`` to ``l < phantom_logit`` at ``sw > 0``), so for
116-
negative counts the raw signed denominator can cross zero; a smooth
116+
NEGATIVE counts the raw signed denominator can cross zero; a smooth
117117
strictly-positive floor (see the inline comment at the denominator)
118-
keeps the normalization finite, positive and C-infinity for arbitrary
119-
finite logits while perturbing the in-design regime by only
120-
~``exp(-2 * attnw_shift)`` relative.
118+
keeps the normalization finite and positive there. The floor is gated
119+
to the negative-count, small-denominator regime, so for
120+
``phantom_count >= 0`` (a plain positive softmax sum -- no pole exists)
121+
and for every in-design negative-count segment the output is
122+
BIT-IDENTICAL to the floor-free formula: the dense term-for-term parity
123+
is exact, not merely approximate.
121124
"""
122125
xp = array_api_compat.array_namespace(data)
123126
dev = array_api_compat.device(data)
@@ -158,9 +161,18 @@ def segment_softmax(
158161
ex = ex * xp.astype(mask_b, ex.dtype)
159162
denom = segment_sum(ex, segment_ids, num_segments)
160163
if ph_b is not None:
161-
ph_term = xp.exp(xp.full_like(seg_max, phantom_logit) - seg_max)
162-
denom = denom + xp.astype(ph_b, denom.dtype) * ph_term
163-
# Smooth strictly-positive floor for the SIGNED compensation: real
164+
# exponent clamped at 80 (exp(80) ~ 5.5e34 is finite in BOTH float32
165+
# and float64): pl - seg_max > 80 means every real logit sits > 80
166+
# below the phantom logit; an unclamped exp would overflow (inf in
167+
# float32 already at ~88) and poison the signed sum with inf - inf.
168+
ph_arg = xp.minimum(
169+
xp.full_like(seg_max, phantom_logit) - seg_max,
170+
xp.full_like(seg_max, 80.0),
171+
)
172+
ph_term = xp.exp(ph_arg)
173+
ph_f = xp.astype(ph_b, denom.dtype)
174+
denom = denom + ph_f * ph_term
175+
# Smooth strictly-positive floor for NEGATIVE counts only: real
164176
# logits are not bounded below by ``phantom_logit`` (the smooth
165177
# envelope maps ``raw < -shift`` to ``l < -shift`` at ``sw > 0``),
166178
# so with a negative count the signed denominator D can cross zero
@@ -169,19 +181,25 @@ def segment_softmax(
169181
#
170182
# D~ = D + delta * exp(-D / delta), delta = sel*ph_term / 40
171183
#
172-
# Properties: (i) C-infinity in the logits and monotone-decreasing
173-
# towards larger deficits; (ii) D~ >= delta > 0 for ANY finite
174-
# logits (minimum at D == 0), so the weights stay finite and are
175-
# bounded by ``40 * n_real / sel``; (iii) in the in-design regime
176-
# every real logit is >= phantom_logit, hence D >= sel * ph_term
177-
# == 40 * delta and the correction is <= exp(-40) * delta -- more
178-
# than 1e-17 RELATIVE below D, i.e. it underflows to EXACT identity
179-
# in float64 and the dense term-for-term parity is bit-preserved;
180-
# (iv) the boundary-edge cancellation survives (a smooth function
181-
# of the already-continuous D). The exponent is clamped at 700 to
182-
# avoid inf*0 pathologies for astronomically negative D (there the
183-
# floor is astronomically large and the weights are ~0, as a
184-
# maximal phantom deficit should give).
184+
# applied ONLY where (a) the count is negative (for count >= 0 the
185+
# denominator is a plain positive softmax sum -- no pole -- and the
186+
# dense term-for-term parity must stay BIT-exact) and (b)
187+
# D < 40 * delta (beyond that the correction is <= exp(-40) * delta
188+
# < 1e-19 RELATIVE to D -- sub-ulp in float32 AND float64, so the
189+
# gate boundary is bit-invisible; in-design every real logit is
190+
# >= phantom_logit, hence D >= sel * ph_term == 40 * delta and the
191+
# floor never fires). Properties inside the active region: C-inf
192+
# in the logits, D~ >= delta > 0 for any finite logits, weights
193+
# bounded by ~40 * n_real / sel, and the boundary-edge cancellation
194+
# survives (a smooth function of the already-continuous D).
195+
#
196+
# The inactive branch substitutes (0, 1) for (D, delta) BEFORE the
197+
# division: even a zero cotangent cannot rescue exp(-D/delta) under
198+
# autodiff when delta underflows (backward forms D/delta**2 -> inf,
199+
# and 0 * inf = NaN in torch/jax float32), so the pathological
200+
# division must never be constructed. The exponent cap 80 bounds
201+
# the active branch's exp for float32 while keeping D~ positive for
202+
# any physical edge count (needs n_real/sel < exp(80)/40 ~ 1e33).
185203
if mask is not None:
186204
n_real_seg = segment_sum(
187205
xp.astype(mask, denom.dtype), segment_ids, num_segments
@@ -192,12 +210,15 @@ def segment_softmax(
192210
segment_ids,
193211
num_segments,
194212
)
195-
sel_b = xp.reshape(
196-
n_real_seg, n_real_seg.shape + (1,) * (denom.ndim - 1)
197-
) + xp.astype(ph_b, denom.dtype)
213+
sel_b = (
214+
xp.reshape(n_real_seg, n_real_seg.shape + (1,) * (denom.ndim - 1)) + ph_f
215+
)
198216
delta = xp.maximum(sel_b, xp.ones_like(sel_b)) * ph_term / 40.0
199-
e_arg = xp.minimum(-denom / delta, xp.full_like(denom, 700.0))
200-
denom = denom + delta * xp.exp(e_arg)
217+
active = xp.logical_and(ph_f < 0.0, denom < 40.0 * delta)
218+
denom_a = xp.where(active, denom, xp.zeros_like(denom))
219+
delta_a = xp.where(active, delta, xp.ones_like(delta))
220+
e_arg = xp.minimum(-denom_a / delta_a, xp.full_like(denom, 80.0))
221+
denom = xp.where(active, denom + delta_a * xp.exp(e_arg), denom)
201222
denom_e = xp.take(denom, segment_ids, axis=0)
202223
safe = xp.where(denom_e > 0, denom_e, xp.ones_like(denom_e))
203224
return ex / safe

source/tests/common/dpmodel/test_repformer_graph_ops.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,79 @@ def test_local_atten_parity(smooth):
328328
)
329329

330330

331+
def test_local_atten_below_phantom_dense_parity():
332+
"""OutisLi review: finite valid projection weights can push every smooth
333+
logit below ``-attnw_shift``. With ``n_real == sel`` the signed phantom
334+
count is 0 -- the denominator is a plain positive softmax sum -- and the
335+
graph route must match dense EXACTLY (the always-on floor used to return
336+
~0.0018 where dense gives 1.0)."""
337+
la = LocalAtten(1, 1, 1, smooth=True, precision="float64", seed=1)
338+
la.mapq.w = np.array([[1.0]])
339+
la.mapkv.w = np.array([[-30.0, 1.0]]) # key -30, value 1
340+
la.head_map.w = np.array([[1.0]]) # identity head
341+
la.head_map.b = np.array([0.0])
342+
nf, nloc, nnei = 1, 1, 2
343+
g1 = np.ones((nf * nloc, 1))
344+
gg1 = np.ones((nf, nloc, nnei, 1))
345+
mask = np.ones((nf, nloc, nnei), dtype=bool)
346+
sw = np.ones((nf, nloc, nnei))
347+
ref = la.call(g1.reshape(nf, nloc, 1), gg1, mask, sw) # == [[[1.0]]]
348+
dst = np.zeros(nnei, dtype=np.int64)
349+
got = la.call_graph(
350+
g1,
351+
gg1.reshape(-1, 1),
352+
mask.reshape(-1),
353+
sw.reshape(-1),
354+
dst,
355+
nf * nloc,
356+
nnei, # sel == n_real: phantom count 0
357+
)
358+
np.testing.assert_allclose(
359+
np.asarray(got), ref.reshape(1, 1), rtol=1e-12, atol=0.0
360+
)
361+
# anti-vacuity: the logits really are below -attnw_shift and the dense
362+
# result is the nontrivial value from the review
363+
np.testing.assert_allclose(np.asarray(got), [[1.0]], rtol=1e-12)
364+
365+
366+
def test_atten2map_below_phantom_dense_parity():
367+
"""Same below-``-attnw_shift`` regime for the pair attention map: with
368+
all key slots real (phantom count 0) graph must equal dense exactly."""
369+
a2m = Atten2Map(1, 1, 1, has_gate=False, smooth=True, precision="float64", seed=2)
370+
a2m.mapqk.w = np.array([[1.0, -30.0]]) # query 1, key -30 -> logits -30
371+
nf, nloc, nnei = 1, 1, 2
372+
g2 = np.ones((nf, nloc, nnei, 1))
373+
h2 = np.zeros((nf, nloc, nnei, 3))
374+
h2[..., 0] = 1.0
375+
mask = np.ones((nf, nloc, nnei), dtype=bool)
376+
sw = np.ones((nf, nloc, nnei))
377+
ref = a2m.call(g2, h2, mask, sw) # (nf, nloc, nnei, nnei, nh)
378+
n_total = nf * nloc
379+
dst = np.repeat(np.arange(n_total, dtype=np.int64), nnei)
380+
q_e, k_e, pm = center_edge_pairs(
381+
dst,
382+
mask.reshape(-1),
383+
n_total,
384+
include_self=True,
385+
ordered=True,
386+
static_nnei=nnei,
387+
)
388+
got = a2m.call_graph(
389+
g2.reshape(-1, 1),
390+
h2.reshape(-1, 3),
391+
sw.reshape(-1),
392+
q_e,
393+
k_e,
394+
pm,
395+
nf * nloc * nnei,
396+
nnei,
397+
)
398+
ref_pairs = ref[0, 0, np.asarray(q_e) % nnei, np.asarray(k_e) % nnei, :]
399+
np.testing.assert_allclose(np.asarray(got), ref_pairs, rtol=1e-12, atol=0.0)
400+
# anti-vacuity: weight 0.5 per key slot times h2h2t = 1/sqrt(3)
401+
np.testing.assert_allclose(np.asarray(got), 0.5 / np.sqrt(3.0), rtol=1e-12)
402+
403+
331404
def test_atten2map_graph_torch():
332405
import torch
333406

source/tests/common/dpmodel/test_segment_softmax.py

Lines changed: 122 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""segment_max / segment_softmax (NeighborGraph PR-D segment toolkit)."""
33

44
import numpy as np
5+
import pytest
56

67
from deepmd.dpmodel.utils.neighbor_graph import (
78
segment_max,
@@ -122,9 +123,14 @@ class TestSignedPhantomStrictPositivity:
122123
raw signed denominator ``sum exp(l) + (sel - n) exp(ph)`` then crosses
123124
zero (e.g. ``sel=1``, logits ``[-21, -21]``, ``ph=-20``), which used to
124125
hit the ``denom > 0`` where-guard and return weights ``[1, 1]`` (sum 2)
125-
on one side and a pole on the other. The smooth floor
126-
``(D + sqrt(D^2 + delta^2)) / 2`` keeps the normalization finite,
127-
positive and smooth for any finite logits.
126+
on one side and a pole on the other. The exponential-tail floor
127+
``D + delta * exp(-D / delta)`` keeps the normalization finite,
128+
positive and smooth there; it is GATED to the negative-count,
129+
small-denominator regime so that ``phantom_count >= 0`` (no pole
130+
possible) and every in-design negative-count segment stay BIT-exact
131+
with the floor-free dense formula, and its inactive branch is
132+
constructed without the ``-D / delta`` division so float32 autodiff
133+
stays NaN-free.
128134
"""
129135

130136
def test_reviewer_repro_finite_positive(self) -> None:
@@ -199,10 +205,121 @@ def _sweep(lo: float, hi: float, num: int) -> np.ndarray:
199205
f"-> fine {step_fine:.4f}): discontinuity or pole at the crossing"
200206
)
201207

208+
def test_zero_phantom_count_below_phantom_exact_dense(self) -> None:
209+
"""OutisLi review: ``phantom_count == 0`` means ``n_real == sel`` --
210+
no phantom term exists and the denominator is a plain positive
211+
softmax sum, so the output must be the EXACT dense softmax even
212+
when every logit sits far below ``phantom_logit`` (the always-on
213+
floor used to return ~0.0009 instead of 0.5 here).
214+
"""
215+
data = np.array([-30.0, -30.0])
216+
seg = np.array([0, 0], dtype=np.int64)
217+
w = segment_softmax(
218+
data, seg, 1, phantom_count=np.array([0.0]), phantom_logit=-20.0
219+
)
220+
np.testing.assert_array_equal(w, [0.5, 0.5])
221+
# and BIT-equal to the phantom-free primitive on generic data
222+
rng = np.random.default_rng(11)
223+
data = rng.normal(size=9) * 30.0 # spans far below AND above -20
224+
seg = np.array([0, 0, 0, 1, 1, 1, 1, 2, 2], dtype=np.int64)
225+
w_zero = segment_softmax(
226+
data, seg, 3, phantom_count=np.zeros(3), phantom_logit=-20.0
227+
)
228+
w_none = segment_softmax(data, seg, 3)
229+
np.testing.assert_array_equal(w_zero, w_none)
230+
231+
def test_positive_count_below_phantom_exact_dense(self) -> None:
232+
"""count > 0 with all real logits below ``phantom_logit``: the
233+
denominator is positive (phantom terms only ADD), so the fixed-width
234+
dense softmax must be reproduced exactly -- the floor must not fire.
235+
"""
236+
data = np.array([-30.0, -30.0])
237+
seg = np.array([0, 0], dtype=np.int64)
238+
w = segment_softmax(
239+
data, seg, 1, phantom_count=np.array([1.0]), phantom_logit=-20.0
240+
)
241+
full = np.array([-30.0, -30.0, -20.0])
242+
ref = np.exp(full - full.max())
243+
ref = ref / ref.sum()
244+
np.testing.assert_allclose(w, ref[:2], rtol=1e-15, atol=0.0)
245+
246+
def test_float32_torch_backward_finite(self) -> None:
247+
"""OutisLi review: with a tiny ``delta`` the floor's backward forms
248+
``D / delta**2`` -> inf, and ``0 * inf = NaN`` poisons the gradients
249+
in float32 even though the forward is exact. The gated safe-where
250+
construction must keep float32 autodiff finite and correct, in both
251+
the inactive (reviewer repro) and active (deep-deficit) regimes.
252+
"""
253+
import torch
254+
255+
# inactive-floor regime: logits far above phantom_logit
256+
data = torch.tensor([20.0, 21.0], dtype=torch.float32, requires_grad=True)
257+
ids = torch.tensor([0, 0], dtype=torch.int64)
258+
w = segment_softmax(
259+
data,
260+
ids,
261+
1,
262+
phantom_count=torch.tensor([-1.0], dtype=torch.float32),
263+
phantom_logit=-20.0,
264+
)
265+
v = torch.tensor([1.0, 2.0])
266+
(w * v).sum().backward()
267+
assert torch.all(torch.isfinite(data.grad))
268+
# analytic softmax-jacobian reference: g_i = w_i * (v_i - sum_j w_j v_j)
269+
# (the phantom term exp(-41) is negligible at float32 resolution)
270+
w64 = np.exp([20.0, 21.0] - np.float64(21.0))
271+
w64 = w64 / w64.sum()
272+
ref = w64 * (np.array([1.0, 2.0]) - (w64 * [1.0, 2.0]).sum())
273+
np.testing.assert_allclose(data.grad.numpy(), ref, rtol=1e-4)
274+
assert np.abs(ref).max() > 0.1 # nontrivial gradient, not all-zero
275+
276+
# active-floor regime: signed denominator below the floor threshold
277+
data = torch.tensor([-21.0, -21.0], dtype=torch.float32, requires_grad=True)
278+
w = segment_softmax(
279+
data,
280+
ids,
281+
1,
282+
phantom_count=torch.tensor([-1.0], dtype=torch.float32),
283+
phantom_logit=-20.0,
284+
)
285+
(w * v).sum().backward()
286+
assert torch.all(torch.isfinite(data.grad))
287+
288+
def test_float32_jax_backward_finite(self) -> None:
289+
"""Same float32 autodiff guarantee through JAX (the reviewer
290+
reproduced the NaN gradient there as well).
291+
"""
292+
jax = pytest.importorskip("jax")
293+
jnp = jax.numpy
294+
295+
ids = np.array([0, 0], dtype=np.int64)
296+
v = np.array([1.0, 2.0], dtype=np.float32)
297+
298+
def loss(x): # noqa: ANN001, ANN202
299+
w = segment_softmax(
300+
x,
301+
jnp.asarray(ids),
302+
1,
303+
phantom_count=jnp.asarray([-1.0], dtype=jnp.float32),
304+
phantom_logit=-20.0,
305+
)
306+
return (w * jnp.asarray(v)).sum()
307+
308+
# inactive-floor regime (reviewer repro)
309+
g = jax.grad(loss)(jnp.asarray([20.0, 21.0], dtype=jnp.float32))
310+
assert np.all(np.isfinite(np.asarray(g)))
311+
w64 = np.exp([20.0, 21.0] - np.float64(21.0))
312+
w64 = w64 / w64.sum()
313+
ref = w64 * (np.array([1.0, 2.0]) - (w64 * [1.0, 2.0]).sum())
314+
np.testing.assert_allclose(np.asarray(g), ref, rtol=1e-4)
315+
# active-floor regime
316+
g = jax.grad(loss)(jnp.asarray([-21.0, -21.0], dtype=jnp.float32))
317+
assert np.all(np.isfinite(np.asarray(g)))
318+
202319
def test_floor_does_not_disturb_dense_parity_regime(self) -> None:
203320
"""In the in-design regime (all logits >= phantom_logit, count >= 0)
204-
the floor's correction is ~exp(-2*shift) relative -- invisible at
205-
the 1e-12 level used by the dense-parity suites.
321+
the floor never fires (its gate excludes non-negative counts), so
322+
the fixed-width dense softmax is reproduced exactly.
206323
"""
207324
rng = np.random.default_rng(3)
208325
data = rng.normal(size=7) # normal-scale logits, shift 20 below

0 commit comments

Comments
 (0)