Skip to content

Commit 6acb75e

Browse files
authored
fix: green-light CI — Dense fit-flag tracer, buffer-donation test pollution, L1 loss contract; Codecov token (#855)
fix: make full test suite (CI) green — Dense fit-flag tracer, buffer-donation pollution, L1 loss; Codecov token Source fix ---------- brainpy/dnn/linear.py: ``Dense.update`` did ``if share.load('fit', False) and self.online_fit_by is not None:``. Inside a grad-/jit-traced fit step (BPFF/BPTT) the ``fit`` share value is a JAX tracer, so converting it to a Python bool raised ``TracerBoolConversionError`` and broke the canonical Dense/RNNCell back-prop training example. Reordered to consult the static ``*_fit_by`` configuration first; the ``and`` then short-circuits before the tracer is forced when online/offline fitting is not configured. This also removes a cross-test pollution: when the fit raised mid-trace it left a stale traced ``fit`` in the global ``share`` store, so the next test running ``Dense.update`` (e.g. ``LoopOverTime`` over a ``Dense``) also raised. Test isolation -------------- brainpy/running/jax_multiprocessing_test.py: ``test_vectorize_map_partial_chunk_clear_buffer`` ran ``jax_vectorize_map(..., clear_buffer=True)``, which invokes the process-global ``bm.clear_buffer_memory()`` and deletes EVERY live device buffer, poisoning later test modules ("deleted/donated buffer" errors). Patched the wipe to a no-op for the duration of the call (same guard already used in boost_misc_test and train_analysis_glue_fixes_test) so the code path stays covered without nuking the shared session. Test contract updates --------------------- brainpy/train/back_propagation_test.py: rewrote the pinned-defect test into a regression test that asserts ``Dense`` trains under BPFF (finite losses, weight moves) and that a subsequent plain forward pass does not raise (pollution guard). brainpy/losses/comparison_coverage_test.py: ``l1_loss`` delegates to ``braintools.metric.l1_loss`` (>=0.3.0, the required/CI dependency), which reduces each sample to its mean absolute error then applies the batch reduction. Updated the L1 expectations (none -> [1.5, 3.5], sum -> 5.0, mean -> 2.5) and comments to the 0.3.0 contract; the previous numbers pinned stale braintools 0.1.10 behaviour. CI -- .github/workflows/CI.yml: Codecov upload now passes ``token: ${{ secrets.CODECOV_TOKEN }}`` and ``slug: brainpy/BrainPy``.
1 parent 0d29d16 commit 6acb75e

5 files changed

Lines changed: 71 additions & 34 deletions

File tree

.github/workflows/CI.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,11 @@ jobs:
5454
MPLBACKEND: Agg # Use non-interactive backend for matplotlib
5555
run: |
5656
pytest --cov=brainpy --cov-report=xml brainpy/
57-
- name: Upload coverage to Codecov
57+
- name: Upload coverage reports to Codecov
5858
uses: codecov/codecov-action@v5
5959
with:
60+
token: ${{ secrets.CODECOV_TOKEN }}
61+
slug: brainpy/BrainPy
6062
files: ./coverage.xml
6163
fail_ci_if_error: false
6264

brainpy/dnn/linear.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,21 @@ def update(self, x):
125125
if self.b is not None:
126126
res += self.b
127127

128-
# online fitting data
129-
if share.load('fit', False) and self.online_fit_by is not None:
128+
# Online/offline fitting data recording.
129+
#
130+
# The (static, Python-level) ``*_fit_by`` configuration is checked *first*
131+
# so that the ``fit`` share value is only consulted when online/offline
132+
# fitting is actually enabled. Inside a grad-/jit-traced fit step (e.g.
133+
# ``BPFF.fit`` / ``BPTT.fit``) the ``fit`` flag is a JAX tracer; converting
134+
# it to a Python bool would raise ``TracerBoolConversionError``. Because a
135+
# plain ``Dense`` used for back-prop training leaves both ``*_fit_by`` as
136+
# ``None``, the ``and`` short-circuits on the static check and never forces
137+
# the tracer, letting the canonical RNNCell/Dense BPTT example train.
138+
if self.online_fit_by is not None and share.load('fit', False):
130139
self.fit_record['input'] = x
131140
self.fit_record['output'] = res
132141

133-
# offline fitting data
134-
if share.load('fit', False) and self.offline_fit_by is not None:
142+
if self.offline_fit_by is not None and share.load('fit', False):
135143
self.fit_record['input'] = x
136144
self.fit_record['output'] = res
137145
return res

brainpy/losses/comparison_coverage_test.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -221,26 +221,24 @@ def test_class_wrapper(self):
221221
# ---------------------------------------------------------------------------
222222
class TestRegressionLosses:
223223
def test_l1_loss_reductions(self):
224-
# P1-L1: l1_loss delegates to braintools.metric.l1_loss, which for
225-
# reduction='none' returns the per-row L1 *norm* (sum of abs over the
226-
# trailing axes, reshaped to (N, -1)), NOT the per-row mean. So for the
227-
# (2, 2) input below the 'none' output is the per-row sums [3, 7]; 'sum'
228-
# then totals them (10) and 'mean' averages them (5). (The previous
229-
# expectations of [1.5, 3.5]/5/2.5 encoded an incorrect per-row-mean
230-
# assumption about braintools and were pre-existing baseline failures.)
224+
# ``l1_loss`` delegates to ``braintools.metric.l1_loss`` (>=0.3.0), which
225+
# reduces each sample to its *mean* absolute error over the trailing axes
226+
# (shape (N,)) and then applies the batch reduction. For the (2, 2) input
227+
# below the per-sample means are [mean(1,2), mean(3,4)] = [1.5, 3.5]; so
228+
# 'none' -> [1.5, 3.5], 'sum' -> 5.0, 'mean' -> 2.5.
231229
x = jnp.array([[1., 2.], [3., 4.]])
232230
y = jnp.zeros((2, 2))
233231
none = np.asarray(C.l1_loss(x, y, reduction='none'))
234-
assert np.allclose(none, [3.0, 7.0]) # per-row L1 norm (sum of abs)
235-
assert float(C.l1_loss(x, y, reduction='sum')) == pytest.approx(10.0)
236-
assert float(C.l1_loss(x, y, reduction='mean')) == pytest.approx(5.0)
232+
assert np.allclose(none, [1.5, 3.5]) # per-sample mean abs error
233+
assert float(C.l1_loss(x, y, reduction='sum')) == pytest.approx(5.0)
234+
assert float(C.l1_loss(x, y, reduction='mean')) == pytest.approx(2.5)
237235

238236
def test_l1_class(self):
239237
x = jnp.array([[1., 2.], [3., 4.]])
240238
y = jnp.zeros((2, 2))
241239
layer = C.L1Loss(reduction='sum')
242-
# sum over per-row L1 norms [3, 7] = 10.0
243-
assert float(layer.update(x, y)) == pytest.approx(10.0)
240+
# sum over per-sample mean abs errors [1.5, 3.5] = 5.0
241+
assert float(layer.update(x, y)) == pytest.approx(5.0)
244242

245243
def test_l2_loss_elementwise(self):
246244
out = np.asarray(C.l2_loss(jnp.array([2.0, 0.0]), jnp.array([0.0, 0.0])))

brainpy/running/jax_multiprocessing_test.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,18 @@ def test_vectorize_map_partial_chunk():
5656

5757
def test_vectorize_map_partial_chunk_clear_buffer():
5858
args = [np.arange(5.0)]
59-
r = np.asarray(jax_vectorize_map(_double, args, num_parallel=2, clear_buffer=True))
59+
# NOTE: clear_buffer=True calls the process-global ``bm.clear_buffer_memory()``,
60+
# which deletes EVERY live device array -- including module-level constants and
61+
# persistent Variables in *other* test modules -- poisoning the rest of the
62+
# shared pytest session (later tests then hit "deleted/donated buffer" errors).
63+
# Patch it to a no-op so the clear_buffer code path is still exercised for
64+
# coverage without nuking the session.
65+
_orig_clear = bm.clear_buffer_memory
66+
bm.clear_buffer_memory = lambda *a, **k: None
67+
try:
68+
r = np.asarray(jax_vectorize_map(_double, args, num_parallel=2, clear_buffer=True))
69+
finally:
70+
bm.clear_buffer_memory = _orig_clear
6071
np.testing.assert_allclose(r, np.arange(5.0) * 2.0)
6172

6273

brainpy/train/back_propagation_test.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -487,22 +487,28 @@ def test_bptrainer_abstract_step_funcs_raise():
487487

488488

489489
# ---------------------------------------------------------------------------
490-
# Pinned defect (NOT in fix scope -- documents current behavior)
490+
# Regression: ``Dense`` trains cleanly under a BPFF/BPTT fit loop
491491
# ---------------------------------------------------------------------------
492492

493-
def test_dense_layer_fit_flag_is_traced_defect():
494-
"""PIN: ``bp.dnn.Dense`` under a BPFF/BPTT fit loop raises on the ``fit`` flag.
495-
496-
``brainpy/dnn/linear.py:129`` does
497-
``if share.load('fit', False) and self.online_fit_by is not None:``.
498-
Under the installed ``brainstate`` (0.5.x), inside the jitted / grad-traced
499-
fit step the ``fit`` flag is a JAX *tracer*, so the boolean ``and`` raises
500-
``jax.errors.TracerBoolConversionError``. This blocks the canonical
501-
``RNNCell``/``Dense`` BPTT example. It is an API-drift defect in the layer,
502-
not in ``back_propagation.py``; pinned here so the regression is visible.
493+
def test_dense_layer_fit_flag_under_grad_trace():
494+
"""``bp.dnn.Dense`` must train under a BPFF fit loop without a tracer error.
495+
496+
Inside the grad-/jit-traced fit step the ``fit`` flag is a JAX *tracer*.
497+
``Dense.update`` previously did
498+
``if share.load('fit', False) and self.online_fit_by is not None:`` which
499+
converted that tracer to a Python bool and raised
500+
``jax.errors.TracerBoolConversionError``, blocking the canonical
501+
``RNNCell``/``Dense`` BPTT example. ``Dense.update`` now checks the static
502+
``*_fit_by`` configuration first so the tracer is only consulted when online
503+
/offline fitting is enabled. This regression test trains a plain ``Dense``
504+
for a couple of epochs and asserts the fit completes with finite losses and
505+
an updated weight.
506+
507+
A *pollution guard* is included: a stale traced ``fit`` left in the global
508+
``share`` store by a previous (broken) fit used to make the next forward
509+
pass through a ``Dense`` raise. Running a plain forward pass after the fit
510+
confirms no traced state leaked.
503511
"""
504-
import jax
505-
506512
class DenseFF(bp.DynamicalSystem):
507513
def __init__(self):
508514
super().__init__()
@@ -516,11 +522,23 @@ def reset_state(self, batch_size=1, **kwargs):
516522

517523
with bm.training_environment():
518524
model = DenseFF()
525+
w_before = np.asarray(model.lin.W).copy()
519526
trainer = bp.BPFF(model, loss_fun=_mse, optimizer=bp.optim.Adam(lr=0.01),
520527
progress_bar=False)
521-
with pytest.raises(jax.errors.TracerBoolConversionError):
522-
trainer.fit([(bm.random.random((4, 3)), bm.random.random((4, 2)))],
523-
num_epoch=1)
528+
trainer.fit([(bm.random.random((4, 3)), bm.random.random((4, 2)))],
529+
num_epoch=2)
530+
531+
# the fit ran: losses are recorded and finite, and the weight moved.
532+
losses = trainer.get_hist_metric(phase='fit', metric='loss')
533+
assert len(losses) > 0
534+
assert all(np.isfinite(float(v)) for v in losses)
535+
assert not np.allclose(np.asarray(model.lin.W), w_before)
536+
537+
# pollution guard: a plain forward pass through a Dense must not raise from
538+
# a stale traced ``fit`` value left in the global ``share`` store.
539+
out = model(bm.random.random((4, 3)))
540+
assert tuple(out.shape) == (4, 2)
541+
assert bool(np.all(np.isfinite(np.asarray(out))))
524542

525543

526544
if __name__ == '__main__':

0 commit comments

Comments
 (0)