Skip to content

Conversation

Legend101Zz
Copy link
Contributor

@Legend101Zz Legend101Zz commented Jun 27, 2025

C++ Dynamic Arrays for JIT Replacement

As part of our ongoing effort to replace Brian’s current Cython-based just-in-time (JIT) compilation mechanism, this PR introduces the first draft implementation of new C++-based dynamic arrays for use in the runtime system.

This is an initial step toward our broader goal of replacing Python-based runtime data structures (like DynamicArray, SpikeQueue, etc.) with fully native C++ equivalents. The long-term plan is to unify runtime and standalone modes using shared C++ templates—eliminating Cython entirely from the core simulation loop.


@Legend101Zz Legend101Zz marked this pull request as ready for review July 9, 2025 06:49
@Legend101Zz
Copy link
Contributor Author

Legend101Zz commented Jul 9, 2025


@mstimberg Finally got it working! 🎉
After battling countless bugs and syntax errors (and with my IDE refusing to play nice with Cython 🥲 , so had to catpture the errors while building itself only ), I finally managed to build the cythondynamicarray module and generate the .cpp and .so files successfully. 🥳

Screenshot 2025-07-09 at 19 08 00


@mstimberg
Copy link
Member

As I mentioned in a comment, I will only have a closer look at the code later, but I just made a small commit to fix a test suite issue (it was using Brian directly from source to get the location of the Cython cache dir, but importing Brian from source now fails). If the tests still fail, it will be about actual problems in this PR, I hope 😉

Copy link
Member

@mstimberg mstimberg left a comment

Choose a reason for hiding this comment

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

A few comments below – things seem to be a bit broken at the moment, but it should be easier to debug them now that the tests are correctly working again 😊

@Legend101Zz
Copy link
Contributor Author

All tests pass now :)

@Legend101Zz Legend101Zz requested a review from mstimberg July 12, 2025 19:47
@Legend101Zz
Copy link
Contributor Author

@mstimberg so some more context here on that unusual problem we are facing with the test_openmp_consistency test ... so as discussed I was sequentially removing parts of test that were added and for the last commit the testsuite failed : 732cbc5

Screenshot 2025-09-12 at 00 11 32

so this is as far as we can go in terms of removing things that were changed in the test ...

I'll revert back the above commit and also try to make sense of this more , like why the test fails if we remove this piece of code ( and that too as we know from earlier trials only when we run the full testsuite on all OS , if we run a partial testsuite just using the failing ubuntu OS that passes )

Ahh and also please whenever you get the chance , you can review the PR now as I guess we have hit the limit as to what changes we can remove from the test 😅

…as it already takes a copy"

This reverts commit 732cbc5.
Copy link
Member

@mstimberg mstimberg left a comment

Choose a reason for hiding this comment

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

I finally found some time to look into the PR in more details… Looks very good, I don't have any major objections, my main comments are about, well, comments, and some about potential to simplify the code. There's one big thing that is still missing: the statemonitor.pyx templates still goes through Python to resize the dynamic variable. This would make sense to change as part of this PR, I think. There is a similar issue for the synapse creation templates still calling owner.resize, but this is slightly more work to change, I'd be happy with doing this in a separate PR as well.

