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
4 changes: 3 additions & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@ jobs:
MPLBACKEND: Agg # Use non-interactive backend for matplotlib
run: |
pytest --cov=brainpy --cov-report=xml brainpy/
- name: Upload coverage to Codecov
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: brainpy/BrainPy
Comment on lines +57 to +61

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚨 question (security): Explicit Codecov token usage in CI might be unnecessary for a public repo and increases secret surface area.

Using token: ${{ secrets.CODECOV_TOKEN }} adds an extra secret to manage and expose to this workflow. For public repos, recent codecov/codecov-action versions typically work with just the GitHub-provided token, which simplifies configuration and slightly reduces risk.

If this repo is public and not using private uploads or advanced Codecov features, consider verifying whether the explicit token and slug are required and removing them if they’re not.

files: ./coverage.xml
fail_ci_if_error: false

Expand Down
16 changes: 12 additions & 4 deletions brainpy/dnn/linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,21 @@ def update(self, x):
if self.b is not None:
res += self.b

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

# offline fitting data
if share.load('fit', False) and self.offline_fit_by is not None:
if self.offline_fit_by is not None and share.load('fit', False):
self.fit_record['input'] = x
self.fit_record['output'] = res
return res
Expand Down
22 changes: 10 additions & 12 deletions brainpy/losses/comparison_coverage_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,26 +221,24 @@ def test_class_wrapper(self):
# ---------------------------------------------------------------------------
class TestRegressionLosses:
def test_l1_loss_reductions(self):
# P1-L1: l1_loss delegates to braintools.metric.l1_loss, which for
# reduction='none' returns the per-row L1 *norm* (sum of abs over the
# trailing axes, reshaped to (N, -1)), NOT the per-row mean. So for the
# (2, 2) input below the 'none' output is the per-row sums [3, 7]; 'sum'
# then totals them (10) and 'mean' averages them (5). (The previous
# expectations of [1.5, 3.5]/5/2.5 encoded an incorrect per-row-mean
# assumption about braintools and were pre-existing baseline failures.)
# ``l1_loss`` delegates to ``braintools.metric.l1_loss`` (>=0.3.0), which
# reduces each sample to its *mean* absolute error over the trailing axes
# (shape (N,)) and then applies the batch reduction. For the (2, 2) input
# below the per-sample means are [mean(1,2), mean(3,4)] = [1.5, 3.5]; so
# 'none' -> [1.5, 3.5], 'sum' -> 5.0, 'mean' -> 2.5.
x = jnp.array([[1., 2.], [3., 4.]])
y = jnp.zeros((2, 2))
none = np.asarray(C.l1_loss(x, y, reduction='none'))
assert np.allclose(none, [3.0, 7.0]) # per-row L1 norm (sum of abs)
assert float(C.l1_loss(x, y, reduction='sum')) == pytest.approx(10.0)
assert float(C.l1_loss(x, y, reduction='mean')) == pytest.approx(5.0)
assert np.allclose(none, [1.5, 3.5]) # per-sample mean abs error
assert float(C.l1_loss(x, y, reduction='sum')) == pytest.approx(5.0)
assert float(C.l1_loss(x, y, reduction='mean')) == pytest.approx(2.5)

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

def test_l2_loss_elementwise(self):
out = np.asarray(C.l2_loss(jnp.array([2.0, 0.0]), jnp.array([0.0, 0.0])))
Expand Down
13 changes: 12 additions & 1 deletion brainpy/running/jax_multiprocessing_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,18 @@ def test_vectorize_map_partial_chunk():

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


Expand Down
50 changes: 34 additions & 16 deletions brainpy/train/back_propagation_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,22 +487,28 @@ def test_bptrainer_abstract_step_funcs_raise():


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

def test_dense_layer_fit_flag_is_traced_defect():
"""PIN: ``bp.dnn.Dense`` under a BPFF/BPTT fit loop raises on the ``fit`` flag.

``brainpy/dnn/linear.py:129`` does
``if share.load('fit', False) and self.online_fit_by is not None:``.
Under the installed ``brainstate`` (0.5.x), inside the jitted / grad-traced
fit step the ``fit`` flag is a JAX *tracer*, so the boolean ``and`` raises
``jax.errors.TracerBoolConversionError``. This blocks the canonical
``RNNCell``/``Dense`` BPTT example. It is an API-drift defect in the layer,
not in ``back_propagation.py``; pinned here so the regression is visible.
def test_dense_layer_fit_flag_under_grad_trace():
"""``bp.dnn.Dense`` must train under a BPFF fit loop without a tracer error.

Inside the grad-/jit-traced fit step the ``fit`` flag is a JAX *tracer*.
``Dense.update`` previously did
``if share.load('fit', False) and self.online_fit_by is not None:`` which
converted that tracer to a Python bool and raised
``jax.errors.TracerBoolConversionError``, blocking the canonical
``RNNCell``/``Dense`` BPTT example. ``Dense.update`` now checks the static
``*_fit_by`` configuration first so the tracer is only consulted when online
/offline fitting is enabled. This regression test trains a plain ``Dense``
for a couple of epochs and asserts the fit completes with finite losses and
an updated weight.

A *pollution guard* is included: a stale traced ``fit`` left in the global
``share`` store by a previous (broken) fit used to make the next forward
pass through a ``Dense`` raise. Running a plain forward pass after the fit
confirms no traced state leaked.
"""
import jax

class DenseFF(bp.DynamicalSystem):
def __init__(self):
super().__init__()
Expand All @@ -516,11 +522,23 @@ def reset_state(self, batch_size=1, **kwargs):

with bm.training_environment():
model = DenseFF()
w_before = np.asarray(model.lin.W).copy()
trainer = bp.BPFF(model, loss_fun=_mse, optimizer=bp.optim.Adam(lr=0.01),
progress_bar=False)
with pytest.raises(jax.errors.TracerBoolConversionError):
trainer.fit([(bm.random.random((4, 3)), bm.random.random((4, 2)))],
num_epoch=1)
trainer.fit([(bm.random.random((4, 3)), bm.random.random((4, 2)))],
num_epoch=2)

# the fit ran: losses are recorded and finite, and the weight moved.
losses = trainer.get_hist_metric(phase='fit', metric='loss')
assert len(losses) > 0
assert all(np.isfinite(float(v)) for v in losses)
assert not np.allclose(np.asarray(model.lin.W), w_before)

# pollution guard: a plain forward pass through a Dense must not raise from
# a stale traced ``fit`` value left in the global ``share`` store.
out = model(bm.random.random((4, 3)))
assert tuple(out.shape) == (4, 2)
assert bool(np.all(np.isfinite(np.asarray(out))))


if __name__ == '__main__':
Expand Down
Loading