Skip to content

Multi-Chip Hardware Compiler

Target-agnostic SNN compiler with built-in specs for Loihi 2, SynSense Xylo/Speck, BrainChip Akida, SpiNNaker2, and BrainScaleS-2.

Chip Specifications

sc_neurocore.chip_compiler.chip_spec

Chip specification model for multi-target compilation.

Each neuromorphic chip is described by a ChipSpec: cores, neurons/core, weight precision, connectivity constraints, supported neuron models, and on-chip learning capabilities. Built-in specs for Loihi 2, SynSense Xylo/Speck, BrainChip Akida, and SpiNNaker2.

Specs can be loaded from YAML for custom/future chips.

BUILTIN_CHIPS = {'loihi2': ChipSpec(name='loihi2', vendor='Intel', total_cores=128, core=(CoreSpec(max_neurons=128, max_synapses_per_neuron=8192, weight_bits=8, supported_neuron_types=['LIF', 'ALIF', 'Izhikevich', 'Compartmental'], has_on_chip_learning=True, learning_rules=['STDP', 'R-STDP', 'e-prop'], max_delay_steps=63)), clock_mhz=100, power_mw_per_core=0.5, routing_topology='mesh', max_fan_out=8192, analog_noise_cv=0.0), 'xylo': ChipSpec(name='xylo', vendor='SynSense', total_cores=1, core=(CoreSpec(max_neurons=1000, max_synapses_per_neuron=1000, weight_bits=8, supported_neuron_types=['IAF', 'LIF'], has_on_chip_learning=False, max_delay_steps=15)), clock_mhz=50, power_mw_per_core=0.1, routing_topology='crossbar', max_fan_out=1000, analog_noise_cv=0.0), 'speck': ChipSpec(name='speck', vendor='SynSense', total_cores=1, core=(CoreSpec(max_neurons=32768, max_synapses_per_neuron=512, weight_bits=4, supported_neuron_types=['IAF'], has_on_chip_learning=False, max_delay_steps=0)), clock_mhz=200, power_mw_per_core=0.5, routing_topology='crossbar', max_fan_out=512, analog_noise_cv=0.0), 'akida': ChipSpec(name='akida', vendor='BrainChip', total_cores=80, core=(CoreSpec(max_neurons=256, max_synapses_per_neuron=4096, weight_bits=4, supported_neuron_types=['IF', 'LIF'], has_on_chip_learning=True, learning_rules=['STDP'], max_delay_steps=0)), clock_mhz=300, power_mw_per_core=0.3, routing_topology='mesh', max_fan_out=4096, analog_noise_cv=0.0), 'spinnaker2': ChipSpec(name='spinnaker2', vendor='University of Manchester / Dresden', total_cores=152, core=(CoreSpec(max_neurons=1024, max_synapses_per_neuron=16384, weight_bits=16, supported_neuron_types=['LIF', 'Izhikevich', 'HH', 'Custom'], has_on_chip_learning=True, learning_rules=['STDP', 'R-STDP', 'custom'], max_delay_steps=256)), clock_mhz=500, power_mw_per_core=2.0, routing_topology='mesh', max_fan_out=16384, analog_noise_cv=0.0), 'brainscales2': ChipSpec(name='brainscales2', vendor='University of Heidelberg', total_cores=1, core=(CoreSpec(max_neurons=512, max_synapses_per_neuron=256, weight_bits=6, supported_neuron_types=['AdEx', 'LIF'], has_on_chip_learning=True, learning_rules=['STDP', 'correlation'], max_delay_steps=4)), clock_mhz=125, power_mw_per_core=30.0, routing_topology='crossbar', max_fan_out=256, analog_noise_cv=0.2)} module-attribute

ChipSpec dataclass

Full neuromorphic chip specification.

Parameters

name : str Chip identifier (e.g., 'loihi2', 'xylo', 'akida'). vendor : str total_cores : int core : CoreSpec Per-core specification (assumes homogeneous cores). clock_mhz : float power_mw_per_core : float Estimated dynamic power per active core. routing_topology : str 'mesh', 'crossbar', 'tree', 'ring' max_fan_out : int Maximum outgoing connections per neuron. analog_noise_cv : float Coefficient of variation for analog process variation. 0.0 for fully digital chips.

Source code in src/sc_neurocore/chip_compiler/chip_spec.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@dataclass
class ChipSpec:
    """Full neuromorphic chip specification.

    Parameters
    ----------
    name : str
        Chip identifier (e.g., 'loihi2', 'xylo', 'akida').
    vendor : str
    total_cores : int
    core : CoreSpec
        Per-core specification (assumes homogeneous cores).
    clock_mhz : float
    power_mw_per_core : float
        Estimated dynamic power per active core.
    routing_topology : str
        'mesh', 'crossbar', 'tree', 'ring'
    max_fan_out : int
        Maximum outgoing connections per neuron.
    analog_noise_cv : float
        Coefficient of variation for analog process variation.
        0.0 for fully digital chips.
    """

    name: str
    vendor: str
    total_cores: int
    core: CoreSpec
    clock_mhz: float = 100.0
    power_mw_per_core: float = 1.0
    routing_topology: str = "mesh"
    max_fan_out: int = 4096
    analog_noise_cv: float = 0.0

    @property
    def total_neurons(self) -> int:
        return self.total_cores * self.core.max_neurons

    @property
    def total_power_mw(self) -> float:
        return self.total_cores * self.power_mw_per_core

    def fits(self, n_neurons: int, max_fan_out: int = 0) -> bool:
        """Check if a network fits on this chip."""
        if n_neurons > self.total_neurons:
            return False
        return max_fan_out <= self.max_fan_out

    def cores_needed(self, n_neurons: int) -> int:
        """Minimum cores needed for N neurons."""
        return max(1, -(-n_neurons // self.core.max_neurons))  # ceil division

fits(n_neurons, max_fan_out=0)

Check if a network fits on this chip.

Source code in src/sc_neurocore/chip_compiler/chip_spec.py
81
82
83
84
85
def fits(self, n_neurons: int, max_fan_out: int = 0) -> bool:
    """Check if a network fits on this chip."""
    if n_neurons > self.total_neurons:
        return False
    return max_fan_out <= self.max_fan_out

cores_needed(n_neurons)

Minimum cores needed for N neurons.

Source code in src/sc_neurocore/chip_compiler/chip_spec.py
87
88
89
def cores_needed(self, n_neurons: int) -> int:
    """Minimum cores needed for N neurons."""
    return max(1, -(-n_neurons // self.core.max_neurons))  # ceil division

CoreSpec dataclass

Specification for one neuromorphic core.

Source code in src/sc_neurocore/chip_compiler/chip_spec.py
26
27
28
29
30
31
32
33
34
35
36
@dataclass
class CoreSpec:
    """Specification for one neuromorphic core."""

    max_neurons: int
    max_synapses_per_neuron: int
    weight_bits: int
    supported_neuron_types: list[str]
    has_on_chip_learning: bool = False
    learning_rules: list[str] = field(default_factory=list)
    max_delay_steps: int = 0

Compiler

sc_neurocore.chip_compiler.compiler

Compile an SNN to a target neuromorphic chip.

Partitions the network into cores, checks constraints (neurons/core, fan-out, weight precision, supported neuron types), quantizes weights, and produces a deployment map. Reports constraint violations with specific fix suggestions.

This is the foundation for the "GCC of neuromorphic computing" — one compiler that targets all chips via pluggable chip specs.

CompilationResult dataclass

Result of compiling an SNN to a chip target.

Source code in src/sc_neurocore/chip_compiler/compiler.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@dataclass
class CompilationResult:
    """Result of compiling an SNN to a chip target."""

    chip: str
    success: bool = False
    core_mappings: list[CoreMapping] = field(default_factory=list)
    total_cores_used: int = 0
    total_neurons_mapped: int = 0
    weight_bits: int = 0
    violations: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)
    quantized_weights: list[np.ndarray] = field(default_factory=list, repr=False)

    def summary(self) -> str:
        status = "SUCCESS" if self.success else "FAILED"
        lines = [
            f"Compilation [{self.chip}]: {status}",
            f"  Cores: {self.total_cores_used}",
            f"  Neurons: {self.total_neurons_mapped}",
            f"  Weight precision: {self.weight_bits}-bit",
        ]
        for v in self.violations:  # pragma: no cover
            lines.append(f"  [VIOLATION] {v}")
        for w in self.warnings:  # pragma: no cover
            lines.append(f"  [WARNING] {w}")
        return "\n".join(lines)

CoreMapping dataclass

Mapping of neurons to one chip core.

Source code in src/sc_neurocore/chip_compiler/compiler.py
28
29
30
31
32
33
34
35
36
@dataclass
class CoreMapping:
    """Mapping of neurons to one chip core."""

    core_id: int
    layer_index: int
    neuron_start: int
    neuron_end: int
    n_neurons: int

compile_for_chip(layer_sizes, weights=None, neuron_types=None, target='loihi2')

Compile an SNN to a target neuromorphic chip.

Parameters

layer_sizes : list of (n_inputs, n_neurons) weights : list of ndarray, optional Weight matrices per layer. If provided, will be quantized. neuron_types : list of str, optional Neuron type per layer (e.g., 'LIF', 'Izhikevich'). target : str or ChipSpec Target chip name or spec.

Returns

CompilationResult

Source code in src/sc_neurocore/chip_compiler/compiler.py
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
def compile_for_chip(
    layer_sizes: list[tuple[int, int]],
    weights: list[np.ndarray] | None = None,
    neuron_types: list[str] | None = None,
    target: str | ChipSpec = "loihi2",
) -> CompilationResult:
    """Compile an SNN to a target neuromorphic chip.

    Parameters
    ----------
    layer_sizes : list of (n_inputs, n_neurons)
    weights : list of ndarray, optional
        Weight matrices per layer. If provided, will be quantized.
    neuron_types : list of str, optional
        Neuron type per layer (e.g., 'LIF', 'Izhikevich').
    target : str or ChipSpec
        Target chip name or spec.

    Returns
    -------
    CompilationResult
    """
    if isinstance(target, str):
        chip = BUILTIN_CHIPS.get(target)
        if chip is None:
            return CompilationResult(
                chip=target,
                success=False,
                violations=[
                    f"Unknown chip target '{target}'. Available: {list(BUILTIN_CHIPS.keys())}"
                ],
            )
    else:
        chip = target

    result = CompilationResult(chip=chip.name, weight_bits=chip.core.weight_bits)
    violations = []
    warnings = []
    mappings = []

    total_neurons = sum(n for _, n in layer_sizes)
    result.total_neurons_mapped = total_neurons

    # Check total capacity
    if not chip.fits(total_neurons):
        violations.append(
            f"Network has {total_neurons} neurons but {chip.name} supports "
            f"max {chip.total_neurons} ({chip.total_cores} cores x "
            f"{chip.core.max_neurons} neurons/core)"
        )

    # Check neuron type compatibility
    if neuron_types is not None:
        for i, nt in enumerate(neuron_types):
            if nt not in chip.core.supported_neuron_types:
                violations.append(
                    f"Layer {i}: neuron type '{nt}' not supported on {chip.name}. "
                    f"Supported: {chip.core.supported_neuron_types}"
                )

    # Check fan-out per layer
    for i, (n_in, n_out) in enumerate(layer_sizes):
        if n_out > chip.max_fan_out:
            violations.append(
                f"Layer {i}: fan-out {n_out} exceeds {chip.name} max {chip.max_fan_out}"
            )

    # Partition into cores
    core_id = 0
    for i, (n_in, n_out) in enumerate(layer_sizes):
        neurons_remaining = n_out
        offset = 0
        while neurons_remaining > 0:
            batch = min(neurons_remaining, chip.core.max_neurons)
            mappings.append(
                CoreMapping(
                    core_id=core_id,
                    layer_index=i,
                    neuron_start=offset,
                    neuron_end=offset + batch,
                    n_neurons=batch,
                )
            )
            core_id += 1
            offset += batch
            neurons_remaining -= batch

    result.core_mappings = mappings
    result.total_cores_used = core_id

    if core_id > chip.total_cores:
        violations.append(f"Needs {core_id} cores but {chip.name} has {chip.total_cores}")

    # Quantize weights
    if weights is not None:
        bits = chip.core.weight_bits
        n_levels = 2**bits
        quantized = []
        for w in weights:
            abs_max = max(np.abs(w).max(), 1e-8)
            scale = abs_max / (n_levels // 2 - 1)
            q = np.round(w / scale) * scale
            q = np.clip(q, -abs_max, abs_max)
            quantized.append(q)

            # Warn about precision loss
            mse = float(np.mean((w - q) ** 2))
            if mse > 0.01 * float(np.mean(w**2)):  # pragma: no cover
                warnings.append(
                    f"Weight quantization to {bits}-bit introduces "
                    f"{mse / max(float(np.mean(w**2)), 1e-12) * 100:.1f}% relative error"
                )
        result.quantized_weights = quantized

    # Analog noise warning
    if chip.analog_noise_cv > 0.05:
        warnings.append(
            f"{chip.name} has {chip.analog_noise_cv:.0%} analog noise CV. "
            f"Use variation-aware training (digital_twin.FPGAMismatchModel)"
        )

    result.violations = violations
    result.warnings = warnings
    result.success = len(violations) == 0

    return result