Skip to content

Commit 8248d4d

Browse files
author
Han Wang
committed
fix(dpmodel,cc): strictly positive phantom floor; device-edge flat aparam; effective-layer pair capability
Round-3 review findings (OutisLi + njzjz-bot re-review): 1. The signed phantom denominator could cross zero for real logits below phantom_logit (the smooth envelope maps raw < -attnw_shift to l < -shift at sw > 0 -- reachable with finite, valid parameters), producing a pole / the where-guard's invalid weights-sum>1 fallback. segment_softmax now applies an exponential-tail floor on the signed path: D~ = D + delta*exp(-D/delta), delta = sel*ph_term/40. It is C-infinity, monotone toward deeper deficits, bounded below by delta (weights <= 40*n_real/sel), and in the in-design regime (all logits >= phantom_logit, so D >= sel*ph_term = 40*delta) the correction is <= exp(-40)*delta -- it underflows to EXACT identity in float64, so the dense term-for-term parity remains bit-preserved (verified: the repformer parity suites still pass at 1e-12). Regressions: both reviewers' repros (sel=1 logits [-21,-21]; three entries at -100), a resolution-scaling smoothness sweep across the former zero crossing, and an in-design bit-parity pin vs the exact fixed-width softmax. 2. compute_edges_gpu_impl (Kokkos device-edge route) still padded aparam to rank-3 (1, nnode, daparam) and fed it to run_model_graph, which the flat-ABI artifact rejects; it now routes through extend_graph_aparam (flat (N, daparam), width-validated, ghost-only synthesis). The phantom edge-schema branch keeps its rank-3 layout (run_model_edges_with_comm is a different ABI). 3. uses_compact_edge_pairs() read the CONFIGURED repformer args, but the last layer is built with update_chnnl_2=False which forces its g2/h2 updates off -- an nlayers=1 model never builds compact pairs yet was rejected on torch < 2.6. The capability now reads the EFFECTIVE per-layer flags (the same gate call_graph uses); the guard regression uses nlayers=2 for the positive cases and pins that nlayers=1 is accepted on torch 2.5.1.
1 parent d33ad3b commit 8248d4d

5 files changed

Lines changed: 190 additions & 21 deletions

File tree