Comment on lines +130 to +138
for value, n_ambiguous in tests:
with catch_logs() as l:
G.v.__setitem__(SG, value)
assert (
len(l) == n_warnings
), f"expected {int(n_warnings)}, got {len(l)} warnings"
assert all(
[entry[1].endswith("ambiguous_string_expression") for entry in l]
ambiguous_found = sum(
[1 for entry in l if entry[1].endswith("ambiguous_string_expression")]
)
assert ambiguous_found == n_ambiguous, (
f"Expected {n_ambiguous} ambiguous warnings for value '{value}', "
f"but got {ambiguous_found}"
Copy link
Member

Choose a reason for hiding this comment

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

At least locally, this test seems to pass as it was before (i.e. without filtering by type of warning) – is there a reason to change it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ahh ...so the testsuite was failing earlier and I was not sure why , and since the test seemed trivial ( in the sense that warnings can change provided we changed alot of code ) so I redid the test a bit , let me try the older one again and if that works we can revert back to it :)

Copy link
Contributor Author

@Legend101Zz Legend101Zz Oct 7, 2025

Choose a reason for hiding this comment

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

@mstimberg I reverted the changes and now the test fails for me unfortunately...

Screenshot 2025-10-07 at 09 30 30

so what should we do ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

______________________________ test_state_variables_group_as_index_problematic ______________________________

    @pytest.mark.codegen_independent
    def test_state_variables_group_as_index_problematic():
        G = NeuronGroup(10, "v : 1")
        SG = G[4:9]
        G.v = 1
        tests = [("i", 1), ("N", 1), ("N + i", 2), ("v", 0)]
        for value, n_warnings in tests:
            with catch_logs() as l:
                G.v.__setitem__(SG, value)
>               assert (
                    len(l) == n_warnings
                ), f"expected {int(n_warnings)}, got {len(l)} warnings"
E               AssertionError: expected 1, got 2 warnings
E               assert 2 == 1
E                +  where 2 = len([('WARNING', 'brian2.core.variables.ambiguous_string_expression', "The string expression used for setting 'v' refers t.../python3.13/site-packages/_pytest/python.py', line 156, in pytest_pyfunc_call\n    result = testfunction(**testargs)")])

brian2/tests/test_subgroup.py:128: AssertionError
============================================= warnin

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, could you please change this assert statement so that we can see what the wrong warning is? I'm not getting it on my machine, so it might be something related to the compiler, for example. Try this and post what it prints:

                assert (
                    len(l) == n_warnings
                ), f"expected {int(n_warnings)} for '{value}', got {len(l)} warnings: {[entry[1] for entry in l]}"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mstimberg here

               assert (
                    len(l) == n_warnings
                ), f"expected {int(n_warnings)} for '{value}', got {len(l)} warnings: {[entry[1] for entry in l]}"
E               AssertionError: expected 1 for 'i', got 2 warnings: ['brian2.core.variables.ambiguous_string_expression', 'brian2.core.base.unused_brian_object']
E               assert 2 == 1
E                +  where 2 = len([('WARNING', 'brian2.core.variables.ambiguous_string_expression', "The string expression used for setting 'v' refers t.../python3.13/site-packages/_pytest/python.py', line 156, in pytest_pyfunc_call\n    result = testfunction(**testargs)")])

brian2/tests/test_subgroup.py:128: AssertionError

we get the extra 'brian2.core.base.unused_brian_object' error idk why

Copy link
Contributor Author

Choose a reason for hiding this comment

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

and this is trivial , because it would be so , as we never ran or network as we don't want that here ... so maybe I can add a simple check to avoid this ?

@Legend101Zz
Copy link
Contributor Author

I finally found some time to look into the PR in more details… Looks very good, I don't have any major objections, my main comments are about, well, comments, and some about potential to simplify the code. There's one big thing that is still missing: the statemonitor.pyx templates still goes through Python to resize the dynamic variable. This would make sense to change as part of this PR, I think. There is a similar issue for the synapse creation templates still calling owner.resize, but this is slightly more work to change, I'd be happy with doing this in a separate PR as well.

Great Thank you for the reviews , I'll start to look into them and resolve them quickly, so we can get this finally merged :)

@Legend101Zz
Copy link
Contributor Author

I finally found some time to look into the PR in more details… Looks very good, I don't have any major objections, my main comments are about, well, comments, and some about potential to simplify the code. There's one big thing that is still missing: the statemonitor.pyx templates still goes through Python to resize the dynamic variable. This would make sense to change as part of this PR, I think. There is a similar issue for the synapse creation templates still calling owner.resize, but this is slightly more work to change, I'd be happy with doing this in a separate PR as well.

d985c15

chnages done to remove python indirection from statemoniter.pyx

@Legend101Zz
Copy link
Contributor Author

@mstimberg something happened and the whole testsuite broke down ...

Is it related to the new python version release ?
Screenshot 2025-10-07 at 20 03 14

@Legend101Zz
Copy link
Contributor Author

also @mstimberg does it make sense to have the synapses changes we discussed for resize and _update_synapse_numbers to be part of this PR or do we do that in another PR ?

@mstimberg
Copy link
Member

also @mstimberg does it make sense to have the synapses changes we discussed for resize and _update_synapse_numbers to be part of this PR or do we do that in another PR ?

I think I prefer to have it in a separate PR – it is a well-defined separate issue and in general, it is better to have smaller (but non-trivial) PRs.

@mstimberg
Copy link
Member

@mstimberg something happened and the whole testsuite broke down ...

Is it related to the new python version release ?

Yes, this is very likely related to the new Python release, since tests now run with Python 3.14. I will have a closer look, in general we should be compatible with 3.14 (we are testing things automatically once a week against the unreleased upcoming Python and numpy versions, the last run from two days ago went through without issue: https://github.com/brian-team/brian2/actions/runs/18254511895

@mstimberg
Copy link
Member

Ok, the problem seems to be in the setup-python action, which has not yet been updated with the knowledge of 3.14 (it only has 3.14.0.rc3) – most people do not update their actions as quickly as we do (in our case we use an action to determine the latest Python version), but I am sure it will be fixed quite soon, given that the majority of Python packages on GitHub uses this action 😊

@mstimberg
Copy link
Member

It should only be a matter of minutes, I think: https://redirect.github.com/actions/python-versions/pull/357

@Legend101Zz
Copy link
Contributor Author

also @mstimberg does it make sense to have the synapses changes we discussed for resize and _update_synapse_numbers to be part of this PR or do we do that in another PR ?

I think I prefer to have it in a separate PR – it is a well-defined separate issue and in general, it is better to have smaller (but non-trivial) PRs.

great then .. I may kindly ask for your final review then :)

@mstimberg
Copy link
Member

The Python 3.14 installation issue is sorted out, but there seems to be a genuine test failure related to StateMonitor – I did not yet find time to look into it in more detail.

@mstimberg
Copy link
Member

So, I had a quick look at the test failure, and I think the issue is the one that I described here (where it was for SpikeMonitor, when StateMonitor still did the Python roundtrip for resizing): #1650 (comment) The fix should be the same as b8477f7, adapted for StateMonitor

@Legend101Zz
Copy link
Contributor Author

@mstimberg , now this should pass ( hopefully 🤞) ... the bug was minor I just did the self.variables[var].size = len(self.variables[var].get_value()) so for 2D arrays, we had a failure in test as then the assertion checks for the whole shape ...
Screenshot 2025-10-14 at 17 58 01

@Legend101Zz
Copy link
Contributor Author

Legend101Zz commented Oct 14, 2025

Phew, this is actually a non-trivial issue… The reason why things broke is that the abstract DynamicArrayVariable object (which links to the underlying data but also serves as a description of the data – e.g. code generation does not have to know the values, but needs to know size and type) has a size attribute that before got updated by going from Cython through Python. In your SpikeMonitor, the resizing is now done directly on the underlying C++ object, so it does not go through Python and does not update DynamicArrayVariable.size. For StateMonitor, there is still a call that goes back to Python to resize the DynamicArrayVariable object, so it still works. Now, in runtime mode we can fix this by having the size of a DynamicArrayVariable be dynamic, and derived from the underlying data. I got this to work on my machine, but unfortunately this will also break standalone mode… I have to think a bit about how we can best solve this. I think we might have to route DynamicArrayVariable.size through the device, so that it can work differently on runtime mode and standalone mode, but this would be a major change (and break brian2cuda, etc.), so I don't want to rush this. Maybe as an intermediate fix, you could add a after_run function to SpikeMonitor (this function already exists in the base class, and will automatically called at the end of the simulation; make sure to add a super()... call as well). In this function, you can then update the .size attribute of the dynamic arrays so that they reflect the size of the underlying data. This hopefully should make the tests pass and is maybe also the best general solution in the longer term – it should only break code that expects that the DynamicArrayVariable.size attribute for the SpikeMonitor items is up-to-date during a run, but I have a hard time thinking of a situation where something else would rely on the number of elements stored in a monitor during a run – this could only be some manual NetworkOperation, but I wouldn't worry about this for now.

@mstimberg , had a yander at this while looking at the above fix ... and did now think about this ... yes ideally , I agree with you the best solution would be for us to route DynamicArrayVariable.size through the device ... but as you mentioned that can break some of the brian2cuda stuff ... so what if we had something like :

# In Device base class
class Device:
    def supports_dynamic_size_access(self):
        """Whether this device supports reading array sizes dynamically."""
        return False
    
    def get_dynamic_array_size(self, array_var):
        raise NotImplementedError("This device does not support dynamic size access")

and then

class RuntimeDevice(Device):
    def supports_dynamic_size_access(self):
        return True
    
    def get_dynamic_array_size(self, array_var):
        val = self.get_value(array_var, access_data=True)
        return val.shape if array_var.ndim > 1 else len(val)

So then In ArrayVariable we would have :

class ArrayVariable(Variable):
    @property
    def size(self):
        """Get size - dynamic in runtime mode, no changes in standalone."""
        if (getattr(self, 'dynamic', False) and 
            self.device.supports_dynamic_size_access()):
            return self.device.get_dynamic_array_size(self)
        return self.size

another , high level thought would be to add this property in the Brian preferences to make this more configurable , just a thought :)

@Legend101Zz Legend101Zz requested a review from mstimberg October 15, 2025 00:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants