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 JSON 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
Python
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
90
91
92
@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
Python
84
85
86
87
88
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
Python
90
91
92
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
Python
29
30
31
32
33
34
35
36
37
38
39
@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

load_chip_spec(path)

Load and validate a chip spec from a JSON file.

Source code in src/sc_neurocore/chip_compiler/chip_spec.py
Python
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def load_chip_spec(path: str | Path) -> ChipSpec:
    """Load and validate a chip spec from a JSON file."""
    source = Path(path)
    try:
        with source.open(encoding="utf-8") as f:
            payload = json.load(f)
    except json.JSONDecodeError as exc:
        raise ValueError(f"{source} is not valid chip spec JSON: {exc}") from exc

    if not isinstance(payload, dict):
        raise ValueError("chip spec JSON root must be an object")

    chip = _validate_chip_payload(payload, source=str(source))
    core = _validate_core_payload(payload["core"], source=str(source))
    return ChipSpec(core=core, **chip)

load_chip_spec() accepts UTF-8 JSON files for custom chip targets. The loader rejects non-object roots, missing or unexpected fields, invalid scalar types, non-finite numeric values, unsupported routing topologies, and malformed core specifications before constructing ChipSpec / CoreSpec objects.

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
Python
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
@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
Python
29
30
31
32
33
34
35
36
37
@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
Python
 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
194
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