deepmd/dpmodel/descriptor/dpa2.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -757,13 +757,22 @@ def uses_compact_edge_pairs(self) -> bool:
757757
EquiVarApply). ``check_graph_trace_torch_version`` keys its
758758
torch >= 2.6 requirement on this capability.
759759
760+
Derived from the EFFECTIVE per-layer flags, not the configured
761+
arguments: the LAST repformer layer is built with
762+
``update_chnnl_2=False``, which forces its ``update_g2_has_attn``
763+
and ``update_h2`` off -- so e.g. ``nlayers=1`` never runs
764+
``center_edge_pairs`` even when the arguments enable it, and old
765+
torch stays usable. This mirrors the gate
766+
``DescrptBlockRepformers.call_graph`` itself uses to decide whether
767+
to build the pairs.
768+
760769
Returns
761770
-------
762771
bool
763772
Whether tracing :meth:`call_graph` runs ``center_edge_pairs``.
764773
"""
765-
return bool(
766-
self.repformer_args.update_g2_has_attn or self.repformer_args.update_h2
774+
return any(
775+
ll.update_g2_has_attn or ll.update_h2 for ll in self.repformers.layers
767776
)
768777

769778
def disable_graph_lower(self) -> None:

deepmd/dpmodel/utils/neighbor_graph/segment.py

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,17 @@ def segment_softmax(
110110
carry-all regime ``n_real > sel``, where a clamped (non-negative) count
111111
would drop the compensation and leave a finite ``exp(-attnw_shift)``
112112
denominator step at the crossing. For ``n_real <= sel`` the scheme is
113-
term-for-term equal to the dense softmax. The denominator is positive
114-
whenever every real logit is ``>= phantom_logit`` (guaranteed by the
115-
smooth-envelope logit construction for sane pre-shift logits); the
116-
existing non-positive-denominator guard below keeps the out-of-design
117-
corner defined.
113+
term-for-term equal to the dense softmax. Real logits are NOT bounded
114+
below by ``phantom_logit`` (the smooth envelope maps pre-shift logits
115+
``raw < -attnw_shift`` to ``l < phantom_logit`` at ``sw > 0``), so for
116+
negative counts the raw signed denominator can cross zero; a smooth
117+
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.
118121
"""
119122
xp = array_api_compat.array_namespace(data)
123+
dev = array_api_compat.device(data)
120124
if mask is not None:
121125
# broadcast mask (n,) over any trailing feature dims of data (n, *f)
122126
mask_b = xp.reshape(mask, mask.shape + (1,) * (data.ndim - 1))
@@ -154,9 +158,46 @@ def segment_softmax(
154158
ex = ex * xp.astype(mask_b, ex.dtype)
155159
denom = segment_sum(ex, segment_ids, num_segments)
156160
if ph_b is not None:
157-
denom = denom + xp.astype(ph_b, denom.dtype) * xp.exp(
158-
xp.full_like(seg_max, phantom_logit) - seg_max
159-
)
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+
# logits are not bounded below by ``phantom_logit`` (the smooth
165+
# envelope maps ``raw < -shift`` to ``l < -shift`` at ``sw > 0``),
166+
# so with a negative count the signed denominator D can cross zero
167+
# -- a pole in the attention weights reachable with finite, valid
168+
# model parameters. Exponential-tail floor:
169+
#
170+
# D~ = D + delta * exp(-D / delta), delta = sel*ph_term / 40
171+
#
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).
185+
if mask is not None:
186+
n_real_seg = segment_sum(
187+
xp.astype(mask, denom.dtype), segment_ids, num_segments
188+
)
189+
else:
190+
n_real_seg = segment_sum(
191+
xp.ones(data.shape[:1], dtype=denom.dtype, device=dev),
192+
segment_ids,
193+
num_segments,
194+
)
195+
sel_b = xp.reshape(
196+
n_real_seg, n_real_seg.shape + (1,) * (denom.ndim - 1)
197+
) + xp.astype(ph_b, denom.dtype)
198+
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)
160201
denom_e = xp.take(denom, segment_ids, axis=0)
161202
safe = xp.where(denom_e > 0, denom_e, xp.ones_like(denom_e))
162203
return ex / safe

source/api_cc/src/DeepPotPTExpt.cc

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2386,12 +2386,14 @@ void DeepPotPTExpt::compute_edges_gpu_impl(double* d_atom_energy,
23862386
torch::full({1}, static_cast<std::int64_t>(nnode), opt_i64);
23872387
const at::Tensor n_local =
23882388
torch::full({1}, static_cast<std::int64_t>(nloc), opt_i64);
2389-
at::Tensor graph_aparam = aparam_tensor;
2390-
if (daparam > 0 && nnode > nloc) {
2391-
graph_aparam = torch::cat(
2392-
{aparam_tensor, torch::zeros({1, nnode - nloc, daparam}, opt_f64)},
2393-
1);
2394-
}
2389+
// Flat (N, daparam) graph ABI: validate the width, zero-pad ghost
2390+
// rows, and synthesize a zero tensor on a ghost-only subdomain --
2391+
// the rank-3 (1, nnode, daparam) pad this branch used before is
2392+
// rejected by the artifact (GeneralFitting.call_graph requires
2393+
// rank-2 aparam), so device-edge graph inference with
2394+
// numb_aparam > 0 failed at the artifact boundary.
2395+
at::Tensor graph_aparam =
2396+
extend_graph_aparam(aparam_tensor, nnode, nloc, daparam);
23952397
GraphTensorPack graph_pack;
23962398
graph_pack.atype = atype_t.reshape({nnode});
23972399
graph_pack.n_node = n_node;

source/tests/common/dpmodel/test_segment_softmax.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,108 @@ def test_masked_entry_larger_than_unmasked_max_no_nan() -> None:
110110
ref = np.exp([1.0, 2.0]) / np.exp([1.0, 2.0]).sum()
111111
np.testing.assert_allclose(out[:2], ref, rtol=1e-12)
112112
assert out[2] == 0.0
113+
114+
115+
class TestSignedPhantomStrictPositivity:
116+
"""The SIGNED phantom denominator must stay strictly positive for
117+
arbitrary finite logits (OutisLi / njzjz-bot review).
118+
119+
Real logits are not bounded below by ``phantom_logit``: the smooth
120+
envelope maps pre-shift logits ``raw < -attnw_shift`` to
121+
``l < phantom_logit`` whenever ``sw > 0``. With a negative count the
122+
raw signed denominator ``sum exp(l) + (sel - n) exp(ph)`` then crosses
123+
zero (e.g. ``sel=1``, logits ``[-21, -21]``, ``ph=-20``), which used to
124+
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.
128+
"""
129+
130+
def test_reviewer_repro_finite_positive(self) -> None:
131+
# sel=1, two real entries below the phantom logit -> raw D < 0.
132+
data = np.array([-21.0, -21.0])
133+
seg = np.array([0, 0])
134+
w = segment_softmax(
135+
data,
136+
seg,
137+
1,
138+
phantom_count=np.array([-1.0]), # sel - n_real = 1 - 2
139+
phantom_logit=-20.0,
140+
)
141+
assert np.all(np.isfinite(w))
142+
assert np.all(w >= 0.0)
143+
# a positive normalization cannot return the where-guard's [1, 1]
144+
assert w.sum() < 2.0
145+
146+
def test_njzjz_repro_three_entries(self) -> None:
147+
data = np.array([-100.0, -100.0, -100.0])
148+
seg = np.array([0, 0, 0])
149+
w = segment_softmax(
150+
data,
151+
seg,
152+
1,
153+
phantom_count=np.array([-2.0]), # sel=1, n_real=3
154+
phantom_logit=-20.0,
155+
)
156+
assert np.all(np.isfinite(w))
157+
assert np.all(w >= 0.0)
158+
# deep-suppressed entries with a huge phantom deficit -> near-zero
159+
# weights, NOT the where-guard's [1, 1, 1].
160+
assert w.sum() < 1.0
161+
162+
def test_smooth_across_former_zero_crossing(self) -> None:
163+
"""Sweep a logit through the raw denominator's zero crossing: the
164+
weights must stay finite and BOUNDED (<= 40 * n_real / sel from the
165+
floor scale), and their increments must SCALE with the sweep
166+
resolution -- the smoothness signature a pole cannot fake (at a
167+
pole, refining the grid does not shrink the largest step).
168+
"""
169+
170+
def _sweep(lo: float, hi: float, num: int) -> np.ndarray:
171+
outs = []
172+
for l2 in np.linspace(lo, hi, num):
173+
w = segment_softmax(
174+
np.array([-21.0, float(l2)]),
175+
np.array([0, 0]),
176+
1,
177+
phantom_count=np.array([-1.0]),
178+
phantom_logit=-20.0,
179+
)
180+
assert np.all(np.isfinite(w))
181+
assert np.all(w >= 0.0)
182+
# floor-scale bound: 40 * n_real / sel = 80 here
183+
assert np.all(w <= 80.0)
184+
outs.append(w)
185+
return np.stack(outs)
186+
187+
# D_raw(l) = exp(-21) + exp(l) - exp(-20) crosses zero at l = ln(
188+
# exp(-20) - exp(-21)) ~= -20.4587 (the LocalAtten sw-sweep pole
189+
# from the review, expressed directly in logit space).
190+
crossing = np.log(np.exp(-20.0) - np.exp(-21.0))
191+
coarse = _sweep(crossing - 0.5, crossing + 0.5, 201)
192+
step_coarse = np.abs(np.diff(coarse, axis=0)).max()
193+
# refine 10x: a smooth function's largest step shrinks ~10x; a pole
194+
# (or the old where-guard plateau jump) would keep an O(1) step.
195+
fine = _sweep(crossing - 0.5, crossing + 0.5, 2001)
196+
step_fine = np.abs(np.diff(fine, axis=0)).max()
197+
assert step_fine < 0.2 * step_coarse, (
198+
f"steps do not shrink under refinement (coarse {step_coarse:.4f} "
199+
f"-> fine {step_fine:.4f}): discontinuity or pole at the crossing"
200+
)
201+
202+
def test_floor_does_not_disturb_dense_parity_regime(self) -> None:
203+
"""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.
206+
"""
207+
rng = np.random.default_rng(3)
208+
data = rng.normal(size=7) # normal-scale logits, shift 20 below
209+
seg = np.zeros(7, dtype=np.int64)
210+
w_floor = segment_softmax(
211+
data, seg, 1, phantom_count=np.array([3.0]), phantom_logit=-20.0
212+
)
213+
# reference: the exact fixed-width softmax with 3 phantom slots
214+
full = np.concatenate([data, np.full(3, -20.0)])
215+
ref = np.exp(full - full.max())
216+
ref = ref / ref.sum()
217+
np.testing.assert_allclose(w_floor, ref[:7], rtol=1e-12, atol=1e-15)

source/tests/pt_expt/utils/test_graph_pt2_metadata.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -275,11 +275,20 @@ class _NoDesc:
275275
@pytest.mark.parametrize(
276276
("repformer_overrides", "should_raise"),
277277
[
278-
({}, True), # default dpa2: update_g2_has_attn=True -> compact pairs
279-
({"update_g2_has_attn": False, "update_h2": True}, True), # h2 consumer
280-
({"update_g2_has_attn": False, "update_h2": False}, False), # no pairs
278+
# nlayers >= 2 so a non-last layer actually consumes compact pairs
279+
# (the LAST layer is built with update_chnnl_2=False, which forces
280+
# its g2/h2 updates off).
281+
({"nlayers": 2}, True), # default update_g2_has_attn=True
282+
({"nlayers": 2, "update_g2_has_attn": False, "update_h2": True}, True),
283+
(
284+
{"nlayers": 2, "update_g2_has_attn": False, "update_h2": False},
285+
False,
286+
), # no pair consumers on any layer
287+
# nlayers=1: the only layer is the last -> NO effective compact-pair
288+
# consumer even with the arguments enabled; torch 2.5 stays usable.
289+
({"nlayers": 1}, False),
281290
],
282-
ids=["default_g2_attn", "update_h2", "no_pair_consumers"],
291+
ids=["g2_attn_2layers", "update_h2_2layers", "no_pair_consumers", "single_layer"],
283292
)
284293
def test_graph_trace_version_guard_dpa2_compact_pairs(
285294
monkeypatch, repformer_overrides, should_raise
@@ -293,7 +302,10 @@ def test_graph_trace_version_guard_dpa2_compact_pairs(
293302
the descriptor capability ``uses_compact_edge_pairs()``: DPA2's
294303
``update_g2_has_attn`` (default True) and ``update_h2`` both run the
295304
compact ``center_edge_pairs`` realization; with both off the lower
296-
traces backed symbols only and old torch stays usable.
305+
traces backed symbols only and old torch stays usable. The capability
306+
reads the EFFECTIVE per-layer flags: the last layer's g2/h2 updates
307+
are structurally off (``update_chnnl_2=False``), so a single-layer
308+
repformer never builds compact pairs and must NOT be rejected.
297309
"""
298310
import torch
299311

0 commit comments

Comments
 (0)