From acc67a6f1c7b1983716eebae650aa52e2276e91c Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Thu, 4 Jul 2024 14:44:50 +0800 Subject: [PATCH 001/187] =?UTF-8?q?=F0=9F=A4=96=20update=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/poetry-publish.yml | 3 +-- .github/workflows/pytest-ci.yml | 2 +- .pre-commit-config.yaml | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/poetry-publish.yml b/.github/workflows/poetry-publish.yml index 64172804..d505b8d9 100644 --- a/.github/workflows/poetry-publish.yml +++ b/.github/workflows/poetry-publish.yml @@ -27,6 +27,5 @@ jobs: - name: Publish python poetry package uses: JRubics/poetry-publish@v2.0 with: - python_version: "3.11" + poetry_install_options: "--sync" pypi_token: ${{ secrets.PYPI_API_TOKEN }} - ignore_dev_requirements: "yes" diff --git a/.github/workflows/pytest-ci.yml b/.github/workflows/pytest-ci.yml index 83b4d1f9..a5bfbb4b 100644 --- a/.github/workflows/pytest-ci.yml +++ b/.github/workflows/pytest-ci.yml @@ -31,7 +31,7 @@ jobs: - name: Install test dependencies run: | - poetry install --with test + poetry install --with test --sync - name: Run pytest uses: pavelzw/pytest-action@v2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 910bd466..3e03697b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,7 +46,7 @@ repos: args: [--pytest-test-first] - id: requirements-txt-fixer - id: pretty-format-json - args: [--autofix] + args: [--autofix, --indent 2] - id: no-commit-to-branch - repo: https://github.com/python-poetry/poetry From 03cf5fad155bce2af3fd1c4150f0cd6662c22753 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Thu, 4 Jul 2024 20:01:01 +0800 Subject: [PATCH 002/187] =?UTF-8?q?=E2=9C=A8=20MetaNeuron=20supports=20ANN?= =?UTF-8?q?=20runtime=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/components/neuron/base.py | 323 ++++++++++++++++-------------- paibox/components/neuron/utils.py | 70 +++++-- paibox/mixin.py | 9 +- paibox/types.py | 17 +- 4 files changed, 250 insertions(+), 169 deletions(-) diff --git a/paibox/components/neuron/base.py b/paibox/components/neuron/base.py index 29d5bb06..51899572 100644 --- a/paibox/components/neuron/base.py +++ b/paibox/components/neuron/base.py @@ -1,7 +1,13 @@ +import sys import warnings from collections.abc import Iterable from typing import Any, Literal, NoReturn, Optional, Union +if sys.version_info >= (3, 10): + from typing import TypeAlias +else: + from typing_extensions import TypeAlias + import numpy as np from numpy.typing import NDArray from paicorelib import ( @@ -13,13 +19,25 @@ SIM, TM, HwConfig, - MaxPoolingEnable, + InputWidthFormat, SpikeWidthFormat, + SNNModeEnable, + CoreMode, + get_core_mode, ) from paibox.base import NeuDyn from paibox.exceptions import PAIBoxWarning, ShapeError -from paibox.types import LeakVType, Shape, SpikeType, VoltageType +from paibox.types import ( + NEUOUT_U8_DTYPE, + SPIKE_DTYPE, + VOLTAGE_DTYPE, + LeakVType, + NeuOutType, + Shape, + SpikeType, + VoltageType, +) from paibox.utils import ( arg_check_non_neg, arg_check_non_pos, @@ -28,16 +46,31 @@ shape2num, ) -from .utils import NEG_THRES_MIN, _is_leak_v_overflow, _mask, vjt_overflow +from .utils import ( + BIT_TRUNCATE_MAX, + NEG_THRES_MIN, + _leak_v_check, + _mask, + vjt_overflow, + _input_width_format, + _spike_width_format, + _get_neu_out_dtype, +) __all__ = ["Neuron"] L = Literal +NeuOutTruncType: TypeAlias = NDArray[NEUOUT_U8_DTYPE] class MetaNeuron: """Meta neuron""" + input_width: InputWidthFormat + spike_width: SpikeWidthFormat + snn_en: SNNModeEnable + mode: CoreMode + def __init__( self, shape: Shape, @@ -53,6 +86,9 @@ def __init__( leak_v: Union[int, LeakVType], synaptic_integr: SIM, bit_truncation: int, + input_width: InputWidthFormat, + spike_width: SpikeWidthFormat, + snn_en: SNNModeEnable, overflow_strict: bool, keep_shape: bool = False, ) -> None: @@ -62,6 +98,12 @@ def __init__( self._shape = as_shape(shape) self._n_neuron = shape2num(self._shape) + self.input_width = input_width + self.spike_width = spike_width + self.snn_en = snn_en + # check whether the mode is valid + self.mode = get_core_mode(input_width, spike_width, snn_en) + # DO NOT modify the names of the following variables. # They will be exported to the parameter verification model. self.reset_mode = reset_mode @@ -76,6 +118,11 @@ def __init__( self.synaptic_integr = synaptic_integr self.bit_truncation = bit_truncation # Unsigned 5-bit + # Auxiliary attributes or variables. + self._thres_mask = _mask(threshold_mask_bits) + self.thres_mode = self.init_param(TM.NOT_EXCEEDED) + self.overflow_strict = overflow_strict + if isinstance(leak_v, int) or leak_v.size == 1: # np.array([x]) is treated as a scalar. self.leak_v = int(leak_v) @@ -89,17 +136,7 @@ def __init__( f"'leak' is either a scalar or have shape (output channels, ), but got ({self._shape[0]},)." ) - _is_leak_v_overflow(self.leak_v) - - # TODO These two config below are parameters of CORE. - self._spike_width_format: SpikeWidthFormat - self._pool_max_en: MaxPoolingEnable - - # Auxiliary attributes or variables. - self._thres_mask = _mask(threshold_mask_bits) - self.thres_mode = self.init_param(TM.NOT_EXCEEDED).astype(np.uint8) - self._v_th_rand = self.init_param(0).astype(np.int32) - self.overflow_strict = overflow_strict + _leak_v_check(self.leak_v) if self.synaptic_integr is SIM.MODE_STOCHASTIC: warnings.warn( @@ -121,8 +158,13 @@ def __init__( PAIBoxWarning, ) + if bit_truncation > BIT_TRUNCATE_MAX: + raise ValueError( + f"'bit_truncation' should be less than or equal to {BIT_TRUNCATE_MAX}." + ) + def _neuronal_charge( - self, incoming_v: VoltageType, vjt_pre: VoltageType, strict: bool = False + self, incoming_v: VoltageType, vjt_pre: VoltageType ) -> VoltageType: r"""1. Synaptic integration. @@ -135,13 +177,17 @@ def _neuronal_charge( `vjt` = `vjt_pre` + `_rho_w_ij` * \sum^{N-1}_{i=0} * x_i(t) * w_{i,j} """ if incoming_v.ndim == 2: - _v = incoming_v.sum(axis=1, dtype=np.int32) + _v = np.sum(incoming_v, axis=1) else: _v = incoming_v - v_charged = np.add(vjt_pre, _v, dtype=np.int32) + if self.snn_en: + v_charged = vjt_pre + _v + else: + # SNN_EN=0, the previous voltage is unused + v_charged = _v - return vjt_overflow(v_charged, strict) # Handle with overflow here + return vjt_overflow(v_charged, self.overflow_strict) def _neuronal_leak(self, vjt: VoltageType) -> VoltageType: r"""2. Leak integration. @@ -160,14 +206,17 @@ def _neuronal_leak(self, vjt: VoltageType) -> VoltageType: `vjt` = `vjt` + \sgn{`leak_v`}* `_ld` * `_F` """ - if self.leak_direction is LDM.MODE_FORWARD: - _ld = np.ones((self._n_neuron,), dtype=np.bool_) - else: - _ld = np.sign(vjt) + if self.snn_en: + if self.leak_direction is LDM.MODE_FORWARD: + _ld = 1 + else: + _ld = np.sign(vjt) - v_leaked = np.add(vjt, _ld * self.leak_v, dtype=np.int32) + v_leaked = vjt + _ld * self.leak_v + else: + v_leaked = vjt + self.bias - return v_leaked + return vjt_overflow(v_leaked, self.overflow_strict) def _neuronal_fire(self, vjt: VoltageType) -> SpikeType: r"""3. Threshold comparison. @@ -188,24 +237,13 @@ def _neuronal_fire(self, vjt: VoltageType) -> SpikeType: else `spike` = 0 """ - # fixed at 0 since we won't simulate random threshold - _v_th_rand = 0 & self._thres_mask - self._v_th_rand = self.init_param(_v_th_rand).astype(np.int32) - - if self.neg_thres_mode is NTM.MODE_RESET: - _v_th_neg = self.neg_threshold + _v_th_rand - else: - _v_th_neg = self.neg_threshold - - """Fire""" self.thres_mode = np.where( - vjt >= self.pos_threshold + _v_th_rand, + vjt >= self.pos_threshold, TM.EXCEED_POSITIVE, - np.where(vjt < -_v_th_neg, TM.EXCEED_NEGATIVE, TM.NOT_EXCEEDED), - ).astype(np.uint8) - - spike = np.equal(self.thres_mode, TM.EXCEED_POSITIVE) + np.where(vjt + self.neg_threshold < 0, TM.EXCEED_NEGATIVE, TM.NOT_EXCEEDED), + ) + spike = self.thres_mode == TM.EXCEED_POSITIVE return spike def _neuronal_reset(self, vjt: VoltageType) -> VoltageType: @@ -236,48 +274,34 @@ def _neuronal_reset(self, vjt: VoltageType) -> VoltageType: def _when_exceed_pos() -> VoltageType: if self.reset_mode is RM.MODE_NORMAL: - return np.full((self._n_neuron,), self.reset_v, dtype=np.int32) - + return np.full_like(vjt, self.reset_v) elif self.reset_mode is RM.MODE_LINEAR: - return np.subtract( - vjt, self.pos_threshold + self._v_th_rand, dtype=np.int32 - ) + return vjt - self.pos_threshold else: # RM.MODE_NONRESET return vjt def _when_exceed_neg() -> VoltageType: if self.neg_thres_mode is NTM.MODE_RESET: if self.reset_mode is RM.MODE_NORMAL: - return np.full((self._n_neuron,), -self.reset_v, dtype=np.int32) + return np.full_like(vjt, -self.reset_v) elif self.reset_mode is RM.MODE_LINEAR: - return np.add( - vjt, - self.neg_threshold + self._v_th_rand, - dtype=np.int32, - ) + return vjt + self.neg_threshold else: # RM.MODE_NONRESET return vjt - else: - return np.full((self._n_neuron,), -self.neg_threshold, dtype=np.int32) + return np.full_like(vjt, -self.neg_threshold) # USE "=="! v_reset = np.where( self.thres_mode == TM.EXCEED_POSITIVE, _when_exceed_pos(), - np.where( - self.thres_mode == TM.EXCEED_NEGATIVE, - _when_exceed_neg(), - vjt, - ), - ).astype(np.int32) - - self._aux_post_hook() + np.where(self.thres_mode == TM.EXCEED_NEGATIVE, _when_exceed_neg(), vjt), + ) - return v_reset + return v_reset.astype(VOLTAGE_DTYPE) - def _relu(self, vj: VoltageType) -> VoltageType: - r"""ReLU(ANN mode ONLY) + def _bit_truncate(self, vj: VoltageType) -> NeuOutTruncType: + r"""Bit Truncation. If spiking width format is `WIDTH_1BIT`, then if `vj` >= `_pos_threshold`, then @@ -290,66 +314,53 @@ def _relu(self, vj: VoltageType) -> VoltageType: else `_yj` = 0 - NOTE: Truncation of membrane potential - _bit_truncation Position of truncation - 0 8'd0 - 1 [0], 7'd0 - 2 [1:0], 6'd0 - X [X-1:0], {8-X}'d0 - 7 [6:0], 1'd0 - 8 [7:0] - ... ... - X [X-1:X-8] + NOTE: output under x-bit truncation + _bit_truncation Position of truncation + 0 8'd0 + 1 [0], 7'd0 + 2 [1:0], 6'd0 + X [X-1:0], {8-X}'d0 + 7 [6:0], 1'd0 + 8 [7:0] + ... ... + X [X-1:X-8] + + If the MSB of voltage is greater than the truncation bit, return 8'd255. """ - def _when_exceed_pos() -> VoltageType: - if self._spike_width_format is SpikeWidthFormat.WIDTH_1BIT: - return np.ones((self._n_neuron,), dtype=np.int32) - - if self.bit_truncation >= 8: - return np.full( - (self._n_neuron,), - ((vj >> self.bit_truncation) - 8) & ((1 << 8) - 1), - dtype=np.int32, - ) - elif self.bit_truncation > 0: - _mask = (1 << self.bit_truncation) - 1 - _truncated_vj = vj & _mask - return np.full( - (self._n_neuron,), - _truncated_vj << (8 - self.bit_truncation), - dtype=np.int32, - ) + def _truncate() -> VoltageType: + if (vj >> self.bit_truncation) > 0: # Saturate truncation + return np.full_like(vj, _mask(8)) + elif self.bit_truncation == 0: + return self._vjt0 + elif self.bit_truncation < 8: + return (vj << (8 - self.bit_truncation)) & _mask(8) else: - return np.zeros((self._n_neuron,), dtype=np.int32) + return (vj >> (self.bit_truncation - 8)) & _mask(8) - y = np.where( - vj >= self.pos_threshold, - _when_exceed_pos(), - np.zeros((self._n_neuron,), dtype=np.int32), - ).astype(np.int32) - - return y + v_truncated = np.where( + self.thres_mode == TM.EXCEED_POSITIVE, _truncate(), self._vjt0 + ) - def _max_pooling(self, x: np.ndarray) -> None: - # TODO - pass + return v_truncated.astype(NEUOUT_U8_DTYPE) def _aux_pre_hook(self) -> None: - """Pre-hook before the entire activation.""" + """Pre-hook before the entire update.""" pass def _aux_post_hook(self) -> None: - """Post-hook after the entire activation.""" - # Reset the auxiliary threshold mode. - self.thres_mode = self.init_param(TM.NOT_EXCEEDED).astype(np.uint8) + """Post-hook after the entire update.""" + # Reset the auxiliary threshold mode + self.thres_mode = self.init_param(TM.NOT_EXCEEDED) def update( self, incoming_v: VoltageType, vjt_pre: VoltageType - ) -> tuple[SpikeType, VoltageType, NDArray[np.uint8]]: - """Update at one time step.""" + ) -> tuple[Union[SpikeType, NeuOutTruncType], VoltageType]: + """Update at one timestep.""" + self._aux_pre_hook() + # 1. Charge - v_charged = self._neuronal_charge(incoming_v, vjt_pre, self.overflow_strict) + v_charged = self._neuronal_charge(incoming_v, vjt_pre) # 2. Leak & fire if self.leak_comparison is LCM.LEAK_BEFORE_COMP: @@ -359,17 +370,38 @@ def update( spike = self._neuronal_fire(v_charged) v_leaked = self._neuronal_leak(v_charged) - # Store the intermediate threshold mode & return - _debug_thres_mode = self.thres_mode - - # 3. Reset + # 3. Reset. Reset is performed in all modes. v_reset = self._neuronal_reset(v_leaked) - return spike, v_reset, _debug_thres_mode + if self.spike_width is SpikeWidthFormat.WIDTH_8BIT: + # Althought the truncated voltage is of type VOLTAGE_DTYPE, its value <= uint8. + # The voltage to truncate is the one before neuronal reset. + v_truncated = self._bit_truncate(v_leaked) + + self._aux_post_hook() + + if self.spike_width is SpikeWidthFormat.WIDTH_1BIT: + # When output width is 1 bit, bit truncation is not performed. + return spike, v_reset + else: + return v_truncated, v_reset def init_param(self, param: Any) -> np.ndarray: return np.full((self._n_neuron,), param) + @property + def _neu_out_dtype(self) -> type[Union[SPIKE_DTYPE, NEUOUT_U8_DTYPE]]: + """dtype of output of neuron.""" + return _get_neu_out_dtype(self.spike_width) + + @property + def _vjt0(self) -> VoltageType: + return self.init_param(0).astype(VOLTAGE_DTYPE) + + @property + def _neu_out0(self) -> NeuOutType: + return self.init_param(0).astype(self._neu_out_dtype) + @property def varshape(self) -> tuple[int, ...]: return self._shape if self.keep_shape else (self._n_neuron,) @@ -397,11 +429,14 @@ def __init__( leak_integration_mode: Union[L[0, 1], bool, LIM] = LIM.MODE_DETERMINISTIC, leak_v: Union[int, LeakVType] = 0, synaptic_integration_mode: Union[L[0, 1], bool, SIM] = SIM.MODE_DETERMINISTIC, - bit_truncation: int = 0, + bit_truncation: int = 8, *, delay: int = 1, tick_wait_start: int = 1, tick_wait_end: int = 0, + input_width: Union[L[1, 8], InputWidthFormat] = InputWidthFormat.WIDTH_1BIT, + spike_width: Union[L[1, 8], SpikeWidthFormat] = SpikeWidthFormat.WIDTH_1BIT, + snn_en: bool = True, unrolling_factor: int = 1, overflow_strict: bool = False, keep_shape: bool = True, @@ -411,6 +446,12 @@ def __init__( # XXX *(-1) if passing a negative threshold > 0 neg_threshold = (-1) * neg_threshold + if bit_truncation > BIT_TRUNCATE_MAX: + raise ValueError( + f"'bit_truncation' should be less than or equal to {BIT_TRUNCATE_MAX}, " + f"but got {bit_truncation}." + ) + super().__init__( shape, reset_mode, @@ -425,33 +466,26 @@ def __init__( leak_v, SIM(synaptic_integration_mode), arg_check_non_neg(bit_truncation, "bit of tuncation"), + _input_width_format(input_width), + _spike_width_format(spike_width), + SNNModeEnable(snn_en), overflow_strict, keep_shape, ) super(MetaNeuron, self).__init__(name) """Stateful attributes. Vector.""" - # Initial vjt is fixed at 0. - self.set_memory("_vjt", self.init_param(0).astype(np.int32)) - self.set_memory("_inner_spike", self.init_param(0).astype(np.bool_)) - - # Not supported for attributes in ANN mode - self.set_memory("vj", self.init_param(0).astype(np.int32)) - self.set_memory("y", self.init_param(0).astype(np.int32)) - - """Auxiliary internal stateful attributes for debugging""" - self.set_memory( - "_debug_thres_mode", self.init_param(TM.NOT_EXCEEDED).astype(np.uint8) - ) - - # Delay registers + self.set_memory("_vjt", self._vjt0) # Initial vjt is fixed at 0. + self.set_memory("_neu_out", self._neu_out0) self.set_memory( "delay_registers", np.zeros( - (HwConfig.N_TIMESLOT_MAX,) + self._inner_spike.shape, dtype=np.bool_ + (HwConfig.N_TIMESLOT_MAX,) + self._neu_out.shape, + dtype=self._neu_out.dtype, ), ) + """Auxiliary internal stateful attributes for debugging""" self._delay = arg_check_pos(delay, "'delay'") self._tws = arg_check_non_neg(tick_wait_start, "'tick_wait_start'") self._twe = arg_check_non_neg(tick_wait_end, "'tick_wait_end'") @@ -462,31 +496,30 @@ def __len__(self) -> int: def __call__( self, x: Optional[np.ndarray] = None, *args, **kwargs - ) -> Optional[SpikeType]: + ) -> Optional[NeuOutType]: return self.update(x, *args, **kwargs) def update( self, x: Optional[np.ndarray] = None, *args, **kwargs - ) -> Optional[SpikeType]: + ) -> Optional[NeuOutType]: # Priority order is a must. # The neuron doesn't work if `tws = 0` & done working # until `t - tws + 1 > twe` under the condition `twe > 0`. if not self.is_working(): - self._inner_spike = self.init_param(0).astype(np.bool_) + self._neu_out.fill(0) return None - # The neuron is going to work. if x is None: x = self.sum_inputs() + else: + x = np.atleast_1d(x) - self._inner_spike, self._vjt, self._debug_thres_mode = super().update( - x, self._vjt - ) + self._neu_out, self._vjt = super().update(x, self._vjt) idx = (self.timestamp + self.delay_relative - 1) % HwConfig.N_TIMESLOT_MAX - self.delay_registers[idx] = self._inner_spike.copy() + self.delay_registers[idx] = self._neu_out.copy() - return self._inner_spike + return self._neu_out def reset_state(self, *args, **kwargs) -> None: self.reset_memory() # Call reset of `StatusMemory`. @@ -583,16 +616,16 @@ def num_out(self) -> int: return self._n_neuron @property - def output(self) -> SpikeType: - return self.delay_registers + def output(self) -> NeuOutType: + return self._neu_out @property - def spike(self) -> SpikeType: - return self._inner_spike + def spike(self) -> NeuOutType: + return self._neu_out @property - def feature_map(self) -> SpikeType: - return self._inner_spike.reshape(self.varshape) + def feature_map(self) -> NeuOutType: + return self._neu_out.reshape(self.varshape) @property def voltage(self) -> VoltageType: diff --git a/paibox/components/neuron/utils.py b/paibox/components/neuron/utils.py index d661dd2c..055c03f8 100644 --- a/paibox/components/neuron/utils.py +++ b/paibox/components/neuron/utils.py @@ -1,14 +1,27 @@ import warnings -from typing import Union +from typing import Literal, Union import numpy as np +from paicorelib import InputWidthFormat, SpikeWidthFormat from paicorelib.framelib.utils import _mask -from paicorelib.ram_model import LEAK_V_BIT_MAX, LEAK_V_MAX, LEAK_V_MIN +from paicorelib.ram_model import ( + BIT_TRUNCATE_MAX, + LEAK_V_BIT_MAX, + LEAK_V_MAX, + LEAK_V_MIN, +) from paicorelib.ram_model import NEG_THRES_MAX as NEG_THRES_UNSIGNED_MAX from paicorelib.ram_model import VJT_MAX, VJT_MIN, VJT_PRE_BIT_MAX from paibox.exceptions import FunctionalError, PAIBoxWarning -from paibox.types import LeakVType, VoltageType +from paibox.types import ( + LeakVType, + NEUOUT_U8_DTYPE, + SPIKE_DTYPE, + VoltageType, + VOLTAGE_DTYPE, +) + NEG_THRES_MIN = -NEG_THRES_UNSIGNED_MAX @@ -22,8 +35,8 @@ def _is_vjt_overflow(vjt: VoltageType, strict: bool = False) -> bool: - # NOTE: In most cases, membrane potential overflow won't occur, - # otherwise the result is incorrect. + # NOTE: In most cases, membrane potential overflow won't occur, otherwise the result + # may be incorrect. if np.any(vjt > VJT_MAX) or np.any(vjt < VJT_MIN): if strict: raise FunctionalError(VJT_OVERFLOW_TEXT) @@ -51,18 +64,45 @@ def vjt_overflow(vjt: VoltageType, strict: bool = False) -> VoltageType: vjt + VJT_RANGE_LIMIT, vjt, ), - ).astype(np.int32) + ).astype(VOLTAGE_DTYPE) -def _is_leak_v_overflow(leak_v: Union[int, LeakVType], strict: bool = True) -> None: +def _leak_v_check(leak_v: Union[int, LeakVType]) -> None: if isinstance(leak_v, int): if leak_v > LEAK_V_MAX or leak_v < LEAK_V_MIN: - if strict: - raise FunctionalError(LEAK_V_OVERFLOW_TEXT) - else: - warnings.warn(LEAK_V_OVERFLOW_TEXT, PAIBoxWarning) - elif np.any(leak_v > LEAK_V_MAX) or np.any(leak_v < LEAK_V_MIN): - if strict: raise FunctionalError(LEAK_V_OVERFLOW_TEXT) - else: - warnings.warn(LEAK_V_OVERFLOW_TEXT, PAIBoxWarning) + + elif np.any(leak_v > LEAK_V_MAX) or np.any(leak_v < LEAK_V_MIN): + raise FunctionalError(LEAK_V_OVERFLOW_TEXT) + + +L = Literal + + +def _input_width_format(iwf: Union[L[1, 8], InputWidthFormat]) -> InputWidthFormat: + if isinstance(iwf, InputWidthFormat): + return iwf + + if iwf == 1: + return InputWidthFormat.WIDTH_1BIT + else: + return InputWidthFormat.WIDTH_8BIT + + +def _spike_width_format(swf: Union[L[1, 8], SpikeWidthFormat]) -> SpikeWidthFormat: + if isinstance(swf, SpikeWidthFormat): + return swf + + if swf == 1: + return SpikeWidthFormat.WIDTH_1BIT + else: + return SpikeWidthFormat.WIDTH_8BIT + + +def _get_neu_out_dtype( + swf: SpikeWidthFormat, +) -> type[Union[SPIKE_DTYPE, NEUOUT_U8_DTYPE]]: + if swf is SpikeWidthFormat.WIDTH_1BIT: + return SPIKE_DTYPE + else: + return NEUOUT_U8_DTYPE diff --git a/paibox/mixin.py b/paibox/mixin.py index 95c5f5cf..d96f921e 100644 --- a/paibox/mixin.py +++ b/paibox/mixin.py @@ -10,7 +10,7 @@ from .exceptions import RegisterError from .naming import get_unique_name from .node import NodeDict -from .types import VoltageType +from .types import VOLTAGE_DTYPE, VoltageType if typing.TYPE_CHECKING: from paibox.components import FullConnectedSyn @@ -147,13 +147,12 @@ def unregister_master(self, key: str) -> Optional["FullConnectedSyn"]: def get_master_node(self, key: str) -> Optional[Any]: return self.master_nodes.get(key, None) - def sum_inputs(self, *, init: VoltageType = 0, **kwargs) -> VoltageType: # type: ignore - # TODO Out is a np.ndarray right now, but it may be more than one type. - output = init + def sum_inputs(self, *args, **kwargs) -> VoltageType: + output = 0 for node in self.master_nodes.values(): output += node.output.copy() - return np.array(output).astype(np.int32) + return np.asarray(output, dtype=VOLTAGE_DTYPE) class TimeRelatedNode(MixIn): diff --git a/paibox/types.py b/paibox/types.py index 37cdea03..82020805 100644 --- a/paibox/types.py +++ b/paibox/types.py @@ -17,8 +17,17 @@ DataArrayType = TypeVar( "DataArrayType", int, np.bool_, np.integer, list[int], tuple[int, ...], np.ndarray ) -LeakVType: TypeAlias = NDArray[np.int32] -SpikeType: TypeAlias = NDArray[np.bool_] -SynOutType: TypeAlias = NDArray[np.int32] -VoltageType: TypeAlias = NDArray[np.int32] + +LEAK_V_DTYPE = np.int32 +SPIKE_DTYPE = np.bool_ +VOLTAGE_DTYPE = np.int32 +NEUOUT_SPIKE_DTYPE = np.bool_ +NEUOUT_U8_DTYPE = np.uint8 +NEUOUT_DTYPE = Union[NEUOUT_SPIKE_DTYPE, NEUOUT_U8_DTYPE] + +LeakVType: TypeAlias = NDArray[LEAK_V_DTYPE] +SpikeType: TypeAlias = NDArray[SPIKE_DTYPE] +SynOutType: TypeAlias = NDArray[VOLTAGE_DTYPE] +VoltageType: TypeAlias = NDArray[VOLTAGE_DTYPE] +NeuOutType: TypeAlias = NDArray[NEUOUT_DTYPE] WeightType: TypeAlias = NDArray[Union[np.bool_, np.int8]] From 2dfbd910b2cc5999a43e646b5712b5a4e284215e Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Thu, 4 Jul 2024 20:01:55 +0800 Subject: [PATCH 003/187] =?UTF-8?q?=E2=9C=85=20add=20tests=20for=20neuron?= =?UTF-8?q?=20in=20all=20runtime=20modes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/components/neuron/test_neurons.py | 185 +++++++++++++++++++----- 1 file changed, 151 insertions(+), 34 deletions(-) diff --git a/tests/components/neuron/test_neurons.py b/tests/components/neuron/test_neurons.py index 35281c56..5db42544 100644 --- a/tests/components/neuron/test_neurons.py +++ b/tests/components/neuron/test_neurons.py @@ -1,14 +1,17 @@ import json from copy import copy +from typing import Any, Literal import numpy as np +from numpy.typing import NDArray import pytest -from paicorelib import LCM, LDM, LIM, NTM, RM, SIM, TM, NeuronAttrs +from paicorelib import CoreMode, LCM, LDM, LIM, NTM, RM, SIM, TM, NeuronAttrs import paibox as pb from paibox.components import Neuron from paibox.components.neuron.utils import VJT_MAX, VJT_MIN from paibox.exceptions import ShapeError +from paibox.types import NEUOUT_U8_DTYPE, VoltageType from paibox.utils import as_shape, shape2num @@ -54,6 +57,29 @@ def test_NeuronParams_check(): with pytest.raises(ShapeError): n4 = pb.LIF((10, 20), 1, bias=np.ones((100,))) + # If CoreMode specifies all configurations, there will be no invalid situations. + if len(CoreMode) < 8: + with pytest.raises(ValueError): + n5 = pb.LIF((100,), 10, input_width=8, spike_width=8, snn_en=True) + + +L = Literal + + +def _reg_kwds(iw: L[1, 8], sw: L[1, 8], snn_en: L[0, 1]) -> dict[str, Any]: + return {"input_width": iw, "spike_width": sw, "snn_en": bool(snn_en)} + + +_reg000_kwds = _reg_kwds(1, 1, 0) +_reg001_kwds = _reg_kwds(1, 1, 1) +_reg010_kwds = _reg_kwds(1, 8, 0) +_reg011_kwds = _reg_kwds(1, 8, 1) +_reg100_kwds = _reg_kwds(8, 1, 0) +_reg110_kwds = _reg_kwds(8, 8, 0) +_bann_kwds = _reg000_kwds +_ann_kwds = _reg110_kwds +_snn_kwds = _reg001_kwds + class TestNeuronBehavior: sim = SIM.MODE_DETERMINISTIC @@ -93,6 +119,7 @@ def test_neuronal_charge(self, incoming_v, x, expected): self.sim, self.bt, keep_shape=True, + **_snn_kwds, ) v_charged = n1._neuronal_charge(x, incoming_v) @@ -140,6 +167,7 @@ def test_neuronal_leak(self, lim, ld, incoming_v, leak_v, expected): self.sim, self.bt, keep_shape=True, + **_snn_kwds, ) v_leaked = n1._neuronal_leak(incoming_v) @@ -172,6 +200,7 @@ def test_neuronal_fire(self, ntm, incoming_v, neg_thres, pos_thres, expected): self.sim, self.bt, keep_shape=True, + **_snn_kwds, ) spike = n1._neuronal_fire(incoming_v) @@ -208,6 +237,7 @@ def test_neuronal_reset(self, ntm, thr_mode, reset_mode, expected): self.sim, self.bt, keep_shape=True, + **_snn_kwds, ) # Set the threshold mode manually @@ -253,6 +283,7 @@ def test_vjt_overflow(self, incoming_v, expected_v, expected_spike): self.leak_v, self.sim, self.bt, + **_snn_kwds, ) pb.FRONTEND_ENV["t"] += 1 # Only update when n1 starts working @@ -288,13 +319,12 @@ def test_neuron_keep_shape(): n2 = pb.TonicSpiking((4, 4), 5, keep_shape=False) assert n1.spike.shape == (16,) + assert n1.spike.shape == n1.output.shape assert n1.voltage.shape == (4, 4) - assert n1.output.shape == (256, 16) assert n1.feature_map.shape == (4, 4) assert n2.spike.shape == (16,) assert n2.voltage.shape == (16,) - assert n2.output.shape == (256, 16) assert n2.feature_map.shape == (16,) @@ -364,11 +394,11 @@ def test_NeuronSubView_illegal(self, slice, expectation): n_subview = n[slice] -class TestNeuron: +class TestNeuronModeSNN: # iss = 001 def test_IF_hard_reset(self): n1 = pb.IF(1, 5, 2) - inp_data = np.array([2, -1, 3, 5, 1, 2, 4, -2], dtype=np.int8) + incoming_v = np.array([2, -1, 3, 5, 1, 2, 4, -2], dtype=np.int8) expected_spike = np.array( [[0], [0], [0], [1], [0], [1], [1], [0]], dtype=np.bool_ ) @@ -376,9 +406,9 @@ def test_IF_hard_reset(self): [[2], [1], [4], [2], [3], [2], [2], [0]], dtype=np.int32 ) - for i in range(inp_data.size): + for i in range(incoming_v.size): pb.FRONTEND_ENV["t"] += 1 - n1.update(inp_data[i]) + n1.update(incoming_v[i]) assert np.array_equal(n1.spike, expected_spike[i]) assert np.array_equal(n1.voltage, expected_vol[i]) @@ -386,7 +416,7 @@ def test_IF_hard_reset(self): def test_IF_soft_reset(self): n1 = pb.IF(1, 5, None) - inp_data = np.array([2, -1, 3, 5, 1, 2, 4, -2], dtype=np.int8) + incoming_v = np.array([2, -1, 3, 5, 1, 2, 4, -2], dtype=np.int8) expected_spike = np.array( [[0], [0], [0], [1], [1], [0], [1], [0]], dtype=np.bool_ ) @@ -394,9 +424,9 @@ def test_IF_soft_reset(self): [[2], [1], [4], [4], [0], [2], [1], [-1]], dtype=np.int32 ) - for i in range(inp_data.size): + for i in range(incoming_v.size): pb.FRONTEND_ENV["t"] += 1 - n1.update(inp_data[i]) + n1.update(incoming_v[i]) assert np.array_equal(n1.spike, expected_spike[i]) assert np.array_equal(n1.voltage, expected_vol[i]) @@ -405,7 +435,7 @@ def test_LIF_hard_reset(self): # hard reset + leak before comparison n1 = pb.LIF(shape=1, threshold=5, reset_v=2, leak_v=-1) - inp_data = np.array([2, -1, 3, 5, 1, 2, 4, -2], dtype=np.int8) + incoming_v = np.array([2, -1, 3, 5, 1, 2, 4, -2], dtype=np.int8) expected_spike = np.array( [[0], [0], [0], [1], [0], [0], [1], [0]], dtype=np.bool_ ) @@ -413,9 +443,9 @@ def test_LIF_hard_reset(self): [[1], [-1], [1], [2], [2], [3], [2], [-1]], dtype=np.int32 ) - for i in range(inp_data.size): + for i in range(incoming_v.size): pb.FRONTEND_ENV["t"] += 1 - n1.update(inp_data[i]) + n1.update(incoming_v[i]) assert np.array_equal(n1.spike, expected_spike[i]) assert np.array_equal(n1.voltage, expected_vol[i]) @@ -423,7 +453,7 @@ def test_LIF_hard_reset(self): def test_LIF_soft_reset(self): n1 = pb.LIF(1, 5, reset_v=None, leak_v=-1) - inp_data = np.array([2, -1, 3, 5, 1, 2, 4, -2], dtype=np.int8) + incoming_v = np.array([2, -1, 3, 5, 1, 2, 4, -2], dtype=np.int8) expected_spike = np.array( [[0], [0], [0], [1], [0], [0], [0], [0]], dtype=np.bool_ ) @@ -431,9 +461,9 @@ def test_LIF_soft_reset(self): [[1], [-1], [1], [0], [0], [1], [4], [1]], dtype=np.int32 ) - for i in range(inp_data.size): + for i in range(incoming_v.size): pb.FRONTEND_ENV["t"] += 1 - n1.update(inp_data[i]) + n1.update(incoming_v[i]) assert np.array_equal(n1.spike, expected_spike[i]) assert np.array_equal(n1.voltage, expected_vol[i]) @@ -443,13 +473,13 @@ def test_LIF_with_bias(self): n1 = pb.LIF(shape=1, threshold=6, reset_v=1, leak_v=0, bias=2) assert n1.leak_v == n1.bias == 2 - inp_data = np.array([1, 1, 0, 1, 0, 1], dtype=np.bool_) + incoming_v = np.array([1, 1, 0, 1, 0, 1], dtype=np.bool_) expected_spike = np.array([[0], [1], [0], [1], [0], [1]], dtype=np.bool_) expected_vol = np.array([[3], [1], [3], [1], [3], [1]], dtype=np.int32) - for i in range(inp_data.size): + for i in range(incoming_v.size): pb.FRONTEND_ENV["t"] += 1 - n1.update(inp_data[i]) + n1.update(incoming_v[i]) assert np.array_equal(n1.spike, expected_spike[i]) assert np.array_equal(n1.voltage, expected_vol[i]) @@ -459,13 +489,13 @@ def test_LIF_both_leak_bias(self): n1 = pb.LIF(shape=1, threshold=6, leak_v=-1, bias=2) assert n1.leak_v == n1.bias == 1 - inp_data = np.array([1, 1, 0, 1, 0, 1], dtype=np.bool_) + incoming_v = np.array([1, 1, 0, 1, 0, 1], dtype=np.bool_) expected_spike = np.array([[0], [0], [0], [1], [0], [0]], dtype=np.bool_) expected_vol = np.array([[2], [4], [5], [1], [2], [4]], dtype=np.int32) - for i in range(inp_data.size): + for i in range(incoming_v.size): pb.FRONTEND_ENV["t"] += 1 - n1.update(inp_data[i]) + n1.update(incoming_v[i]) assert np.array_equal(n1.spike, expected_spike[i]) assert np.array_equal(n1.voltage, expected_vol[i]) @@ -473,7 +503,7 @@ def test_LIF_both_leak_bias(self): def test_TonicSpiking(self): n1 = pb.TonicSpiking(1, fire_step=3) - inp_data = np.array([1, 1, 1, 1, 0, 1, 0, 1, 0, 1], dtype=np.bool_) + incoming_v = np.array([1, 1, 1, 1, 0, 1, 0, 1, 0, 1], dtype=np.bool_) expected_spike = np.array( [[0], [0], [1], [0], [0], [0], [0], [1], [0], [0]], dtype=np.bool_ ) @@ -481,9 +511,9 @@ def test_TonicSpiking(self): [[1], [2], [0], [1], [1], [2], [2], [0], [0], [1]], dtype=np.int32 ) - for i in range(inp_data.size): + for i in range(incoming_v.size): pb.FRONTEND_ENV["t"] += 1 - n1.update(inp_data[i]) + n1.update(incoming_v[i]) assert np.array_equal(n1.spike, expected_spike[i]) assert np.array_equal(n1.voltage, expected_vol[i]) @@ -491,7 +521,7 @@ def test_TonicSpiking(self): def test_PhasicSpiking(self): n1 = pb.PhasicSpiking(1, fire_step=3, neg_floor=-2) - inp_data = np.array([1, 1, 1, 1, 0, 1, 0, 1, 0, 1], dtype=np.bool_) + incoming_v = np.array([1, 1, 1, 1, 0, 1, 0, 1, 0, 1], dtype=np.bool_) expected_spike = np.array( [[0], [0], [1], [0], [0], [0], [0], [0], [0], [0]], dtype=np.bool_ ) @@ -499,9 +529,9 @@ def test_PhasicSpiking(self): [[2], [4], [-3], [-2], [-2], [-2], [-2], [-2], [-2], [-2]], dtype=np.int32 ) - for i in range(inp_data.size): + for i in range(incoming_v.size): pb.FRONTEND_ENV["t"] += 1 - n1.update(inp_data[i]) + n1.update(incoming_v[i]) assert np.array_equal(n1.spike, expected_spike[i]) assert np.array_equal(n1.voltage, expected_vol[i]) @@ -509,13 +539,13 @@ def test_PhasicSpiking(self): def test_SpikingRelu(self): n1 = pb.SpikingRelu(1) - inp_data = np.random.randint(0, 2, size=(20, 1), dtype=np.bool_) + incoming_v = np.random.randint(0, 2, size=(20, 1), dtype=np.bool_) - for i in range(inp_data.size): + for i in range(incoming_v.size): pb.FRONTEND_ENV["t"] += 1 - n1.update(inp_data[i]) + n1.update(incoming_v[i]) - assert np.array_equal(n1.spike, inp_data[i]) + assert np.array_equal(n1.spike, incoming_v[i]) def test_sum_inputs_behavior(self, build_Net2): net = build_Net2 @@ -584,11 +614,98 @@ def test_AvgPool_Neuron(self, n_window): n1 = Neuron(shape=(1,), leak_v=1 - typical_round(n_window / 2), neg_threshold=0) # Generate upper triangular matrix where the number of 1's increases in sequence. - inp_data = np.tril(np.ones((1 + n_window, n_window), dtype=np.bool_)) + incoming_v = np.tril(np.ones((1 + n_window, n_window), dtype=np.bool_)) for i in range(1 + n_window): pb.FRONTEND_ENV["t"] += 1 - n1.update(np.sum(inp_data[i])) + n1.update(np.sum(incoming_v[i])) expected = (i + 1) >= typical_round(n_window / 2) assert np.array_equal(n1.spike[0], expected) + + +class TestNeuronAllModes: + """Test neuron with specified 'spike width' & 'snn_en'. + + NOTE: '001' is SNN mode which is tested in the previous cases. + """ + + @staticmethod + def _ann_vjt_func(vj: VoltageType, neuron: Neuron) -> NDArray[NEUOUT_U8_DTYPE]: + def _bit_tuncate(bit_tunc: int, vj: VoltageType): + if bit_tunc == 0: + return np.zeros_like(vj) + elif vj >> bit_tunc > 0: # Saturate truncation + return np.full_like(vj, 255) + elif bit_tunc < 8: + return (vj << (8 - bit_tunc)) & 255 + else: + return (vj >> (bit_tunc - 8)) & 255 + + return np.where( + vj >= neuron.pos_threshold, + _bit_tuncate(neuron.bit_truncation, vj), + neuron._vjt0, + ).astype(NEUOUT_U8_DTYPE) + + @pytest.mark.parametrize("reg_kwds", [_reg010_kwds, _reg110_kwds]) + def test_IF_ss10(self, reg_kwds): + n1 = pb.IF(1, 0, 0, bit_truncation=8, **reg_kwds) + + incoming_v = np.random.randint( + np.iinfo(np.int16).min, np.iinfo(np.int16).max, size=(8,), dtype=np.int32 + ) + + for i in range(incoming_v.size): + pb.FRONTEND_ENV["t"] += 1 + n1.update(incoming_v[i]) + v_bt = self._ann_vjt_func( + np.asarray(incoming_v[i], dtype=np.int32), + n1, + ) + + assert np.array_equal(n1.spike, v_bt) + + def test_LIF_ss11(self): + pos_thres = 8000 + n1 = pb.LIF(1, pos_thres, bit_truncation=12, **_reg011_kwds) + + incoming_v = np.random.randint(-10000, 10000, size=(20,), dtype=np.int32) + pre_vjt = 0 + + for i in range(incoming_v.size): + pb.FRONTEND_ENV["t"] += 1 + n1.update(incoming_v[i]) + + pre_vjt += incoming_v[i] + spike = pre_vjt >= pos_thres + + v_bt = self._ann_vjt_func( + np.asarray(pre_vjt, dtype=np.int32), + n1, + ) + + if spike: + pre_vjt -= pos_thres + + assert np.array_equal(n1.spike, v_bt) + + @pytest.mark.parametrize("reg_kwds", [_reg000_kwds, _reg100_kwds]) + def test_LIF_ss00(self, reg_kwds): + pos_thres = 8000 + n1 = pb.LIF(1, pos_thres, reset_v=2000, bit_truncation=10, **reg_kwds) + + incoming_v = np.random.randint(-10000, 10000, size=(20,), dtype=np.int32) + pre_vjt = 0 + + for i in range(incoming_v.size): + pb.FRONTEND_ENV["t"] += 1 + n1.update(incoming_v[i]) + + pre_vjt = incoming_v[i] + spike = pre_vjt >= pos_thres + + if spike: + pre_vjt = 2000 + + assert np.array_equal(n1.spike[0], spike) From ec5f1aee12974df4d1ace3a69805d1d4758966d4 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 5 Jul 2024 14:32:56 +0800 Subject: [PATCH 004/187] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20the=20d?= =?UTF-8?q?type=20of=20output=20of=20neuron.=20Use=20NeuOutType=20instead?= =?UTF-8?q?=20of=20SpikeType=20&=20u8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/components/neuron/base.py | 29 +++--------- paibox/components/synapses/base.py | 19 ++++---- paibox/components/synapses/conv_utils.py | 41 +++++++++------- paibox/components/synapses/transforms.py | 60 +++++++++++++----------- paibox/mixin.py | 4 +- paibox/types.py | 3 +- 6 files changed, 75 insertions(+), 81 deletions(-) diff --git a/paibox/components/neuron/base.py b/paibox/components/neuron/base.py index 51899572..3fa0576f 100644 --- a/paibox/components/neuron/base.py +++ b/paibox/components/neuron/base.py @@ -1,15 +1,8 @@ -import sys import warnings from collections.abc import Iterable from typing import Any, Literal, NoReturn, Optional, Union -if sys.version_info >= (3, 10): - from typing import TypeAlias -else: - from typing_extensions import TypeAlias - import numpy as np -from numpy.typing import NDArray from paicorelib import ( LCM, LDM, @@ -30,12 +23,10 @@ from paibox.exceptions import PAIBoxWarning, ShapeError from paibox.types import ( NEUOUT_U8_DTYPE, - SPIKE_DTYPE, VOLTAGE_DTYPE, LeakVType, NeuOutType, Shape, - SpikeType, VoltageType, ) from paibox.utils import ( @@ -54,13 +45,11 @@ vjt_overflow, _input_width_format, _spike_width_format, - _get_neu_out_dtype, ) __all__ = ["Neuron"] L = Literal -NeuOutTruncType: TypeAlias = NDArray[NEUOUT_U8_DTYPE] class MetaNeuron: @@ -218,7 +207,7 @@ def _neuronal_leak(self, vjt: VoltageType) -> VoltageType: return vjt_overflow(v_leaked, self.overflow_strict) - def _neuronal_fire(self, vjt: VoltageType) -> SpikeType: + def _neuronal_fire(self, vjt: VoltageType) -> NeuOutType: r"""3. Threshold comparison. 3.1 Random threshold. @@ -244,7 +233,7 @@ def _neuronal_fire(self, vjt: VoltageType) -> SpikeType: ) spike = self.thres_mode == TM.EXCEED_POSITIVE - return spike + return spike.astype(NEUOUT_U8_DTYPE) def _neuronal_reset(self, vjt: VoltageType) -> VoltageType: r"""4. Reset. @@ -300,7 +289,7 @@ def _when_exceed_neg() -> VoltageType: return v_reset.astype(VOLTAGE_DTYPE) - def _bit_truncate(self, vj: VoltageType) -> NeuOutTruncType: + def _bit_truncate(self, vj: VoltageType) -> NeuOutType: r"""Bit Truncation. If spiking width format is `WIDTH_1BIT`, then @@ -355,7 +344,7 @@ def _aux_post_hook(self) -> None: def update( self, incoming_v: VoltageType, vjt_pre: VoltageType - ) -> tuple[Union[SpikeType, NeuOutTruncType], VoltageType]: + ) -> tuple[NeuOutType, VoltageType]: """Update at one timestep.""" self._aux_pre_hook() @@ -389,18 +378,13 @@ def update( def init_param(self, param: Any) -> np.ndarray: return np.full((self._n_neuron,), param) - @property - def _neu_out_dtype(self) -> type[Union[SPIKE_DTYPE, NEUOUT_U8_DTYPE]]: - """dtype of output of neuron.""" - return _get_neu_out_dtype(self.spike_width) - @property def _vjt0(self) -> VoltageType: return self.init_param(0).astype(VOLTAGE_DTYPE) @property def _neu_out0(self) -> NeuOutType: - return self.init_param(0).astype(self._neu_out_dtype) + return self.init_param(0).astype(NEUOUT_U8_DTYPE) @property def varshape(self) -> tuple[int, ...]: @@ -480,8 +464,7 @@ def __init__( self.set_memory( "delay_registers", np.zeros( - (HwConfig.N_TIMESLOT_MAX,) + self._neu_out.shape, - dtype=self._neu_out.dtype, + (HwConfig.N_TIMESLOT_MAX,) + self._neu_out.shape, dtype=NEUOUT_U8_DTYPE ), ) diff --git a/paibox/components/synapses/base.py b/paibox/components/synapses/base.py index e9e404a3..4163cdca 100644 --- a/paibox/components/synapses/base.py +++ b/paibox/components/synapses/base.py @@ -6,7 +6,7 @@ from paibox.base import NeuDyn, SynSys from paibox.exceptions import RegisterError, ShapeError -from paibox.types import DataArrayType, SynOutType, WeightType +from paibox.types import DataArrayType, NeuOutType, SynOutType, WeightType from ..modules import BuildingModule from ..neuron import Neuron @@ -50,7 +50,6 @@ def __init__( name: Optional[str] = None, ) -> None: super().__init__(name) - self._source = source self._target = target @@ -66,19 +65,21 @@ def __init__( def __call__(self, *args, **kwargs) -> SynOutType: return self.update(*args, **kwargs) - def update(self, spike: Optional[np.ndarray] = None, *args, **kwargs) -> SynOutType: - # Retrieve the spike at index `timestamp` of the dest neurons + def update(self, x: Optional[NeuOutType] = None, *args, **kwargs) -> SynOutType: + # Retrieve the output at [timestamp] of the dest neurons if self.dest.is_working(): if isinstance(self.source, InputProj): - synin = self.source.output.copy() if spike is None else spike + synin = self.source.output if x is None else np.atleast_1d(x) else: idx = self.dest.timestamp % HwConfig.N_TIMESLOT_MAX - synin = self.source.output[idx].copy() if spike is None else spike + synin = ( + self.source.delay_registers[idx] if x is None else np.atleast_1d(x) + ) else: # Retrieve 0 to the dest neurons if it is not working - synin = np.zeros_like(self.source.spike) + synin = np.zeros_like(self.source.output) - self._synout = self.comm(synin).ravel().astype(np.int32) + self._synout = self.comm(synin).ravel() return self._synout def reset_state(self, *args, **kwargs) -> None: @@ -88,7 +89,7 @@ def reset_state(self, *args, **kwargs) -> None: def __copy__(self) -> "FullConnSyn": return self.__deepcopy__() - def __deepcopy__(self, memo=None, _nil=[]) -> "FullConnSyn": + def __deepcopy__(self) -> "FullConnSyn": self._n_copied += 1 return FullConnSyn( diff --git a/paibox/components/synapses/conv_utils.py b/paibox/components/synapses/conv_utils.py index 05d04b2e..a7235084 100644 --- a/paibox/components/synapses/conv_utils.py +++ b/paibox/components/synapses/conv_utils.py @@ -1,13 +1,18 @@ from collections.abc import Iterable from functools import partial from itertools import repeat -from typing import Any import numpy as np from numpy.typing import NDArray from paibox.exceptions import ShapeError -from paibox.types import SpikeType, SynOutType, WeightType +from paibox.types import ( + NEUOUT_U8_DTYPE, + VOLTAGE_DTYPE, + NeuOutType, + SynOutType, + WeightType, +) from .conv_types import Size1Type, Size2Type, Size3Type, SizeAnyType, _Order2d, _Order3d @@ -232,14 +237,14 @@ def _pool2d_kernel_unroll( def _func_pool2d( - x_chw: SpikeType, + x_chw: NeuOutType, out_shape: Size2Type, ksize: Size2Type, stride: Size2Type, padding: Size2Type, type: str, threshold: int, -) -> SpikeType: +) -> NeuOutType: xcin, xh, xw = x_chw.shape kh, kw = ksize oh, ow = out_shape @@ -276,13 +281,15 @@ def _func_pool2d( ) if type == "avg": - return out >= threshold + result = out >= threshold else: - return out.astype(np.bool_) + result = out + + return result.astype(NEUOUT_U8_DTYPE) def _conv1d_faster( - x_cl: NDArray[Any], + x_cl: NeuOutType, out_shape: Size1Type, kernel: WeightType, stride: Size1Type, @@ -309,11 +316,11 @@ def _conv1d_faster( out = col_fm @ col_kernel.T # + self.bias # (ol, cout) -> (cout, ol) - return out.astype(np.int32).T + return out.T.astype(VOLTAGE_DTYPE) def _conv2d_faster( - x_chw: NDArray[Any], + x_chw: NeuOutType, out_shape: Size2Type, kernel: WeightType, stride: Size2Type, @@ -343,9 +350,9 @@ def _conv2d_faster( # (oh*ow, cin*kh*kw) * (cout, cin*kh*kw)^T = (oh*ow, cout) out = col_fm @ col_kernel.T # + self.bias # (oh*ow, cout) -> (cout, oh*ow) -> (cout, oh, ow) - out = out.astype(np.int32).T.reshape((cout,) + out_shape) + out = out.T.reshape((cout,) + out_shape) - return out + return out.astype(VOLTAGE_DTYPE) def _convtranspose1d_unroll( @@ -516,7 +523,7 @@ def _convtranspose2d_unroll( def _convtranspose1d_faster( - x_cl: NDArray[Any], + x_cl: NeuOutType, out_shape: Size1Type, kernel: WeightType, stride: Size1Type, @@ -565,11 +572,11 @@ def _convtranspose1d_faster( # output_padding out = np.pad(out, ((0, 0), (0, output_padding[0])), mode="constant") - return out.astype(np.int32) + return out.astype(VOLTAGE_DTYPE) def _convtranspose2d_faster( - x_chw: NDArray[Any], + x_chw: NeuOutType, out_shape: Size2Type, kernel: WeightType, stride: Size2Type, @@ -615,7 +622,7 @@ def _convtranspose2d_faster( # (oh*ow, cin*kh*kw) * (cin*kh*kw, cout) = (oh*ow, cout) out_col = col_fm @ kernel_col.T # (oh*ow, cout) -> (oh, ow, cout) -> (cout, oh, ow) - out = out_col.astype(np.int32).T.reshape((cout,) + (noh, now)) + out = out_col.astype(VOLTAGE_DTYPE).T.reshape((cout,) + (noh, now)) # padding & output_padding # inverse padding @@ -633,7 +640,7 @@ def _convtranspose2d_faster( def _1d_im2col( - x_padded: NDArray[Any], ol: int, kl: int, stride: Size1Type + x_padded: NeuOutType, ol: int, kl: int, stride: Size1Type ) -> NDArray[np.int64]: cols = np.zeros((ol, x_padded.shape[0] * kl), dtype=np.int64) @@ -648,7 +655,7 @@ def _1d_im2col( def _2d_im2col( - x_padded: NDArray[Any], oh: int, ow: int, kh: int, kw: int, stride: Size2Type + x_padded: NeuOutType, oh: int, ow: int, kh: int, kw: int, stride: Size2Type ) -> NDArray[np.int64]: cols = np.zeros((oh * ow, x_padded.shape[0] * kh * kw), dtype=np.int64) diff --git a/paibox/components/synapses/transforms.py b/paibox/components/synapses/transforms.py index 845d6025..64a6b8ce 100644 --- a/paibox/components/synapses/transforms.py +++ b/paibox/components/synapses/transforms.py @@ -6,7 +6,14 @@ from paicorelib import WeightPrecision as WP from paibox.exceptions import AutoOptimizationWarning, ShapeError -from paibox.types import DataArrayType, IntScalarType, SpikeType, SynOutType, WeightType +from paibox.types import ( + DataArrayType, + IntScalarType, + NeuOutType, + SynOutType, + WeightType, + VOLTAGE_DTYPE, +) from paibox.utils import is_shape, shape2num, typical_round from .conv_types import Size1Type, Size2Type, SizeAnyType @@ -92,14 +99,6 @@ def _set_coarse_dtype(raw_w: DataArrayType) -> WeightType: raise ValueError(f"weight out of range int8, got [{_min}, {_max}].") if _array.dtype > np.int8: - # XXX If it is automatically optimized to int8, it cannot be converted using the 'same_kind' rule. - # if _max <= MAX_INT1 and _min >= MIN_INT1: - # warnings.warn( - # f"dtype of weight is optimized automatically, {_array.dtype} -> bool.", - # AutoOptimizationWarning, - # ) - # _dtype = np.bool_ - # else: warnings.warn( f"dtype of weight is optimized automatically, {_array.dtype} -> int8.", AutoOptimizationWarning, @@ -139,13 +138,15 @@ def _get_weight_precision(weight: WeightType, enable_wp_opt: bool) -> WP: class Transform: def __init__(self, weights: DataArrayType) -> None: self.weights = _set_coarse_dtype(weights) - """The actual weights in synapses. Stored in `np.bool_` or `np.int8` format.""" + self.weights.setflags(write=False) def __call__(self, *args, **kwargs) -> SynOutType: - """Ensure that in all subclasses, the output dimensions are (M,).""" - raise NotImplementedError + # Ensure that in all subclasses, the output dimensions are (M,). + raise NotImplementedError( + "function '__call__' must be implemented in the subclasses." + ) def _get_wp(self, enable_wp_opt: bool) -> WP: return _get_weight_precision(self.weights, enable_wp_opt) @@ -153,7 +154,9 @@ def _get_wp(self, enable_wp_opt: bool) -> WP: @property def connectivity(self) -> WeightType: """The connectivity matrix in `np.ndarray` format.""" - raise NotImplementedError + raise NotImplementedError( + "property 'connectivity' must be implemented in the subclasses." + ) class OneToOne(Transform): @@ -185,9 +188,9 @@ def __init__(self, num: int, weights: DataArrayType) -> None: f"the ndim of weights must be 0 or 1, but got {self.weights.ndim}." ) - def __call__(self, x: SpikeType, *args, **kwargs) -> SynOutType: + def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: # (N,) * (N,) -> (N,) - return x * self.weights.astype(np.int32) + return x * self.weights.astype(VOLTAGE_DTYPE) @property def connectivity(self): @@ -233,20 +236,21 @@ def __init__(self, conn_size: Size2Type, weights: DataArrayType) -> None: f"the ndim of weights must be 0 or 2, but got {self.weights.ndim}." ) - def __call__(self, x: SpikeType, *args, **kwargs) -> SynOutType: + def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: """ NOTE: - - When weights is a scalar, the output is a scalar (sum * w) & repeated \ - `conn_size[1]` times. + - When weights is a scalar, the output is a scalar (sum * w) & repeated `conn_size[1]` times. - When weights is a matrix, the output is the dot product of `x` & weights. """ if self.weights.ndim == 0: - sum_x = np.sum(x, axis=None, dtype=np.int32) + sum_x = np.sum(x, dtype=VOLTAGE_DTYPE) # (M,) - output = np.full((self.conn_size[1],), self.weights * sum_x, dtype=np.int32) + output = np.full( + (self.conn_size[1],), self.weights * sum_x, dtype=VOLTAGE_DTYPE + ) else: # (N,) @ (N, M) -> (M,) - output = x @ self.weights.astype(np.int32) + output = x @ self.weights.astype(VOLTAGE_DTYPE) return output @@ -284,11 +288,11 @@ def __init__( super().__init__(weights) - def __call__(self, x: SpikeType, *args, **kwargs) -> SynOutType: + def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: # (n?, k) @ (k, m?) -> (n?, m?) _x = x.reshape(self.in_shape).transpose(self.axes) - return _x @ self.weights.astype(np.int32) + return _x @ self.weights.astype(VOLTAGE_DTYPE) @staticmethod def _matmul_unroll( @@ -343,7 +347,7 @@ def __init__( super().__init__(kernel) - def __call__(self, x: SpikeType, *args, **kwargs) -> SynOutType: + def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: cin = self.weights.shape[1] # if self.fm_order == "LC": @@ -381,7 +385,7 @@ def __init__( super().__init__(kernel) - def __call__(self, x: SpikeType, *args, **kwargs) -> SynOutType: + def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: cin = self.weights.shape[1] # if self.fm_order == "HWC": @@ -421,7 +425,7 @@ def __init__( super().__init__(kernel) - def __call__(self, x: np.ndarray, *args, **kwargs) -> SynOutType: + def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: cin = self.weights.shape[1] # if self.fm_order == "LC": @@ -471,7 +475,7 @@ def __init__( super().__init__(kernel) - def __call__(self, x: np.ndarray, *args, **kwargs) -> SynOutType: + def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: cin = self.weights.shape[1] # if self.fm_order == "HWC": @@ -529,7 +533,7 @@ def __init__( super().__init__(1) - def __call__(self, x: SpikeType, *args, **kwargs) -> SpikeType: + def __call__(self, x: NeuOutType, *args, **kwargs) -> NeuOutType: # if self.fm_order == "HWC": # # (N,) -> (H, W, C) -> (C, H, W) # _x = x.reshape(self.in_shape + (self.channels,)).transpose(2, 0, 1) diff --git a/paibox/mixin.py b/paibox/mixin.py index d96f921e..f16af011 100644 --- a/paibox/mixin.py +++ b/paibox/mixin.py @@ -144,13 +144,13 @@ def register_master( def unregister_master(self, key: str) -> Optional["FullConnectedSyn"]: return self.master_nodes.pop(key, None) - def get_master_node(self, key: str) -> Optional[Any]: + def get_master_node(self, key: str) -> Optional["FullConnectedSyn"]: return self.master_nodes.get(key, None) def sum_inputs(self, *args, **kwargs) -> VoltageType: output = 0 for node in self.master_nodes.values(): - output += node.output.copy() + output = output + node.output.copy() # do not use += return np.asarray(output, dtype=VOLTAGE_DTYPE) diff --git a/paibox/types.py b/paibox/types.py index 82020805..4392684e 100644 --- a/paibox/types.py +++ b/paibox/types.py @@ -23,11 +23,10 @@ VOLTAGE_DTYPE = np.int32 NEUOUT_SPIKE_DTYPE = np.bool_ NEUOUT_U8_DTYPE = np.uint8 -NEUOUT_DTYPE = Union[NEUOUT_SPIKE_DTYPE, NEUOUT_U8_DTYPE] LeakVType: TypeAlias = NDArray[LEAK_V_DTYPE] SpikeType: TypeAlias = NDArray[SPIKE_DTYPE] SynOutType: TypeAlias = NDArray[VOLTAGE_DTYPE] VoltageType: TypeAlias = NDArray[VOLTAGE_DTYPE] -NeuOutType: TypeAlias = NDArray[NEUOUT_DTYPE] +NeuOutType: TypeAlias = NDArray[NEUOUT_U8_DTYPE] WeightType: TypeAlias = NDArray[Union[np.bool_, np.int8]] From 1c721d67620f8c4b20fa51bfccd1fb9bcc2c0cdd Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 5 Jul 2024 14:33:38 +0800 Subject: [PATCH 005/187] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20the=20u?= =?UTF-8?q?pdate=20types=20of=20projection=20&=20simplify=20output=20handl?= =?UTF-8?q?ing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/components/projection.py | 90 +++++++++++++++------------------ 1 file changed, 41 insertions(+), 49 deletions(-) diff --git a/paibox/components/projection.py b/paibox/components/projection.py index 73e25ad7..0872e501 100644 --- a/paibox/components/projection.py +++ b/paibox/components/projection.py @@ -1,7 +1,7 @@ import inspect import sys from collections.abc import Callable -from typing import Optional, Union +from typing import Literal, Optional, Union import numpy as np @@ -14,11 +14,12 @@ from paibox.context import _FRONTEND_CONTEXT from paibox.exceptions import ShapeError, SimulationError from paibox.mixin import TimeRelatedNode -from paibox.types import DataType, Shape, SpikeType +from paibox.types import NEUOUT_U8_DTYPE, DataType, NeuOutType, Shape from paibox.utils import as_shape, shape2num __all__ = ["InputProj"] +L = Literal P = ParamSpec("P") @@ -26,12 +27,27 @@ def _func_bypass(x: DataType) -> DataType: return x -class Projection(DynamicSys): - def __call__(self, *args, **kwargs) -> SpikeType: +class Projection(DynamicSys, TimeRelatedNode): + def __call__(self, *args, **kwargs) -> NeuOutType: return self.update(*args, **kwargs) + @property + def delay_relative(self) -> int: + return 1 # Fixed + + @property + def tick_wait_start(self) -> int: + return 1 # Fixed + + @property + def tick_wait_end(self) -> int: + return 0 # Fixed + + +class InputProj(Projection): + # TODO Since the input port can be equivalent to the output of a neuron, is it more appropriate + # to use a neuron as an input port? -class InputProj(Projection, TimeRelatedNode): def __init__( self, input: Optional[Union[DataType, Callable[P, DataType]]], @@ -43,9 +59,8 @@ def __init__( """The input node of network. Arguments: - - input: the input value of the projection node. It can be numeric value or callable\ - function(function or `Encoder`). - - shape_out: the shape of the output. + - input: the input value of the projection node. It can be a numeric value or a callable function. + - shape_out: the shape of the output.. - keep_shape: wether to keep the shape when retieving the feature map. - name: the name of the node. Optional. """ @@ -59,42 +74,31 @@ def __init__( self._func_input = input else: # Numeric input self._num_input = input - self._func_input = _func_bypass + self._func_input = None self._shape = as_shape(shape_out) self.keep_shape = keep_shape + self.set_memory("_neu_out", np.zeros((self.num_out,), dtype=NEUOUT_U8_DTYPE)) - self.set_memory("_inner_spike", np.zeros((self.num_out,), dtype=np.bool_)) + def update(self, *args, **kwargs) -> NeuOutType: + _input = self._get_neumeric_input(**kwargs) - def update(self, **kwargs) -> SpikeType: - _spike = self._get_neumeric_input(**kwargs) - - if isinstance(_spike, (int, np.bool_, np.integer)): - # XXX In order to simplify the situation where one neuron is connected to - # multiple axons in the simulation (the actual input node output size is 8), - # one input node is temporarily allowed to output 8 bits of data. - if isinstance(_spike, (np.bool_, np.integer)): - _dtype = _spike.dtype - else: - _dtype = np.int8 - - self._inner_spike = np.full((self.num_out,), _spike, dtype=_dtype) - - elif isinstance(_spike, np.ndarray): - if shape2num(_spike.shape) != self.num_out: + if isinstance(_input, (int, np.bool_, np.integer)): + self._neu_out = np.full_like(self._neu_out, _input, dtype=NEUOUT_U8_DTYPE) + elif isinstance(_input, np.ndarray): + if _input.size != self._neu_out.size: raise ShapeError( - f"cannot reshape output value from {_spike.shape} to ({self.num_out},)." + f"cannot reshape output value from {_input.shape} to {self._neu_out.shape}." ) - self._inner_spike = _spike.ravel() - + self._neu_out = _input.ravel().astype(NEUOUT_U8_DTYPE) else: # should never be reached raise TypeError( f"expected type int, np.bool_, np.integer or np.ndarray, " - f"but got {_spike}, type {type(_spike)}." + f"but got {_input}, type {type(_input)}." ) - return self._inner_spike + return self._neu_out def reset_state(self) -> None: self.reset_memory() # Call reset of `StatusMemory`. @@ -149,28 +153,16 @@ def input(self, value: DataType) -> None: self._num_input = value @property - def output(self) -> SpikeType: - return self._inner_spike - - @property - def spike(self) -> SpikeType: - return self._inner_spike + def output(self) -> NeuOutType: + return self._neu_out @property - def feature_map(self) -> SpikeType: - return self.output.reshape(self.varshape) + def spike(self) -> NeuOutType: + return self._neu_out @property - def delay_relative(self) -> int: - return 1 # Fixed - - @property - def tick_wait_start(self) -> int: - return 1 # Fixed - - @property - def tick_wait_end(self) -> int: - return 0 # Fixed + def feature_map(self) -> NeuOutType: + return self._neu_out.reshape(self.varshape) def _call_with_ctx(f: Callable[..., DataType], *args, **kwargs) -> DataType: From f604bf3c372693ac42f1955ae63b0b8d83266220 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 5 Jul 2024 14:34:21 +0800 Subject: [PATCH 006/187] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20the=20u?= =?UTF-8?q?pdate=20types=20of=20fmodules=20&=20simplify=20output=20handlin?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/components/functional.py | 80 ++++++++++++++++++------------- paibox/components/modules.py | 84 +++++++++++++++++++++++---------- 2 files changed, 104 insertions(+), 60 deletions(-) diff --git a/paibox/components/functional.py b/paibox/components/functional.py index 31df789c..764ce2c5 100644 --- a/paibox/components/functional.py +++ b/paibox/components/functional.py @@ -4,13 +4,20 @@ from typing import Literal, Optional, Union import numpy as np -from numpy.typing import NDArray from paicorelib import NTM, RM, TM from paibox.base import NeuDyn, NodeList from paibox.exceptions import PAIBoxDeprecationWarning, ShapeError from paibox.network import DynSysGroup -from paibox.types import IntScalarType, SpikeType, VoltageType +from paibox.types import ( + NEUOUT_U8_DTYPE, + VOLTAGE_DTYPE, + IntScalarType, + NeuOutType, + SpikeType, + VoltageType, + WeightType, +) from paibox.utils import ( arg_check_non_neg, arg_check_pos, @@ -26,6 +33,7 @@ FunctionalModule2to1WithV, FunctionalModuleWithV, TransposeModule, + set_rt_mode, ) from .neuron import Neuron from .neuron.neurons import * @@ -57,6 +65,7 @@ ] +@set_rt_mode(1, 1, 1) class BitwiseAND(FunctionalModule2to1): inherent_delay = 0 @@ -89,7 +98,7 @@ def __init__( """ super().__init__(neuron_a, neuron_b, keep_shape=keep_shape, name=name, **kwargs) - def spike_func(self, x1: SpikeType, x2: SpikeType, **kwargs) -> SpikeType: + def spike_func(self, x1: NeuOutType, x2: NeuOutType, **kwargs) -> NeuOutType: return x1 & x2 def build(self, network: DynSysGroup, **build_options) -> BuiltComponentType: @@ -126,6 +135,7 @@ def build(self, network: DynSysGroup, **build_options) -> BuiltComponentType: return generated +@set_rt_mode(1, 1, 1) class BitwiseNOT(FunctionalModule): inherent_delay = 0 @@ -157,8 +167,8 @@ def __init__( **kwargs, ) - def spike_func(self, x1: SpikeType, **kwargs) -> SpikeType: - return ~x1 + def spike_func(self, x1: NeuOutType, **kwargs) -> NeuOutType: + return x1 == 0 # x1 is an array in uint8 def build(self, network: DynSysGroup, **build_options) -> BuiltComponentType: n1_not = LIF( @@ -187,6 +197,7 @@ def build(self, network: DynSysGroup, **build_options) -> BuiltComponentType: return generated +@set_rt_mode(1, 1, 1) class BitwiseOR(FunctionalModule2to1): inherent_delay = 0 @@ -209,7 +220,7 @@ def __init__( """ super().__init__(neuron_a, neuron_b, keep_shape=keep_shape, name=name, **kwargs) - def spike_func(self, x1: SpikeType, x2: SpikeType, **kwargs) -> SpikeType: + def spike_func(self, x1: NeuOutType, x2: NeuOutType, **kwargs) -> NeuOutType: return x1 | x2 def build(self, network: DynSysGroup, **build_options) -> BuiltComponentType: @@ -243,6 +254,7 @@ def build(self, network: DynSysGroup, **build_options) -> BuiltComponentType: return generated +@set_rt_mode(1, 1, 1) class BitwiseXOR(FunctionalModule2to1): inherent_delay = 1 @@ -266,7 +278,7 @@ def __init__( """ super().__init__(neuron_a, neuron_b, keep_shape=keep_shape, name=name, **kwargs) - def spike_func(self, x1: SpikeType, x2: SpikeType, **kwargs) -> SpikeType: + def spike_func(self, x1: NeuOutType, x2: NeuOutType, **kwargs) -> NeuOutType: return x1 ^ x2 def build(self, network: DynSysGroup, **build_options) -> BuiltComponentType: @@ -364,7 +376,7 @@ def __init__( **kwargs, ) - def spike_func(self, x1: SpikeType, **kwargs) -> SpikeType: + def spike_func(self, x1: NeuOutType, **kwargs) -> NeuOutType: return x1 def build(self, network: DynSysGroup, **build_options) -> BuiltComponentType: @@ -417,6 +429,7 @@ def build(self, network: DynSysGroup, **build_options) -> BuiltComponentType: return generated +@set_rt_mode(1, 1, 1) class SpikingAdd(FunctionalModule2to1WithV): inherent_delay = 0 @@ -457,12 +470,12 @@ def __init__( super().__init__(neuron_a, neuron_b, keep_shape=keep_shape, name=name, **kwargs) - def spike_func(self, vjt: VoltageType, **kwargs) -> tuple[SpikeType, VoltageType]: + def spike_func(self, vjt: VoltageType, **kwargs) -> tuple[NeuOutType, VoltageType]: """Simplified neuron computing mechanism as the operator function.""" return _spike_func_sadd_ssub(vjt, self.pos_threshold, self.reset_v) def synaptic_integr( - self, x1: SpikeType, x2: SpikeType, vjt_pre: VoltageType + self, x1: NeuOutType, x2: NeuOutType, vjt_pre: VoltageType ) -> VoltageType: return _sum_inputs_sadd_ssub( x1, x2, self.factor_a, self.factor_b, vjt_pre, strict=self.overflow_strict @@ -501,6 +514,7 @@ def build(self, network: DynSysGroup, **build_options) -> BuiltComponentType: return generated +@set_rt_mode(1, 1, 1) class _SpikingPool2dWithV(FunctionalModuleWithV): inherent_delay = 0 @@ -547,11 +561,11 @@ def __init__( **kwargs, ) - def spike_func(self, vjt: VoltageType, **kwargs) -> tuple[SpikeType, VoltageType]: + def spike_func(self, vjt: VoltageType, **kwargs) -> tuple[NeuOutType, VoltageType]: return _spike_func_avg_pool(vjt, self.pos_thres) - def synaptic_integr(self, x1: SpikeType, vjt_pre: VoltageType) -> VoltageType: - return vjt_overflow((vjt_pre + self.tfm(x1).ravel()).astype(np.int32)) + def synaptic_integr(self, x1: NeuOutType, vjt_pre: VoltageType) -> VoltageType: + return vjt_overflow(vjt_pre + self.tfm(x1).ravel()) def build(self, network: DynSysGroup, **build_options) -> BuiltComponentType: n1_ap2d = IF( @@ -579,6 +593,7 @@ def build(self, network: DynSysGroup, **build_options) -> BuiltComponentType: return generated +@set_rt_mode(1, 1, 1) class _SpikingPool2d(FunctionalModule): inherent_delay = 0 @@ -629,7 +644,7 @@ def __init__( **kwargs, ) - def spike_func(self, x1: SpikeType, **kwargs) -> SpikeType: + def spike_func(self, x1: NeuOutType, **kwargs) -> NeuOutType: return self.tfm(x1) def build(self, network: DynSysGroup, **build_options) -> BuiltComponentType: @@ -664,12 +679,6 @@ def build(self, network: DynSysGroup, **build_options) -> BuiltComponentType: generated = [n1_p2d, syn1] self._rebuild_out_intf(network, n1_p2d, *generated, **build_options) - # for syns in self.module_intf.output: - # syns.source = n1_p2d - - # network._add_components(*generated) - # network._remove_components(self) - return generated @@ -774,6 +783,7 @@ def __init__( ) +@set_rt_mode(1, 1, 1) class SpikingSub(FunctionalModule2to1WithV): inherent_delay = 0 factor_a: int = 1 @@ -803,12 +813,12 @@ def __init__( self.overflow_strict = overflow_strict super().__init__(neuron_a, neuron_b, keep_shape=keep_shape, name=name, **kwargs) - def spike_func(self, vjt: VoltageType, **kwargs) -> tuple[SpikeType, VoltageType]: + def spike_func(self, vjt: VoltageType, **kwargs) -> tuple[NeuOutType, VoltageType]: """Simplified neuron computing mechanism to generate output spike.""" return _spike_func_sadd_ssub(vjt, self.pos_threshold) def synaptic_integr( - self, x1: SpikeType, x2: SpikeType, vjt_pre: VoltageType + self, x1: NeuOutType, x2: NeuOutType, vjt_pre: VoltageType ) -> VoltageType: return _sum_inputs_sadd_ssub( x1, x2, self.factor_a, self.factor_b, vjt_pre, strict=self.overflow_strict @@ -852,6 +862,7 @@ def build(self, network: DynSysGroup, **build_options) -> BuiltComponentType: "'Transpose2d' will be removed in a future version. Use 'MatMul2d' instead.", category=PAIBoxDeprecationWarning, ) +@set_rt_mode(1, 1, 1) class Transpose2d(TransposeModule): def __init__( self, @@ -877,7 +888,7 @@ def __init__( **kwargs, ) - def spike_func(self, x1: SpikeType, **kwargs) -> SpikeType: + def spike_func(self, x1: NeuOutType, **kwargs) -> NeuOutType: _x1 = x1.reshape(self.shape_in) return _x1.T @@ -910,6 +921,7 @@ def build(self, network: DynSysGroup, **build_options) -> BuiltComponentType: "'Transpose3d' will be removed in a future version. Use 'MatMul2d' instead.", category=PAIBoxDeprecationWarning, ) +@set_rt_mode(1, 1, 1) class Transpose3d(TransposeModule): def __init__( self, @@ -940,7 +952,7 @@ def __init__( **kwargs, ) - def spike_func(self, x1: SpikeType, **kwargs) -> SpikeType: + def spike_func(self, x1: NeuOutType, **kwargs) -> NeuOutType: _x1 = x1.reshape(self.shape_in) return _x1.transpose(self.axes) @@ -971,7 +983,7 @@ def build(self, network: DynSysGroup, **build_options) -> BuiltComponentType: def _spike_func_sadd_ssub( vjt: VoltageType, pos_thres: int, reset_v: Optional[int] = None -) -> tuple[SpikeType, VoltageType]: +) -> tuple[NeuOutType, VoltageType]: """Function `spike_func()` in spiking addition & subtraction.""" # Fire thres_mode = np.where( @@ -986,14 +998,14 @@ def _spike_func_sadd_ssub( v_reset = np.where(thres_mode == TM.EXCEED_POSITIVE, reset_v, vjt) # Spike - spike = np.equal(thres_mode, TM.EXCEED_POSITIVE) + spike = thres_mode == TM.EXCEED_POSITIVE - return spike, v_reset + return spike.astype(NEUOUT_U8_DTYPE), v_reset def _spike_func_avg_pool( vjt: VoltageType, pos_thres: int -) -> tuple[SpikeType, VoltageType]: +) -> tuple[NeuOutType, VoltageType]: """Function `spike_func()` in spiking addition & subtraction.""" # Fire thres_mode = np.where( @@ -1001,18 +1013,18 @@ def _spike_func_avg_pool( TM.EXCEED_POSITIVE, np.where(vjt < 0, TM.EXCEED_NEGATIVE, TM.NOT_EXCEEDED), ) - spike = np.equal(thres_mode, TM.EXCEED_POSITIVE) + spike = thres_mode == TM.EXCEED_POSITIVE # Reset v_reset = np.where(thres_mode == TM.EXCEED_POSITIVE, 0, vjt) - return spike, v_reset + return spike.astype(NEUOUT_U8_DTYPE), v_reset def _sum_inputs_sadd_ssub( - x1: SpikeType, x2: SpikeType, f1: int, f2: int, vjt_pre: VoltageType, strict: bool + x1: NeuOutType, x2: NeuOutType, f1: int, f2: int, vjt_pre: VoltageType, strict: bool ) -> VoltageType: """Function `sum_input()` for spiking addition & subtraction.""" - incoming_v = (vjt_pre + x1 * f1 + x2 * f2).astype(np.int32) + incoming_v = (vjt_pre + x1 * f1 + x2 * f2).astype(VOLTAGE_DTYPE) return vjt_overflow(incoming_v, strict) @@ -1029,7 +1041,7 @@ def _shape_check(shape: tuple[int, ...], ndim: int) -> tuple[int, ...]: _shape_ndim3_check = partial(_shape_check, ndim=3) -def _transpose2d_mapping(op_shape: tuple[int, ...]) -> NDArray[np.bool_]: +def _transpose2d_mapping(op_shape: tuple[int, ...]) -> WeightType: """Get the mapping matrix for transpose of 2d array. Argument: @@ -1048,7 +1060,7 @@ def _transpose2d_mapping(op_shape: tuple[int, ...]) -> NDArray[np.bool_]: def _transpose3d_mapping( op_shape: tuple[int, ...], axes: tuple[int, ...] -) -> NDArray[np.bool_]: +) -> WeightType: """Get the mapping matrix for transpose of 3d array. Argument: diff --git a/paibox/components/modules.py b/paibox/components/modules.py index 64f114e8..6d879593 100644 --- a/paibox/components/modules.py +++ b/paibox/components/modules.py @@ -3,16 +3,24 @@ from collections import deque from collections.abc import Sequence from dataclasses import dataclass, field -from typing import ClassVar, Optional, Union +from typing import ClassVar, Literal, Optional, TypeVar, Union import numpy as np -from paicorelib import TM, HwConfig +from paicorelib import ( + InputWidthFormat, + SpikeWidthFormat, + TM, + HwConfig, + SNNModeEnable, + get_core_mode, +) from paibox.base import NeuDyn from paibox.exceptions import NotSupportedError, RegisterError, ShapeError -from paibox.types import SpikeType, VoltageType +from paibox.types import NEUOUT_U8_DTYPE, NeuOutType, VoltageType from paibox.utils import check_elem_unique, shape2num +from .neuron.utils import _input_width_format, _spike_width_format from .projection import InputProj if sys.version_info >= (3, 10): @@ -28,7 +36,7 @@ __all__ = ["BuildingModule"] -MultiInputsType: TypeAlias = list[SpikeType] # Type of inputs of `NeuModule`. +MultiInputsType: TypeAlias = list[NeuOutType] # Type of inputs of `NeuModule`. BuiltComponentType: TypeAlias = list[Union["FullConnectedSyn", "Neuron"]] @@ -85,10 +93,13 @@ def n_output(self) -> int: class NeuModule(NeuDyn, BuildingModule): __gh_build_ignore__ = True - n_return: ClassVar[int] + n_return: ClassVar[int] = 1 """#N of outputs.""" inherent_delay: int = 0 """Internal delay of the module, relative to the external.""" + input_width: ClassVar[InputWidthFormat] = InputWidthFormat.WIDTH_1BIT + spike_width: ClassVar[SpikeWidthFormat] = SpikeWidthFormat.WIDTH_1BIT + snn_en: ClassVar[SNNModeEnable] = SNNModeEnable.ENABLE def __init__( self, @@ -177,25 +188,30 @@ def __init__( super().__init__(**kwargs, name=name) + self.mode = get_core_mode(self.input_width, self.spike_width, self.snn_en) self.keep_shape = keep_shape self._shape_out = shape_out self.register_operand(*operands) # Set memory for only 1 output node. # TODO how to handle with more than 1 output nodes - self.set_memory("_inner_spike", np.zeros((self.num_out,), dtype=np.bool_)) + self.set_memory("_neu_out", np.zeros((self.num_out,), dtype=NEUOUT_U8_DTYPE)) # Delay registers self.set_memory( "delay_registers", np.zeros( - (HwConfig.N_TIMESLOT_MAX,) + self._inner_spike.shape, dtype=np.bool_ + (HwConfig.N_TIMESLOT_MAX,) + self._neu_out.shape, dtype=NEUOUT_U8_DTYPE ), ) # Set a deque for the `synin` to implement the delay of `inherent_delay` for the module. if self.inherent_delay > 0: _init_synin = [ self.n_op - * [np.zeros(self.module_intf.operands[0].num_out, dtype=np.bool_)] + * [ + np.zeros( + self.module_intf.operands[0].num_out, dtype=NEUOUT_U8_DTYPE + ) + ] ] else: _init_synin = [] @@ -211,32 +227,32 @@ def get_inputs(self) -> None: # Retrieve the spike at index `timestamp` of the dest neurons if self.is_working(): if isinstance(op, InputProj): - synin.append(op.output.copy()) + synin.append(op.output) else: idx = self.timestamp % HwConfig.N_TIMESLOT_MAX - synin.append(op.output[idx].copy()) + synin.append(op.delay_registers[idx]) else: # Retrieve 0 to the dest neurons if it is not working synin.append(np.zeros_like(op.spike)) self.synin_deque.append(synin) # Append to the right of the deque. - def update(self, *args, **kwargs) -> Optional[SpikeType]: + def update(self, *args, **kwargs) -> Optional[NeuOutType]: if not self.is_working(): - self._inner_spike = np.zeros((self.num_out,), dtype=np.bool_) + self._neu_out.fill(0) return None self.get_inputs() if self.is_outputing(): synin = self.synin_deque.popleft() # Pop the left of the deque. - self._inner_spike = self.spike_func(*synin).ravel() + self._neu_out = self.spike_func(*synin).ravel() idx = ( self.timestamp - self.inherent_delay + self.delay_relative - 1 ) % HwConfig.N_TIMESLOT_MAX - self.delay_registers[idx] = self._inner_spike.copy() + self.delay_registers[idx] = self._neu_out.copy() - return self._inner_spike + return self._neu_out def _rebuild_out_intf( self, @@ -275,16 +291,16 @@ def num_out(self) -> int: return shape2num(self._shape_out) @property - def output(self) -> SpikeType: + def output(self) -> NeuOutType: return self.delay_registers @property - def spike(self) -> SpikeType: - return self._inner_spike + def spike(self) -> NeuOutType: + return self._neu_out @property - def feature_map(self) -> SpikeType: - return self._inner_spike.reshape(self.varshape) + def feature_map(self) -> NeuOutType: + return self._neu_out.reshape(self.varshape) @property def varshape(self) -> tuple[int, ...]: @@ -383,11 +399,13 @@ def __init__( def synaptic_integr(self, *args, **kwargs) -> VoltageType: """Functions used to describe synaptic integration of the module.""" - raise NotImplementedError + raise NotImplementedError( + "'synaptic_integr' should be implemented in the subclasses." + ) - def update(self, *args, **kwargs) -> Optional[SpikeType]: + def update(self, *args, **kwargs) -> Optional[NeuOutType]: if not self.is_working(): - self._inner_spike = np.zeros((self.num_out,), dtype=np.bool_) + self._neu_out.fill(0) return None self.get_inputs() @@ -396,14 +414,14 @@ def update(self, *args, **kwargs) -> Optional[SpikeType]: synin = self.synin_deque.popleft() # Pop the left of the deque. incoming_v = self.synaptic_integr(*synin, self._vjt) _is, self._vjt = self.spike_func(incoming_v) - self._inner_spike = _is.ravel() + self._neu_out = _is.ravel() idx = ( self.timestamp - self.inherent_delay + self.delay_relative - 1 ) % HwConfig.N_TIMESLOT_MAX - self.delay_registers[idx] = self._inner_spike.copy() + self.delay_registers[idx] = self._neu_out.copy() - return self._inner_spike + return self._neu_out @property def voltage(self) -> VoltageType: @@ -429,6 +447,20 @@ def __init__( ) +L = Literal +_T = TypeVar("_T", bound=NeuModule) + + +def set_rt_mode(input_width: L[1, 8], spike_width: L[1, 8], snn_en: L[0, 1]): + def wrapper(cls: type[_T]) -> type[_T]: + cls.input_width = _input_width_format(input_width) + cls.spike_width = _spike_width_format(spike_width) + cls.snn_en = SNNModeEnable(snn_en) + return cls + + return wrapper + + def _shape_check2( neuron_a: Union[NeuDyn, InputProj], neuron_b: Union[NeuDyn, InputProj], From d08a11e62d0792df88931c8599379a07e4d28afa Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 5 Jul 2024 14:35:05 +0800 Subject: [PATCH 007/187] =?UTF-8?q?=E2=9C=85=20skip=20the=20tests=20of=20d?= =?UTF-8?q?eprecated=20fmodules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/components/test_functional.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/components/test_functional.py b/tests/components/test_functional.py index 7b4b4530..9913fb11 100644 --- a/tests/components/test_functional.py +++ b/tests/components/test_functional.py @@ -551,6 +551,7 @@ def test_SpikingPool2dWithV_mapping(self, ensure_dump_dir): mapper.compile() mapper.export(fp=ensure_dump_dir) + @pytest.mark.skipif(hasattr(pb.Transpose2d, "__deprecated__")) @pytest.mark.parametrize("shape", [(32, 16), (1, 32), (64,), (128, 1), 48]) def test_Transpose2d(self, shape): from tests.shared_networks import TransposeModule_T2d_Net @@ -581,6 +582,7 @@ def test_Transpose2d(self, shape): expected = inpa[i - 2].T.ravel() assert np.array_equal(sim1.data[net1.probe2][i], expected) + @pytest.mark.skipif(hasattr(pb.Transpose2d, "__deprecated__")) def test_Transpose2d_mapping(self, ensure_dump_dir): from tests.shared_networks import TransposeModule_T2d_Net @@ -591,6 +593,7 @@ def test_Transpose2d_mapping(self, ensure_dump_dir): mapper.compile() mapper.export(fp=ensure_dump_dir) + @pytest.mark.skipif(hasattr(pb.Transpose2d, "__deprecated__")) @pytest.mark.parametrize( "shape, axes", [ @@ -632,6 +635,7 @@ def test_Transpose3d(self, shape, axes): expected = inpa[i - 2].transpose(axes).ravel() assert np.array_equal(sim1.data[net1.probe2][i], expected) + @pytest.mark.skipif(hasattr(pb.Transpose2d, "__deprecated__")) def test_Transpose3d_mapping(self, ensure_dump_dir): from tests.shared_networks import TransposeModule_T3d_Net From 43369c2f44e87004d1b3be23761917880ef1b38f Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 8 Jul 2024 17:26:50 +0800 Subject: [PATCH 008/187] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Unify=20weight=20d?= =?UTF-8?q?ata=20types=20&=20optimize=20weight=20precision=20processing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🎨 update typing --- paibox/backend/placement.py | 20 ++--- paibox/components/synapses/transforms.py | 51 ++++++----- paibox/types.py | 5 +- tests/backend/conftest.py | 12 +-- tests/backend/test_mapper.py | 8 +- tests/components/synapses/test_synapses.py | 37 ++++---- tests/components/synapses/test_transforms.py | 89 +++++++++----------- 7 files changed, 105 insertions(+), 117 deletions(-) diff --git a/paibox/backend/placement.py b/paibox/backend/placement.py index d4e6554e..809b3eb8 100644 --- a/paibox/backend/placement.py +++ b/paibox/backend/placement.py @@ -8,7 +8,7 @@ from paibox.components import FullConnectedSyn, Neuron from paibox.exceptions import GraphBuildError, ResourceError, TruncationWarning -from paibox.types import WeightType +from paibox.types import WeightType, WEIGHT_DTYPE from paibox.utils import check_attr_same, count_unique_elem from .conf_template import ( @@ -313,9 +313,11 @@ def raw_weight_of_dest(self) -> list[WeightType]: w_of_dest.append(syn.connectivity) else: # Fill with 0. - w_of_dest.append(np.zeros((s.num_out, d.num_in), dtype=np.int8)) + w_of_dest.append( + np.zeros((s.num_out, d.num_in), dtype=WEIGHT_DTYPE) + ) - w_dest = np.vstack(w_of_dest, dtype=np.int8) + w_dest = np.vstack(w_of_dest) w_of_neurons.append(w_dest) # Check @@ -430,9 +432,8 @@ def _fold_raw_weights(self, raw_weights: list[WeightType]) -> WeightType: n_fold = self.n_timeslot if self.lcn_ex == LCN_EX.LCN_1X: - w_folded = np.hstack(raw_weights, dtype=np.int8) + w_folded = np.hstack(raw_weights) w_folded.setflags(write=False) - return w_folded # LCN_EX > LCN_1X @@ -455,12 +456,11 @@ def _fold_raw_weights(self, raw_weights: list[WeightType]) -> WeightType: ) w_folded_of_axon_segs.append(w_folded_of_axon_seg) - w_folded = np.vstack(w_folded_of_axon_segs, dtype=np.int8) + w_folded = np.vstack(w_folded_of_axon_segs) w_folded_list.append(w_folded) - w_folded = np.hstack(w_folded_list, dtype=np.int8) + w_folded = np.hstack(w_folded_list) w_folded.setflags(write=False) - return w_folded def _weight_ram_mapping(self) -> WeightRamType: @@ -528,7 +528,7 @@ def _nfold_weight( _raw_weight = np.append( raw_weight, - np.zeros((n_row_padding, raw_col), dtype=np.int8), + np.zeros((n_row_padding, raw_col), dtype=WEIGHT_DTYPE), axis=0, ) else: @@ -539,7 +539,7 @@ def _nfold_weight( # Check #2 # assert _raw_weight.shape[0] == expected_row * n_fold - w_folded = np.zeros((expected_row, raw_col * n_fold), dtype=np.int8) + w_folded = np.zeros((expected_row, raw_col * n_fold), dtype=WEIGHT_DTYPE) for i, j in np.ndindex((n_fold, raw_col)): w_col = w_splited[i][:, j] diff --git a/paibox/components/synapses/transforms.py b/paibox/components/synapses/transforms.py index 64a6b8ce..78807f2d 100644 --- a/paibox/components/synapses/transforms.py +++ b/paibox/components/synapses/transforms.py @@ -7,12 +7,13 @@ from paibox.exceptions import AutoOptimizationWarning, ShapeError from paibox.types import ( + WEIGHT_DTYPE, DataArrayType, IntScalarType, NeuOutType, SynOutType, WeightType, - VOLTAGE_DTYPE, + VOLTAGE_DTYPE ) from paibox.utils import is_shape, shape2num, typical_round @@ -70,7 +71,7 @@ def _set_coarse_dtype(raw_w: DataArrayType) -> WeightType: """Convert raw weights to `np.ndarray` coarsely (without optimization). Description: - - For weights of type `bool` or `np.bool_`, set `np.bool_` as the dtype. + - For weights of type `bool` or `np.bool_`, set `np.int8` as the dtype. - For integer scalar weight, set the dtype according to its value. - For array weights, set the dtype according to its minimum & maximum values. For weights in the\ range of int8, the dtype when declared will be followed (i.e. not optimized). @@ -83,12 +84,7 @@ def _set_coarse_dtype(raw_w: DataArrayType) -> WeightType: if raw_w > MAX_INT8 or raw_w < MIN_INT8: raise ValueError(f"weight out of range int8, got {raw_w}.") - if raw_w <= MAX_INT1 and raw_w >= MIN_INT1: - _dtype = np.bool_ - else: - _dtype = np.int8 - - return np.asarray(raw_w, dtype=_dtype) + return np.asarray(raw_w, dtype=WEIGHT_DTYPE) # Convert list or tuple to np.ndarray _array = np.asarray(raw_w) @@ -103,10 +99,10 @@ def _set_coarse_dtype(raw_w: DataArrayType) -> WeightType: f"dtype of weight is optimized automatically, {_array.dtype} -> int8.", AutoOptimizationWarning, ) - _dtype = np.int8 + _dtype = WEIGHT_DTYPE elif _array.dtype == np.bool_ or _array.dtype == np.int8: - _dtype = _array.dtype + _dtype = WEIGHT_DTYPE else: raise TypeError(f"weights must be bool or int8, but got {_array.dtype}.") @@ -128,17 +124,13 @@ def _get_weight_precision(weight: WeightType, enable_wp_opt: bool) -> WP: else: return WP.WEIGHT_WIDTH_8BIT else: - # If weight precision opt is disabled, return WP1 if dtype is np.bool_ else WP8. - if weight.dtype == np.bool_: - return WP.WEIGHT_WIDTH_1BIT - else: - return WP.WEIGHT_WIDTH_8BIT + return WP.WEIGHT_WIDTH_8BIT class Transform: def __init__(self, weights: DataArrayType) -> None: self.weights = _set_coarse_dtype(weights) - """The actual weights in synapses. Stored in `np.bool_` or `np.int8` format.""" + """The actual weights in synapses. Stored in np.int8 format.""" self.weights.setflags(write=False) @@ -195,7 +187,7 @@ def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: @property def connectivity(self): return ( - (self.weights * np.identity(self.num, dtype=np.bool_)) + (self.weights * np.identity(self.num, dtype=WEIGHT_DTYPE)) if self.weights.ndim == 0 else np.diag(self.weights) ) @@ -259,7 +251,7 @@ def connectivity(self): return ( self.weights if self.weights.ndim == 2 - else (self.weights * np.ones(self.conn_size, dtype=np.bool_)) + else (self.weights * np.ones(self.conn_size, dtype=WEIGHT_DTYPE)) ) @@ -305,7 +297,7 @@ def _matmul_unroll( n_oshape = shape2num(out_shape) in_shape_t = tuple(in_shape[i] for i in axes) - w_unrolled = np.zeros((n_ishape, n_oshape), dtype=weights.dtype) + w_unrolled = np.zeros((n_ishape, n_oshape), dtype=WEIGHT_DTYPE) orig_idx = np.arange(n_ishape).reshape(in_shape_t) mapping_tbl = orig_idx.transpose(np.argsort(axes)).ravel() @@ -560,3 +552,24 @@ def connectivity(self): self.stride, self.padding, ) + + +class _CompareMax(AllToAll): + def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: + """The maximum value of the input corresponding to the non-zero columns of the weight matrix is \ + taken as the output. + x = (x1, x2, ..., xn) + w = [n*m] + y = (y1, y2, ..., ym) + """ + if self.weights.ndim == 0: + output = np.full( + (self.conn_size[1],), np.max(x, axis=None), dtype=VOLTAGE_DTYPE + ) + else: + output = np.zeros((self.conn_size[1],), dtype=VOLTAGE_DTYPE) + for col in range(self.conn_size[1]): + non_zero_idx = np.nonzero(self.weights[:, col])[0] + output[col] = np.max(x[non_zero_idx]) + + return output diff --git a/paibox/types.py b/paibox/types.py index 4392684e..f5a90365 100644 --- a/paibox/types.py +++ b/paibox/types.py @@ -1,5 +1,5 @@ import sys -from typing import TypeVar, Union +from typing import TypeVar import numpy as np from numpy.typing import NDArray @@ -21,6 +21,7 @@ LEAK_V_DTYPE = np.int32 SPIKE_DTYPE = np.bool_ VOLTAGE_DTYPE = np.int32 +WEIGHT_DTYPE = np.int8 NEUOUT_SPIKE_DTYPE = np.bool_ NEUOUT_U8_DTYPE = np.uint8 @@ -29,4 +30,4 @@ SynOutType: TypeAlias = NDArray[VOLTAGE_DTYPE] VoltageType: TypeAlias = NDArray[VOLTAGE_DTYPE] NeuOutType: TypeAlias = NDArray[NEUOUT_U8_DTYPE] -WeightType: TypeAlias = NDArray[Union[np.bool_, np.int8]] +WeightType: TypeAlias = NDArray[WEIGHT_DTYPE] diff --git a/tests/backend/conftest.py b/tests/backend/conftest.py index cf27ad13..7d1285ce 100644 --- a/tests/backend/conftest.py +++ b/tests/backend/conftest.py @@ -1282,55 +1282,48 @@ class TestData: ) cflags_weight_bit_opt_data = ParametrizedTestData( - args="range, scalar, dtype, expected_wp_noopt, expected_wp_opt", + args="range, scalar, dtype, expected_wp_opt", data=[ ( ((0, 2), (0, 2)), 1, (np.bool_, np.bool_), WP.WEIGHT_WIDTH_1BIT, - WP.WEIGHT_WIDTH_1BIT, ), ( ((0, 2), (0, 2)), -1, (np.bool_, np.bool_), - WP.WEIGHT_WIDTH_8BIT, WP.WEIGHT_WIDTH_2BIT, ), ( ((0, 2), (0, 2)), 1, (np.bool_, np.int8), - WP.WEIGHT_WIDTH_8BIT, WP.WEIGHT_WIDTH_1BIT, ), ( ((0, 2), (0, 2)), -2, (np.int8, np.bool_), - WP.WEIGHT_WIDTH_8BIT, WP.WEIGHT_WIDTH_2BIT, ), ( ((0, 2), (0, 2)), 1, (np.int8, np.int8), - WP.WEIGHT_WIDTH_8BIT, WP.WEIGHT_WIDTH_1BIT, ), ( ((0, 2), (-2, 2)), -8, (np.bool_, np.int8), - WP.WEIGHT_WIDTH_8BIT, WP.WEIGHT_WIDTH_4BIT, ), ( ((0, 2), (-2, 2)), 7, (np.bool_, np.int8), - WP.WEIGHT_WIDTH_8BIT, WP.WEIGHT_WIDTH_4BIT, ), ( @@ -1338,13 +1331,11 @@ class TestData: 127, (np.bool_, np.int8), WP.WEIGHT_WIDTH_8BIT, - WP.WEIGHT_WIDTH_8BIT, ), ( ((-2, 2), (-8, 8)), 7, (np.int8, np.int8), - WP.WEIGHT_WIDTH_8BIT, WP.WEIGHT_WIDTH_4BIT, ), ( @@ -1352,7 +1343,6 @@ class TestData: -100, (np.int8, np.int8), WP.WEIGHT_WIDTH_8BIT, - WP.WEIGHT_WIDTH_8BIT, ), ], ) diff --git a/tests/backend/test_mapper.py b/tests/backend/test_mapper.py index 29b14572..21f3cabbc 100644 --- a/tests/backend/test_mapper.py +++ b/tests/backend/test_mapper.py @@ -2,7 +2,7 @@ import numpy as np import pytest -from paicorelib import Coord, HwConfig +from paicorelib import Coord, HwConfig, WeightPrecision as WP import paibox as pb from paibox.base import SynSys @@ -542,9 +542,7 @@ class TestMapper_cflags: TestData.cflags_weight_bit_opt_data["args"], TestData.cflags_weight_bit_opt_data["data"], ) - def test_cflags_weight_bit_opt( - self, range, scalar, dtype, expected_wp_noopt, expected_wp_opt - ): + def test_cflags_weight_bit_opt(self, range, scalar, dtype, expected_wp_opt): # s1, s2, s3 will be grouped in one core block. class Net(pb.Network): def __init__(self): @@ -578,7 +576,7 @@ def __init__(self): mapper = pb.Mapper() mapper.build(net) mapper.compile(weight_bit_optimization=False) - assert mapper.core_blocks[0].weight_precision == expected_wp_noopt + assert mapper.core_blocks[0].weight_precision == WP.WEIGHT_WIDTH_8BIT mapper.clear() mapper.build(net) diff --git a/tests/components/synapses/test_synapses.py b/tests/components/synapses/test_synapses.py index 7e9b805d..6378b4de 100644 --- a/tests/components/synapses/test_synapses.py +++ b/tests/components/synapses/test_synapses.py @@ -7,6 +7,7 @@ import paibox as pb from paibox.components import FullConnectedSyn from paibox.exceptions import RegisterError, ShapeError +from paibox.types import WEIGHT_DTYPE from paibox.utils import shape2num @@ -127,13 +128,9 @@ def test_FullConn_One2One_scalar(self, n1, n2, scalar_weight, expected_wp): assert (s1.num_in, s1.num_out) == (n1.num_out, n2.num_in) assert np.array_equal( s1.connectivity, - scalar_weight * np.identity(n1.num_out, dtype=np.int8), - ) - assert ( - s1.connectivity.dtype == np.int8 - if expected_wp > WP.WEIGHT_WIDTH_1BIT - else np.bool_ + scalar_weight * np.identity(n1.num_out, dtype=WEIGHT_DTYPE), ) + assert s1.connectivity.dtype == WEIGHT_DTYPE assert s1.weight_precision is expected_wp @pytest.mark.parametrize( @@ -160,7 +157,7 @@ def test_FullConn_One2One_matrix(self): assert np.array_equal( s1.connectivity, np.array([[2, 0, 0], [0, 3, 0], [0, 0, 4]], dtype=np.int8) ) - assert s1.connectivity.dtype == np.int8 + assert s1.connectivity.dtype == WEIGHT_DTYPE assert s1.weight_precision is WP.WEIGHT_WIDTH_4BIT weight = np.array([1, 0, 1, 0], np.int8) @@ -173,10 +170,10 @@ def test_FullConn_One2One_matrix(self): assert np.array_equal( s2.connectivity, np.array( - [[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 0]], dtype=np.bool_ + [[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 0]], dtype=np.int16 ), ) - assert s2.connectivity.dtype == np.int8 + assert s2.connectivity.dtype == WEIGHT_DTYPE assert s2.weight_precision is WP.WEIGHT_WIDTH_1BIT @pytest.mark.parametrize( @@ -193,7 +190,7 @@ def test_FullConn_All2All(self, n1, n2): s1 = pb.FullConn(n1, n2, conn_type=pb.SynConnType.All2All) assert (s1.num_in, s1.num_out) == (n1.num_out, n2.num_in) - assert s1.connectivity.dtype == np.bool_ + assert s1.connectivity.dtype == WEIGHT_DTYPE assert np.array_equal(s1.weights, 1) assert np.array_equal(s1.connectivity, np.ones((n1.num_out, n2.num_in))) @@ -206,14 +203,14 @@ def test_FullConn_All2All_with_weights(self): s1 = pb.FullConn(n1, n2, weight, conn_type=pb.SynConnType.All2All) assert np.array_equal(s1.weights, weight) - assert s1.connectivity.dtype == np.int8 + assert s1.connectivity.dtype == WEIGHT_DTYPE assert s1.weight_precision is WP.WEIGHT_WIDTH_4BIT """2. Weights matrix.""" weight = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) s2 = pb.FullConn(n1, n2, weight, conn_type=pb.SynConnType.All2All) - assert s2.connectivity.dtype == np.int8 + assert s2.connectivity.dtype == WEIGHT_DTYPE assert np.array_equal(s2.weights, weight) assert np.array_equal(s2.connectivity, weight) @@ -266,7 +263,7 @@ def test_MatMul2d_instance(self, n1, n2, w_shape, expectation): s = pb.MatMul2d(n1, n2, weights=weights) assert (s.num_in, s.num_out) == (n1.num_out, n2.num_in) - assert s.connectivity.dtype == np.int8 + assert s.connectivity.dtype == WEIGHT_DTYPE assert np.array_equal(s.weights, weights) @@ -292,7 +289,7 @@ def test_Conv1d_instance(self): ) assert s1.num_in == in_channels * shape2num(in_shape) - assert s1.connectivity.dtype == np.int8 + assert s1.connectivity.dtype == WEIGHT_DTYPE assert s1.connectivity.shape == ( in_channels * shape2num(in_shape), out_channels * shape2num(out_shape), @@ -320,7 +317,7 @@ def test_Conv2d_instance(self): ) assert s1.num_in == in_channels * shape2num(in_shape) - assert s1.connectivity.dtype == np.int8 + assert s1.connectivity.dtype == WEIGHT_DTYPE assert s1.connectivity.shape == ( in_channels * shape2num(in_shape), out_channels * shape2num(out_shape), @@ -344,7 +341,7 @@ def test_Conv1d_inchannel_omitted(self): s1 = pb.Conv1d(n1, n2, weight, stride=stride, kernel_order=korder) assert s1.num_in == in_channels * shape2num(in_shape) - assert s1.connectivity.dtype == np.int8 + assert s1.connectivity.dtype == WEIGHT_DTYPE assert s1.connectivity.shape == ( in_channels * shape2num(in_shape), out_channels * shape2num(out_shape), @@ -403,7 +400,7 @@ def test_ConvTranspose1d_instance(self): ) assert s1.num_in == in_channels * shape2num(in_shape) - assert s1.connectivity.dtype == np.int8 + assert s1.connectivity.dtype == WEIGHT_DTYPE assert s1.connectivity.shape == ( in_channels * shape2num(in_shape), out_channels * shape2num(out_shape), @@ -437,7 +434,7 @@ def test_ConvTranspose2d_instance(self): ) assert s1.num_in == in_channels * shape2num(in_shape) - assert s1.connectivity.dtype == np.int8 + assert s1.connectivity.dtype == WEIGHT_DTYPE assert s1.connectivity.shape == ( in_channels * shape2num(in_shape), out_channels * shape2num(out_shape), @@ -471,7 +468,7 @@ def test_ConvTranspose1d_inchannel_omitted(self): ) assert s1.num_in == in_channels * shape2num(in_shape) - assert s1.connectivity.dtype == np.int8 + assert s1.connectivity.dtype == WEIGHT_DTYPE assert s1.connectivity.shape == ( in_channels * shape2num(in_shape), out_channels * shape2num(out_shape), @@ -505,7 +502,7 @@ def test_ConvTranspose2d_inchannel_omitted(self): ) assert s1.num_in == in_channels * shape2num(in_shape) - assert s1.connectivity.dtype == np.int8 + assert s1.connectivity.dtype == WEIGHT_DTYPE assert s1.connectivity.shape == ( in_channels * shape2num(in_shape), out_channels * shape2num(out_shape), diff --git a/tests/components/synapses/test_transforms.py b/tests/components/synapses/test_transforms.py index 9b7856ea..d94e21b9 100644 --- a/tests/components/synapses/test_transforms.py +++ b/tests/components/synapses/test_transforms.py @@ -3,48 +3,49 @@ from paibox.components.synapses import transforms as tfm from paibox.exceptions import AutoOptimizationWarning +from paibox.types import WEIGHT_DTYPE from paibox.utils import shape2num class TestTransforms: @pytest.mark.parametrize( - "weight, expected_dtype", + "weight", [ - (np.array([1, 2, 3], dtype=np.int8), np.int8), - (np.array([1, 0, 1], dtype=np.bool_), np.bool_), - (np.array([True, False]), np.bool_), - (np.array([True, False], dtype=np.int8), np.int8), - (10, np.int8), - (1, np.bool_), - (True, np.bool_), - (np.int8(1), np.bool_), # automatically optimizated - (np.uint8(99), np.int8), - (np.array([-128, 1, 127], dtype=np.int8), np.int8), - ([1, 2, 3], np.int8), - ((0, 1, 0, 1), np.int8), + np.array([1, 2, 3], dtype=np.int8), + np.array([1, 0, 1], dtype=np.bool_), + np.array([True, False]), + np.array([True, False], dtype=np.int8), + 10, + 1, + True, + np.int8(1), # automatically optimizated + np.uint8(99), + np.array([-128, 1, 127], dtype=np.int8), + [1, 2, 3], + (0, 1, 0, 1), ], ) - def test_weight_dtype_convert(self, weight, expected_dtype): + def test_weight_dtype_convert(self, weight): t = tfm.Transform(weight) - assert t.weights.dtype == expected_dtype + assert t.weights.dtype == WEIGHT_DTYPE @pytest.mark.parametrize( - "weight, expected_dtype", + "weight", [ - (np.array([1, 2, 3]), np.int8), + np.array([1, 2, 3]), # Only automatically optimized to int8 unless specified as bool - (np.array([True, False], dtype=np.int16), np.int8), - (np.array([1, 0, 1], dtype=np.int16), np.int8), # Same as above - (np.array([-128, 1, 127], dtype=np.int32), np.int8), - (np.array([-8, 4, 7]), np.int8), - ([-100, 0, 100], np.int8), + np.array([True, False], dtype=np.int16), + np.array([1, 0, 1], dtype=np.int16), # Same as above + np.array([-128, 1, 127], dtype=np.int32), + np.array([-8, 4, 7]), + [-100, 0, 100], ], ) - def test_weight_dtype_convert_warning(self, weight, expected_dtype): + def test_weight_dtype_convert_warning(self, weight): with pytest.warns(AutoOptimizationWarning): t = tfm.Transform(weight) - assert t.weights.dtype == expected_dtype + assert t.weights.dtype == WEIGHT_DTYPE @pytest.mark.parametrize( "weight", @@ -111,15 +112,8 @@ def test_OneToOne(self): assert y.shape == (4,) @pytest.mark.parametrize( - "weight, expected_dtype", - [ - (1, np.bool_), - (-1, np.int8), - (10, np.int8), - (-100, np.int8), - (-128, np.int8), - (127, np.int8), - ], + "weight", + [1, -1, 10, -100, -128, 127], ids=[ "scalar_1", "scalar_-1", @@ -129,7 +123,7 @@ def test_OneToOne(self): "scalar_-127", ], ) - def test_AllToAll_weight_scalar(self, weight, expected_dtype): + def test_AllToAll_weight_scalar(self, weight): """Test `AllToAll` when weight is a scalar""" num_in, num_out = 10, 20 @@ -138,7 +132,7 @@ def test_AllToAll_weight_scalar(self, weight, expected_dtype): y = f(x) expected = np.full((num_out,), np.sum(x, axis=None), dtype=np.int32) * weight - assert f.connectivity.dtype == expected_dtype + assert f.connectivity.dtype == WEIGHT_DTYPE assert y.dtype == np.int32 assert y.shape == (num_out,) assert y.ndim == 1 @@ -146,37 +140,32 @@ def test_AllToAll_weight_scalar(self, weight, expected_dtype): assert f.connectivity.shape == (num_in, num_out) @pytest.mark.parametrize( - "shape, x, weights, expected_dtype", + "shape, x, weights", [ ( (3, 4), np.random.randint(2, size=(3,), dtype=np.bool_), np.random.randint(2, size=(3, 4), dtype=np.bool_), - np.bool_, ), ( (10, 20), np.random.randint(2, size=(10,), dtype=np.bool_), np.random.randint(127, size=(10, 20), dtype=np.int8), - np.int8, ), ( (20, 10), np.random.randint(2, size=(20,), dtype=np.bool_), np.random.randint(2, size=(20, 10), dtype=np.bool_), - np.bool_, ), ( (2, 2), np.array([1, 1], dtype=np.bool_), np.array([[1, 2], [3, 4]], dtype=np.int8), - np.int8, ), ( (2, 2), np.array([1, 1], dtype=np.bool_), np.array([[127, 0], [3, -128]], dtype=np.int8), - np.int8, ), ], ids=[ @@ -187,43 +176,43 @@ def test_AllToAll_weight_scalar(self, weight, expected_dtype): "weights_int8_4", ], ) - def test_AllToAll_array(self, shape, x, weights, expected_dtype): + def test_AllToAll_array(self, shape, x, weights): """Test `AllToAll` when weights is an array""" f = tfm.AllToAll(shape, weights) y = f(x) expected = x @ weights.copy().astype(np.int32) - assert f.connectivity.dtype == expected_dtype + assert f.connectivity.dtype == WEIGHT_DTYPE assert np.array_equal(y, expected) assert f.connectivity.shape == shape @pytest.mark.parametrize( - "x, weights, expected_dtype", + "x, weights", [ ( np.arange(12, dtype=np.int8).reshape(3, 4), np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]], dtype=np.int8), - np.int8, ), ( np.random.randint(2, size=(10,), dtype=np.bool_), np.random.randint(-10, 10, size=(10, 20), dtype=np.int8), - np.int8, ), ( np.ones((20, 10), dtype=np.bool_), np.random.randint(2, size=(20, 10), dtype=np.bool_), - np.bool_, ), ( np.array((1, 1), dtype=np.bool_), np.array([[127, 0], [3, -128]], dtype=np.int8), - np.int8, ), ], ) - def test_MaskedLinear(self, x, weights, expected_dtype): + def test_MaskedLinear( + self, + x, + weights, + ): if x.ndim == 1: in_shape = (1, x.shape[0]) else: @@ -242,7 +231,7 @@ def test_MaskedLinear(self, x, weights, expected_dtype): y2 = x.flatten() @ f.connectivity.astype(np.int32) expected = x.reshape(in_shape).transpose(axes) @ weights.copy().astype(np.int32) - assert f.connectivity.dtype == expected_dtype + assert f.connectivity.dtype == WEIGHT_DTYPE assert y.shape == oshape assert y2.dtype == np.int32 assert np.array_equal(y, expected) From 17a106053b78bd550d3b2cb6ac7da329166ccae4 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Wed, 10 Jul 2024 09:56:38 +0800 Subject: [PATCH 009/187] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20bit=20r?= =?UTF-8?q?eversal=20functions=20&=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/conf_template.py | 6 ++---- paibox/utils.py | 21 +++++++++++++-------- tests/test_utils.py | 26 +++++++++++++++++++------- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/paibox/backend/conf_template.py b/paibox/backend/conf_template.py index 8bb7baf5..080f8fba 100644 --- a/paibox/backend/conf_template.py +++ b/paibox/backend/conf_template.py @@ -36,10 +36,8 @@ else: from typing_extensions import TypeAlias -from typing_extensions import NotRequired - from paibox.components import Neuron -from paibox.utils import bit_reversal +from paibox.utils import reverse_8bit from .context import _BACKEND_CONTEXT from .types import AxonCoord, NeuSegment, NodeName @@ -603,7 +601,7 @@ def to_clk_en_L2_u8(L2_inchip: list[RoutingCoord]) -> list[int]: for _ in range(8): u8 = bitmap & _mask(8) bitmap >>= 8 - clk_en.append(bit_reversal(u8)) + clk_en.append(reverse_8bit(u8)) return clk_en diff --git a/paibox/utils.py b/paibox/utils.py index cbf6c17c..08c9ec59 100644 --- a/paibox/utils.py +++ b/paibox/utils.py @@ -143,14 +143,19 @@ def typical_round(n: float) -> int: return int(n) + 1 -def bit_reversal(uint: int, n_bit: int = 8) -> int: - """Reverse the bit order of a N-bit unsigned integer, where N is `n_bit`.""" - reversed = 0 - for i in range(n_bit): - if (uint >> i) & 1: - reversed += 1 << (n_bit - 1 - i) - - return reversed +def reverse_8bit(x: int) -> int: + """Reverse the bit order of 8-bit unsigned integer.""" + x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1) + x = ((x & 0xCC) >> 2) | ((x & 0x33) << 2) + x = ((x & 0xF0) >> 4) | ((x & 0x0F) << 4) + return x + + +def reverse_16bit(x: int) -> int: + x = ((x & 0xAAAA) >> 1) | ((x & 0x5555) << 1) + x = ((x & 0xCCCC) >> 2) | ((x & 0x3333) << 2) + x = ((x & 0xF0F0) >> 4) | ((x & 0x0F0F) << 4) + return ((x >> 8) | (x << 8)) & 0xFFFF def arg_check_pos(arg: int, desc: Optional[str] = None) -> int: diff --git a/tests/test_utils.py b/tests/test_utils.py index faa17e2e..c482a099 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,6 @@ import pytest -from paibox.utils import bit_reversal, fn_sgn, typical_round +from paibox.utils import reverse_8bit, reverse_16bit, fn_sgn, typical_round @pytest.mark.parametrize("a,b, expected", [(1, 0, 1), (1, 2, -1), (3, 3, 0)]) @@ -16,12 +16,24 @@ def test_typical_round(n, expected): @pytest.mark.parametrize( - "uint, n_bit, expected", + "x, expected", [ - (0b10110, 5, 0b01101), - (0b0111_0111_1001_1001, 10, 0b1001_1001_11), - (0b1010_1100_1101, 7, 0b1011_001), + (0b1001_0110, 0b0110_1001), + (0b0001_1001, 0b1001_1000), + (0b1100_1101, 0b1011_0011), ], ) -def test_bit_reversal(uint, n_bit, expected): - assert bit_reversal(uint, n_bit) == expected +def test_reverse_8bit(x, expected): + assert reverse_8bit(x) == expected + + +@pytest.mark.parametrize( + "x, expected", + [ + (0b0110_0001_1001_0111, 0b1110_1001_1000_0110), + (0b1110_0011_0001_1001, 0b1001_1000_1100_0111), + (0b1100_1101_1001_1101, 0b1011_1001_1011_0011), + ], +) +def test_reverse_16bit(x, expected): + assert reverse_16bit(x) == expected From d6fe959bbf7b81b1a680305ffd6d4b6fe7aa4fe3 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Sun, 14 Jul 2024 13:36:12 +0800 Subject: [PATCH 010/187] =?UTF-8?q?=E2=9C=A8=20support=20finding=20axons?= =?UTF-8?q?=20address=20with=208-bit=20input=20width?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/segment_utils.py | 89 ++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 30 deletions(-) diff --git a/paibox/backend/segment_utils.py b/paibox/backend/segment_utils.py index 083862e3..e75bd8c5 100644 --- a/paibox/backend/segment_utils.py +++ b/paibox/backend/segment_utils.py @@ -1,5 +1,4 @@ import warnings -from collections.abc import Sequence from functools import partial from math import ceil from typing import Literal @@ -256,22 +255,18 @@ def get_neu_segments( def get_axon_segments( - axons: Sequence[SourceNodeType], tr_max: int, fan_in_max: int + axons: list[SourceNodeType], tr_max: int, n_fanin: int ) -> dict[SourceNodeType, AxonSegment]: """Divide axons into segments by group to fit the hardware constraints. Args: - - axons: The axons to be segmented. - - tr_max: The maximum value of the time slot(=n_timeslot). - - fan_in_max: The value of fan-in per dendrite(=N_FANIN_PER_DENDRITE_XNN). - - TODO Provide an alternative when failed. + - axons: the axons to be segmented. + - tr_max: the maximum value of the time slot(n_timeslot). + - n_fanin: the fan-in of cores. """ - def _seg_alloc(axon: SourceNodeType) -> AxonSegment: + def _seg_alloc(axon: SourceNodeType, offset: int) -> tuple[AxonSegment, int]: """Allocate an axon segment, return the next offset of axon address.""" - nonlocal offset - # The width of assigned address if axon.num_out % tr_max > 0: addr_width = axon.num_out // tr_max + 1 @@ -280,58 +275,92 @@ def _seg_alloc(axon: SourceNodeType) -> AxonSegment: addr_width = axon.num_out // tr_max # n_axon_rest = 0 - if offset + addr_width > fan_in_max: + if offset + addr_width > n_fanin: raise ResourceError( - f"axons address out of range [0, {fan_in_max}) ({offset + addr_width})." + f"axons address out of range [0, {n_fanin}) ({offset + addr_width})." ) - cur_offset = offset - offset += addr_width - - return AxonSegment(axon.num_out, addr_width, cur_offset) + return AxonSegment(axon.num_out, addr_width, offset), offset + addr_width offset = 0 axon_segments = dict() for axon in axons: - segment = _seg_alloc(axon) + segment, offset = _seg_alloc(axon, offset) axon_segments[axon] = segment return axon_segments def aligned_coords( - neu_index: NeuSlice, axon_seg: AxonSegment, delay: int, dest_n_timeslot: int + neu_index: NeuSlice, + axon_seg: AxonSegment, + delay: int, + dest_n_timeslot: int, + is_iw8: bool, ) -> list[AxonCoord]: """Find the axon segments aligned with the index of neuron segment. - The length of axon coordinates is the same as `neu_index`. + NOTE: Axons are described in a tuple (tick_relative, axon_addr). Axis 'tr' is used as the row \ + coordinates while axis 'axon' is used as the column coordinates. + + | ------- AxonSeg[0] ------- | ------- AxonSeg[1] ------- | ... + tr=0 A1[0] A1[1] ... A1[99] A2[0] A2[1] ... A2[199] + tr=1 A1[100] A1[101] ... A1[199] A2[200] A2[201] ... A2[399] + + The target axon may be Ax[100:499], where (tr=0, offset+100) is the start and (tr=2, offset+499)\ + is the end. + offset + | <--------- width --------> | + | ... | ------- AxonSeg[x] ------- | ... + tr=0 ... Ax[0] Ax[1] ... Ax[199] + tr=1 ... Ax[200] Ax[201] ... Ax[399] + tr=2 ... Ax[400] Ax[401] ... Ax[599] + + When the input width is 8 bits, each A[x] occupies 8 bits. The interval of axons is 8. """ - axon_coords = [] addr_width = axon_seg.addr_width addr_offset = axon_seg.addr_offset # tick_relative = n_timeslot * (delay - 1) + tr_offset (start & end) tr_base = dest_n_timeslot * (delay - 1) - tr_offset_start, tr_offset_stop = ( neu_index.start // addr_width, neu_index.stop // addr_width, ) addr_start, addr_stop = (neu_index.start % addr_width, neu_index.stop % addr_width) + _addr_interval = 8 if is_iw8 else 1 + if tr_offset_stop == tr_offset_start: - for addr in range(addr_start, addr_stop): - axon_coords.append(AxonCoord(tr_base + tr_offset_start, addr_offset + addr)) + axon_coords = [ + AxonCoord(tr_base + tr_offset_start, (addr_offset + addr) * _addr_interval) + for addr in range(addr_start, addr_stop) + ] else: - for addr in range(addr_start, addr_width): - axon_coords.append(AxonCoord(tr_base + tr_offset_start, addr_offset + addr)) - + # First row: addr_start -> end + acoords_first = [ + AxonCoord(tr_base + tr_offset_start, (addr_offset + addr) * _addr_interval) + for addr in range(addr_start, addr_width) + ] + + # Middle rows + acoords_mid = [] for tr in range(tr_offset_start + 1, tr_offset_stop): - for addr in range(addr_width): - axon_coords.append(AxonCoord(tr_base + tr, addr_offset + addr)) + acoords_mid.extend( + AxonCoord(tr_base + tr, (addr_offset + addr) * _addr_interval) + for addr in range(addr_width) + ) + + # Last row: start -> addr_stop + acoords_last = [ + AxonCoord(tr_base + tr_offset_stop, (addr_offset + addr) * _addr_interval) + for addr in range(addr_stop) + ] - for addr in range(addr_stop): - axon_coords.append(AxonCoord(tr_base + tr_offset_stop, addr_offset + addr)) + axon_coords = [] + axon_coords.extend(acoords_first) + axon_coords.extend(acoords_mid) + axon_coords.extend(acoords_last) return axon_coords From 3d5caa46c3e7bfc584c94db25d1b1bd3e276e9f0 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Sun, 14 Jul 2024 13:36:56 +0800 Subject: [PATCH 011/187] =?UTF-8?q?=E2=9C=85=20add=20test=20cases=20for=20?= =?UTF-8?q?finding=20axons=20address=20with=208-bit=20input=20width?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/backend/conftest.py | 100 ++++++++++++++++++++++------ tests/backend/test_segment_utils.py | 5 +- 2 files changed, 81 insertions(+), 24 deletions(-) diff --git a/tests/backend/conftest.py b/tests/backend/conftest.py index 7d1285ce..0f3adac2 100644 --- a/tests/backend/conftest.py +++ b/tests/backend/conftest.py @@ -981,17 +981,11 @@ def packbits1(): def n_axon2lcn_ex_proto(n_axon, n_fanin_max) -> LCN_EX: - """Convert #N(of axons) to `LCN_EX` & check. - - NOTE: LCN_EX = log2[ceil(#N/fan-in per dendrite)], where `LCN_1X` = 0. - """ if n_axon < 1: - raise ValueError(f"the number of axons must be positive, but got {n_axon}.") + raise ValueError if (lcn := ((n_axon - 1) // n_fanin_max).bit_length()) > LCN_EX.LCN_64X: - raise ResourceError( - f"required LCN extension out of range {LCN_EX.LCN_64X} ({lcn}). " - ) + raise ResourceError return LCN_EX(lcn) @@ -1620,13 +1614,15 @@ class TestData: ) aligned_coords_test_data = ParametrizedTestData( - args="neu_index, axon_seg, delay, n_timeslot, expected", + args="neu_index, axon_seg, delay, n_timeslot, is_iw8, expected", data=[ + # iw1 ( slice(5, 8), AxonSegment(12, 3, 0), 1, 1 << 1, + False, [ AxonCoord(1, 2), AxonCoord(2, 0), @@ -1638,17 +1634,15 @@ class TestData: AxonSegment(12, 3, 0), 2, 1 << 1, - [ - AxonCoord(2 + 0, 0), - AxonCoord(2 + 0, 1), - AxonCoord(2 + 0, 2), - ], + False, + [AxonCoord(2 + 0, i) for i in range(3)], ), ( slice(1, 5), AxonSegment(12, 3, 0), 2, 1 << 2, + False, [ AxonCoord(4 + 0, 1), AxonCoord(4 + 0, 2), @@ -1661,6 +1655,7 @@ class TestData: AxonSegment(12, 3, 0), 4, 1 << 3, + False, [ AxonCoord(24 + 0, 1), AxonCoord(24 + 0, 2), @@ -1674,15 +1669,78 @@ class TestData: AxonSegment(16, 4, 4), 4, 1 << 4, + False, + [AxonCoord(48 + 0, 4 + 3)] + + [AxonCoord(48 + 1, 4 + i) for i in range(4)] + + [AxonCoord(48 + 2, 4 + 0), AxonCoord(48 + 2, 4 + 1)], + ), + # iw8 + ( + slice(5, 8), + AxonSegment(12, 3, 0), + 1, + 1 << 1, + True, [ - AxonCoord(48 + 0, 4 + 3), - AxonCoord(48 + 1, 4 + 0), - AxonCoord(48 + 1, 4 + 1), - AxonCoord(48 + 1, 4 + 2), - AxonCoord(48 + 1, 4 + 3), - AxonCoord(48 + 2, 4 + 0), - AxonCoord(48 + 2, 4 + 1), + AxonCoord(1, 8 * 2), + AxonCoord(2, 8 * 0), + AxonCoord(2, 8 * 1), ], ), + ( + slice(0, 3), + AxonSegment(12, 3, 0), + 2, + 1 << 1, + True, + [AxonCoord(2 + 0, 8 * i) for i in range(3)], + ), + ( + slice(1, 5), + AxonSegment(12, 3, 0), + 2, + 1 << 2, + True, + [ + AxonCoord(4 + 0, 8 * 1), + AxonCoord(4 + 0, 8 * 2), + AxonCoord(4 + 1, 8 * 0), + AxonCoord(4 + 1, 8 * 1), + ], + ), + ( + slice(1, 6), + AxonSegment(12, 3, 0), + 4, + 1 << 3, + True, + [ + AxonCoord(24 + 0, 8 * 1), + AxonCoord(24 + 0, 8 * 2), + AxonCoord(24 + 1, 8 * 0), + AxonCoord(24 + 1, 8 * 1), + AxonCoord(24 + 1, 8 * 2), + ], + ), + ( + slice(5, 15), + AxonSegment(16, 8, 16), + 1, + 1 << 1, + True, + [AxonCoord(0, 8 * (16 + i)) for i in range(5, 8)] + + [AxonCoord(1, 8 * (16 + i)) for i in range(7)], + ), + ( + slice(5, 35), + AxonSegment(40, 10, 10), + 1, + 1 << 2, + True, + [AxonCoord(0, 8 * (10 + i)) for i in range(5, 10)] + + [AxonCoord(1, 8 * (10 + i)) for i in range(10)] + + [AxonCoord(2, 8 * (10 + i)) for i in range(10)] + + [AxonCoord(3, 8 * (10 + i)) for i in range(5)], + ), ], ) diff --git a/tests/backend/test_segment_utils.py b/tests/backend/test_segment_utils.py index 06981f57..2c6bed4d 100644 --- a/tests/backend/test_segment_utils.py +++ b/tests/backend/test_segment_utils.py @@ -119,6 +119,5 @@ def test_get_axon_segments_boundary(axons): TestData.aligned_coords_test_data["args"], TestData.aligned_coords_test_data["data"], ) -def test_aligned_coords(neu_index, axon_seg, delay, n_timeslot, expected): - axon_coords = aligned_coords(neu_index, axon_seg, delay, n_timeslot) - assert axon_coords == expected +def test_aligned_coords(neu_index, axon_seg, delay, n_timeslot, is_iw8, expected): + assert aligned_coords(neu_index, axon_seg, delay, n_timeslot, is_iw8) == expected From 4a0face423e9b826097468deefed3bc2aadcb0c1 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 15 Jul 2024 11:18:05 +0800 Subject: [PATCH 012/187] =?UTF-8?q?=F0=9F=8E=A8=20update=20formats=20&=20c?= =?UTF-8?q?omments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/conf_template.py | 3 ++- paibox/backend/graphs.py | 6 +++--- paibox/backend/mapper.py | 2 +- paibox/backend/routing.py | 6 ++++++ paibox/components/neuron/neurons.py | 4 ++-- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/paibox/backend/conf_template.py b/paibox/backend/conf_template.py index 080f8fba..13ed9dca 100644 --- a/paibox/backend/conf_template.py +++ b/paibox/backend/conf_template.py @@ -1,3 +1,4 @@ +from collections.abc import Sequence import sys from collections import defaultdict from dataclasses import asdict, dataclass @@ -355,7 +356,7 @@ def gen_config_frames_by_coreconf( write_to_file: bool, fp: Path, split_by_chip: bool, - formats: list[str], + formats: Sequence[str], ) -> dict[ChipCoord, list[FrameArrayType]]: """Generate configuration frames by given the `CorePlmConfig`.""" diff --git a/paibox/backend/graphs.py b/paibox/backend/graphs.py index e411f1a2..e41d7dbc 100644 --- a/paibox/backend/graphs.py +++ b/paibox/backend/graphs.py @@ -302,7 +302,7 @@ def _roundup_to_pow2(n: int) -> int: is_optimized = False if optim_nodes == (): - _optim_nodes = reversed(self.ordered_nodes) + _optim_nodes = list(reversed(self.ordered_nodes)) else: _optim_nodes = optim_nodes @@ -613,8 +613,8 @@ def convert2routing_groups( else: succ_cb_gid_dict[succ_cb._routing_id] = [succ_cb] - for succ_cb in succ_cb_gid_dict.values(): - routing_groups.append(RoutingGroup(*succ_cb)) + for v in succ_cb_gid_dict.values(): + routing_groups.append(RoutingGroup(*v)) routing_groups_succ: dict[RoutingGroup, list[RoutingGroup]] = defaultdict(list) diff --git a/paibox/backend/mapper.py b/paibox/backend/mapper.py index fe043164..45b1164a 100644 --- a/paibox/backend/mapper.py +++ b/paibox/backend/mapper.py @@ -455,7 +455,7 @@ def _member_cb_and_onode_config_export(self) -> OutputDestConf: "n4": {...} # as output node #2 } """ - output_dest_info = defaultdict(dict) + output_dest_info: OutputDestConf = defaultdict(dict) # Shallow copy ocoord = copy(_BACKEND_CONTEXT["output_core_addr_start"]) diff --git a/paibox/backend/routing.py b/paibox/backend/routing.py index 91165361..dc0a1507 100644 --- a/paibox/backend/routing.py +++ b/paibox/backend/routing.py @@ -527,9 +527,15 @@ def chip_coord(self) -> ChipCoord: return self[0].chip_coord + def __contains__(self, cb: CoreBlock) -> bool: + return cb in self.core_blocks + def __getitem__(self, idx: int) -> CoreBlock: return self.core_blocks[idx] + def __iter__(self) -> Iterator[CoreBlock]: + return self.core_blocks.__iter__() + @final class RoutingRoot: diff --git a/paibox/components/neuron/neurons.py b/paibox/components/neuron/neurons.py index b1e8b242..e92f01f6 100644 --- a/paibox/components/neuron/neurons.py +++ b/paibox/components/neuron/neurons.py @@ -28,7 +28,7 @@ def __init__( Args: - shape: shape of neurons. - threshold: when the membrane potential exceeds the threshold, neurons will fire. - - reset_v: If not specified, neurons will do soft reset after firing, v - threshold. If \ + - reset_v: if not specified, neurons will do soft reset after firing, v - threshold. If \ specified, neurons will do hard reset after firing, v = reset_v. - neg_threshold: signed negative theshold. If not specified, it will be the smallest \ negative integer allowed by the hardware. @@ -94,7 +94,7 @@ def __init__( - leak_v: the signed leak voltage will be added directly to the membrane potential. - If it is positive, the membrane potential will increase. - If is is negative, the membrane potential will decrease. - - the final leak_v is leak_v + bias (default=0). + - The final leak_v is leak_v + bias (default=0). - bias: if a signed bias is given, it will be added to `leak_v`. The neuron will leak \ before threshold comparison. `leak_v` will also be considered now. - neg_threshold: signed negative theshold. If not specified, it will be the smallest \ From 62891af47f7545fc7d70a3e08fce32321098367e Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 15 Jul 2024 11:18:49 +0800 Subject: [PATCH 013/187] =?UTF-8?q?=F0=9F=94=A7=20allow=20`None`=20as=20`n?= =?UTF-8?q?eg=5Fthreshold`=20to=20use=20default=20minimum=20threshold?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/components/neuron/base.py | 5 ++++- paibox/components/neuron/neurons.py | 16 +++------------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/paibox/components/neuron/base.py b/paibox/components/neuron/base.py index 3fa0576f..07ca9aa3 100644 --- a/paibox/components/neuron/base.py +++ b/paibox/components/neuron/base.py @@ -407,7 +407,7 @@ def __init__( leak_comparison: LCM = LCM.LEAK_BEFORE_COMP, threshold_mask_bits: int = 0, neg_thres_mode: NTM = NTM.MODE_RESET, - neg_threshold: int = NEG_THRES_MIN, + neg_threshold: Optional[int] = None, pos_threshold: int = 1, leak_direction: LDM = LDM.MODE_FORWARD, leak_integration_mode: Union[L[0, 1], bool, LIM] = LIM.MODE_DETERMINISTIC, @@ -426,6 +426,9 @@ def __init__( keep_shape: bool = True, name: Optional[str] = None, ) -> None: + if neg_threshold is None: + neg_threshold = NEG_THRES_MIN + if neg_threshold > 0: # XXX *(-1) if passing a negative threshold > 0 neg_threshold = (-1) * neg_threshold diff --git a/paibox/components/neuron/neurons.py b/paibox/components/neuron/neurons.py index e92f01f6..46b37251 100644 --- a/paibox/components/neuron/neurons.py +++ b/paibox/components/neuron/neurons.py @@ -6,7 +6,7 @@ from paibox.types import DataArrayType, Shape from .base import Neuron -from .utils import LEAK_V_MAX, NEG_THRES_MIN +from .utils import LEAK_V_MAX __all__ = ["IF", "LIF", "TonicSpiking", "PhasicSpiking", "SpikingRelu"] @@ -52,17 +52,12 @@ def __init__( _reset_v = 0 _rm = RM.MODE_LINEAR - if isinstance(neg_threshold, int): - _neg_threshold = neg_threshold - else: - _neg_threshold = NEG_THRES_MIN - super().__init__( shape, reset_mode=_rm, reset_v=_reset_v, neg_thres_mode=NTM.MODE_SATURATION, - neg_threshold=_neg_threshold, + neg_threshold=neg_threshold, pos_threshold=threshold, keep_shape=keep_shape, name=name, @@ -121,17 +116,12 @@ def __init__( # Support passing in bias & leak_v at the same time _leak_v = leak_v + _bias - if isinstance(neg_threshold, int): - _neg_threshold = neg_threshold - else: - _neg_threshold = NEG_THRES_MIN - super().__init__( shape, reset_mode=_rm, reset_v=_reset_v, neg_thres_mode=NTM.MODE_SATURATION, - neg_threshold=_neg_threshold, + neg_threshold=neg_threshold, pos_threshold=threshold, leak_v=_leak_v, keep_shape=keep_shape, From 36ea6af2ff3bfeacbaaed3c363fd389ce2d0b360 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 15 Jul 2024 17:18:05 +0800 Subject: [PATCH 014/187] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20add=20rt=5Fmode?= =?UTF-8?q?=20for=20ANN=20deployment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/types.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/paibox/backend/types.py b/paibox/backend/types.py index 4cc35fa2..d2fa08cc 100644 --- a/paibox/backend/types.py +++ b/paibox/backend/types.py @@ -45,7 +45,12 @@ SourceNodeType: TypeAlias = NodeType DestNodeType: TypeAlias = Neuron -WeightRamType: TypeAlias = NDArray[np.uint64] # uint64 weights mapped in weight RAM +WRAM_UNPACKED_DTYPE = np.uint8 +WRAM_PACKED_DTYPE = np.uint64 +# Type of unpacked weight in WRAM +WRAMUnpackedType: TypeAlias = NDArray[WRAM_UNPACKED_DTYPE] +# Type of packed weight in WRAM +WRAMPackedType: TypeAlias = NDArray[WRAM_PACKED_DTYPE] _COORD_UNSET = 0 _DEGREE_UNSET = -1 @@ -90,6 +95,7 @@ class EdgeAttr(NamedTuple): class PartitionedEdges(NamedTuple): edges: set[EdgeType] rg_id: int + rt_mode: CoreMode = CoreMode.MODE_SNN # XXX Temp solution NeuSlice: TypeAlias = slice @@ -156,7 +162,7 @@ class AxonSegment(NamedTuple): class CoreAbstract(PAIBoxObject, ABC): """Abstract core class.""" - runtime_mode: CoreMode + rt_mode: CoreMode @property @abstractmethod @@ -166,4 +172,15 @@ def n_core_required(self) -> int: @classmethod @abstractmethod - def build(cls): ... + def build(cls, *args, **kwargs): ... + + +if hasattr(CoreMode, "is_iw8"): + + def is_iw8(mode: CoreMode) -> bool: + return mode.is_iw8 # type: ignore + +else: + + def is_iw8(mode: CoreMode) -> bool: + return mode is CoreMode.MODE_ANN_TO_BANN_OR_SNN or mode is CoreMode.MODE_ANN From bb666bf1092b117458bc4ddfd31a30644764458a Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Tue, 16 Jul 2024 23:38:37 +0800 Subject: [PATCH 015/187] =?UTF-8?q?=F0=9F=92=A5=20support=20ANN=20mode=20o?= =?UTF-8?q?f=20the=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/graphs.py | 21 +- paibox/backend/mapper.py | 7 +- paibox/backend/placement.py | 395 ++++++++++++++++++++---------------- 3 files changed, 241 insertions(+), 182 deletions(-) diff --git a/paibox/backend/graphs.py b/paibox/backend/graphs.py index e41d7dbc..d6db0e1b 100644 --- a/paibox/backend/graphs.py +++ b/paibox/backend/graphs.py @@ -14,7 +14,7 @@ from .constrs import GraphNodeConstrs from .context import _BACKEND_CONTEXT -from .placement import CoreBlock, neuron_repl_prop +from .placement import CoreBlock from .routing import RoutingGroup from .segment_utils import get_neu_segments from .types import * @@ -215,6 +215,8 @@ def graph_partition(self) -> list[PartitionedEdges]: and the edges connected to these partitioned nodes will be returned as a set. Return: a list of partitioned edges & a list of routing groups id. + + TODO constraints in partitioning: iw, sw, snn_en, tws, twe, pool_max_en. """ self.build_check() @@ -264,6 +266,10 @@ def graph_partition(self) -> list[PartitionedEdges]: succ_nodes_set.update(self._raw_nodes[n] for n in self.succ_dg[_node]) succ_nodes_lst: list[NodeType] = list(succ_nodes_set) + mode = succ_nodes_lst[0].mode + if any(mode != node.mode for node in succ_nodes_lst): + raise NotSupportedError("mixed mode is not supported.") + idx_of_sg = GraphNodeConstrs.tick_wait_attr_constr(succ_nodes_lst) if len(idx_of_sg) > 0: @@ -274,10 +280,9 @@ def graph_partition(self) -> list[PartitionedEdges]: e.edge for e in self.pred_dg[succ_nodes_lst[i].name].values() ) - gh_parts.append(PartitionedEdges(succ_edges_sg, rgid)) - + gh_parts.append(PartitionedEdges(succ_edges_sg, rgid, rt_mode=mode)) else: - gh_parts.append(PartitionedEdges(succ_edges_set, rgid)) + gh_parts.append(PartitionedEdges(succ_edges_set, rgid, rt_mode=mode)) rgid += 1 @@ -337,8 +342,8 @@ def _roundup_to_pow2(n: int) -> int: n_core_required_after_copy = len( get_neu_segments( pred_cb_dest, - pred_cb.neuron_capacity, - neuron_repl_prop(pred_cb.n_weight_bits, pred_cb.n_timeslot), + pred_cb.n_fanout, + pred_cb.n_neuron_repl, _BACKEND_CONTEXT.cflags["grouping_optim_target"], ) ) @@ -353,8 +358,8 @@ def _roundup_to_pow2(n: int) -> int: n_core_after_split[i] = len( get_neu_segments( dest, # type: ignore - succ_cb.neuron_capacity, - neuron_repl_prop(succ_cb.n_weight_bits, succ_cb.n_timeslot), + succ_cb.n_fanout, + succ_cb.n_neuron_repl, _BACKEND_CONTEXT.cflags["grouping_optim_target"], ) ) diff --git a/paibox/backend/mapper.py b/paibox/backend/mapper.py index 45b1164a..4f424b68 100644 --- a/paibox/backend/mapper.py +++ b/paibox/backend/mapper.py @@ -36,7 +36,7 @@ ) from .placement import CoreBlock, aligned_coords, max_lcn_of_cb from .routing import RoutingGroup, RoutingRoot -from .types import NeuSegment, NodeDegree, NodeType, SourceNodeType +from .types import NeuSegment, NodeDegree, NodeType, SourceNodeType, is_iw8 __all__ = ["Mapper"] @@ -217,7 +217,9 @@ def build_core_blocks(self) -> None: for part in partitioned_edges: self.core_blocks.append( - CoreBlock.build(*part.edges, seed=0, routing_id=part.rg_id) + CoreBlock.build( + *part.edges, routing_id=part.rg_id, rt_mode=part.rt_mode + ) ) for cur_cb in self.core_blocks: @@ -411,6 +413,7 @@ def _inpproj_config_export(self) -> InputNodeConf: input_cb.axon_segments[inode], 1, input_cb.n_timeslot, + is_iw8(input_cb.rt_mode), ) inp_neuron_dest = InputNeuronDest( diff --git a/paibox/backend/placement.py b/paibox/backend/placement.py index 809b3eb8..f5893f88 100644 --- a/paibox/backend/placement.py +++ b/paibox/backend/placement.py @@ -7,9 +7,14 @@ from paicorelib import WeightPrecision as WP from paibox.components import FullConnectedSyn, Neuron -from paibox.exceptions import GraphBuildError, ResourceError, TruncationWarning +from paibox.exceptions import ( + GraphBuildError, + NotSupportedError, + ResourceError, + TruncationWarning, +) from paibox.types import WeightType, WEIGHT_DTYPE -from paibox.utils import check_attr_same, count_unique_elem +from paibox.utils import check_attr_same from .conf_template import ( CoreConfig, @@ -22,6 +27,8 @@ from .segment_utils import aligned_coords, get_axon_segments, get_neu_segments from .types import ( _COORD_UNSET, + WRAM_PACKED_DTYPE, + WRAM_UNPACKED_DTYPE, AxonCoord, AxonSegment, CoreAbstract, @@ -30,11 +37,36 @@ NeuSegOfCoreBlock, NeuSegOfCorePlm, SourceNodeType, - WeightRamType, + WRAMPackedType, + WRAMUnpackedType, + is_iw8, ) class CoreBlock(CoreAbstract): + + _parents: tuple[FullConnectedSyn, ...] + _routing_id: int + seed: int + """Random seed, legal integer, no more than uint64.""" + _lcn_ex: LCN_EX + _lcn_locked: bool + """Indicate whether `lcn_ex` has been adjusted & locked.""" + target_lcn: LCN_EX + """The target(destination core block) LCN.""" + chip_coord: ChipCoord + """A core block must be placed on a chip.""" + core_coords: list[Coord] + """Assigned core coordinates.""" + core_placements: dict[Coord, "CorePlacement"] + """Core placements.""" + axon_segments: dict[SourceNodeType, AxonSegment] = dict() + """A dictionary of segments of each axon(source node).""" + neuron_segs_of_cb: NeuSegOfCoreBlock = [] + """Neuron segments in the core block. Each element in the list represents the neuron \ + segments in core placement. + """ + def __init__( self, *parents: FullConnectedSyn, @@ -54,38 +86,18 @@ def __init__( """ super().__init__(name) self._parents = parents - self._wp = WP.WEIGHT_WIDTH_8BIT # default value self._routing_id = routing_id - self.runtime_mode = mode - - self._lcn_ex = self._n_axon2lcn_ex() - + self.rt_mode = mode self.seed = seed - """Random seed, legal integer, no more than uint64.""" + self._lcn_ex = self._n_axon2lcn_ex() self.target_lcn = LCN_EX.LCN_1X - """The target(destination core block) LCN.""" - self._lcn_locked = False - """Used to indicate whether `lcn_ex` has been adjusted.""" - - self.core_coords: list[Coord] = list() - """Assigned core coordinates.""" - - self.chip_coord: ChipCoord = Coord(_COORD_UNSET, _COORD_UNSET) - """A core block must be placed on a chip.""" - - self.core_placements: dict[Coord, CorePlacement] = dict() - """Core placements.""" - - # Segment the group of axons. - self.axon_segments: dict[SourceNodeType, AxonSegment] = dict() - """A dictionary of segments of each axon(source node).""" - - self.neuron_segs_of_cb: NeuSegOfCoreBlock = [] - """Neuron segments in the core block. Each element in the list \ - represents the neuron segments in core placement(physical core). - """ + self.core_coords = [] + self.chip_coord = Coord(_COORD_UNSET, _COORD_UNSET) + self.core_placements = dict() + self.axon_segments = dict() + self.neuron_segs_of_cb = [] def group_neurons( self, optim_target: Literal["latency", "core", "both"] = "both" @@ -95,10 +107,7 @@ def group_neurons( raise GraphBuildError("group the neurons after 'lcn_ex' is locked.") self.neuron_segs_of_cb = get_neu_segments( - self.dest, - self.neuron_capacity, - neuron_repl_prop(self.n_weight_bits, self.n_timeslot), - optim_target, + self.dest, self.n_fanout, self.n_neuron_repl, optim_target ) def core_plm_alloc(self) -> None: @@ -130,13 +139,13 @@ def _n_axon2lcn_ex(self) -> LCN_EX: ) if ( - lcn := int((self.n_axon - 1) // self.n_fanin_max).bit_length() + lcn := ((self.n_axon - 1) // self.n_fanin_base).bit_length() ) > LCN_EX.LCN_64X: - _max_n_axons = self.n_fanin_max * (1 << LCN_EX.LCN_64X) + _max_n_axons = self.n_fanin_base << LCN_EX.LCN_64X raise ResourceError( - f"required LCN extension out of range {LCN_EX.LCN_64X} ({lcn}). " - f"The number of axons must be <= {_max_n_axons}. " - f"But synapses {self._obj_repr()} have a total of {self.n_axon} axons." + f"required LCN out of range {LCN_EX.LCN_64X} ({lcn}). The number of axons " + f"must be <= {_max_n_axons}, but synapses {self._obj_repr} have a total of " + f"{self.n_axon} axons." ) return LCN_EX(lcn) @@ -144,15 +153,13 @@ def _n_axon2lcn_ex(self) -> LCN_EX: def copy(self): raise NotImplementedError - """Interfaces""" - @property def obj(self) -> tuple[FullConnectedSyn, ...]: return self._parents @property def shape(self) -> tuple[int, int]: - return (count_unique_elem(self.source), count_unique_elem(self.dest)) + return (len(self.source), len(self.dest)) @property def source(self) -> list[SourceNodeType]: @@ -175,20 +182,12 @@ def n_axon_of(self, index: int) -> int: """Boundary limitations""" @property - def neuron_capacity(self) -> int: - """Neuron capacity. #N of valid dendrites/#N of dendrites required per neuron. - - FIXME This method ONLY works in SNN runtime_mode. For ANN runtime_mode, use table lookup? - """ - return (self.n_dendrite_max >> self.lcn_ex) // self.n_dendrite_per_neuron - - @property - def n_fanin_max(self) -> int: - """Maximum #N of fan-in per dendrite.""" + def n_fanin_base(self) -> int: + """The fan-in of cores.""" return ( - HwConfig.N_FANIN_PER_DENDRITE_ANN - if self.runtime_mode is CoreMode.MODE_ANN - else HwConfig.N_FANIN_PER_DENDRITE_SNN + HwConfig.N_FANIN_PER_DENDRITE_SNN + if self.rt_mode.is_snn + else HwConfig.N_FANIN_PER_DENDRITE_ANN ) @property @@ -200,18 +199,10 @@ def weight_precision(self) -> WP: # Optimized in `s.weight_precision`. return max(s.weight_precision for s in self.obj) - @property - def n_dendrite_per_neuron(self) -> int: - """Multiple dendrites will be combined to achieve higher precision weights. - - FIXME The limit on the number of dendrites in SNN/ANN modes is different, which affects \ - the capacity of neurons in physical core. - """ - return 1 << self.weight_precision - @property def n_weight_bits(self) -> int: - return self.n_dendrite_per_neuron + """Multiple dendrites will be combined to achieve higher precision weights.""" + return 1 << self.weight_precision @property def lcn_ex(self) -> LCN_EX: @@ -219,10 +210,9 @@ def lcn_ex(self) -> LCN_EX: @lcn_ex.setter def lcn_ex(self, lcn_ex: LCN_EX) -> None: - """Set or adjust the `lcn_ex` & lock.""" if lcn_ex > LCN_EX.LCN_64X: raise ResourceError( - f"required LCN extension out of range {LCN_EX.LCN_64X} ({lcn_ex})." + f"required LCN out of range {LCN_EX.LCN_64X} ({lcn_ex})." ) self._lcn_ex = lcn_ex @@ -232,12 +222,17 @@ def lcn_ex(self, lcn_ex: LCN_EX) -> None: def n_timeslot(self) -> int: return 1 << self.lcn_ex + @property + def dendrite_comb_rate(self) -> int: + """#N of dendrites will be combined.""" + return self.lcn_ex + self.weight_precision + @property def tws(self) -> int: """Attribute `tick_wait_start`.""" if not check_attr_same(self.dest, "tick_wait_start"): raise AttributeError( - "Attribute 'tick_wait_start' of the core block are not equal." + "attribute 'tick_wait_start' of the core block are not equal." ) return self.dest[0].tick_wait_start @@ -247,7 +242,7 @@ def twe(self) -> int: """Attribute `tick_wait_end.`""" if not check_attr_same(self.dest, "tick_wait_end"): raise AttributeError( - "Attribute 'tick_wait_end' of the core block are not equal." + "attribute 'tick_wait_end' of the core block are not equal." ) return self.dest[0].tick_wait_end @@ -257,11 +252,12 @@ def n_axon(self) -> int: return sum(s.num_out for s in self.axons) @property - def n_dendrite_max(self) -> int: + def n_fanout(self) -> int: + """The fan-out of cores.""" return ( - HwConfig.N_DENDRITE_MAX_ANN - if self.runtime_mode is CoreMode.MODE_ANN - else HwConfig.N_DENDRITE_MAX_SNN + HwConfig.N_DENDRITE_MAX_SNN >> self.dendrite_comb_rate + if self.rt_mode.is_snn + else FANOUT_IW8[self.dendrite_comb_rate] ) @property @@ -274,10 +270,7 @@ def unrolling_factor(self) -> list[int]: @property def n_neuron_of_plm(self) -> list[int]: - """A list of the #N of neurons on each `CorePlacement`. - - FIXME Different in SNN/ANN runtime_mode. - """ + """A list of the #N of neurons on each `CorePlacement`.""" if len(self.core_coords) == 0: raise GraphBuildError("do this after coordinates assignment.") @@ -295,7 +288,7 @@ def group_axons(self) -> None: raise GraphBuildError("get axon segments after 'lcn_ex' is locked.") self.axon_segments = get_axon_segments( - self.axons, self.n_timeslot, self.n_fanin_max + self.axons, self.n_timeslot, self.n_fanin_base ) @cached_property @@ -340,6 +333,20 @@ def get_raw_weight_of_coord(self, idx: int) -> list[WeightType]: return w_of_neu_segs + @property + def n_neuron_repl(self) -> int: + """The number of neurons that need to be repeatedly placed into NRAM. + + For example, in SNN mode, N[0:3] with LCN_2X & WP8: + NRAM [0] [1] ... [15] [16] [17] ... [31] ... + N[0] N[0] ... N[0] N[1] N[1] ... N[1] ... + + But at 8-bit input width, neurons don't need to be replicated. + NRAM [0] [1] ... [15] [16] ... + N[0] N[1] ... N[15] N[16] ... + """ + return 1 << self.dendrite_comb_rate if self.rt_mode.is_snn else 1 + def __len__(self) -> int: return self.n_core_required @@ -349,21 +356,27 @@ def __repr__(self) -> str: def __str__(self) -> str: return f"<{self.name} of target '{self.obj}'>" + @property def _obj_repr(self) -> str: """The representation of the names of target objects.""" return ", ".join(n.name for n in self.obj) @classmethod - def build(cls, *synapses: FullConnectedSyn, routing_id: int, seed: int = 0): + def build( + cls, + *synapses: FullConnectedSyn, + routing_id: int, + rt_mode: CoreMode, + seed: int = 0, + ): """Group synapses & build `CoreBlock`.""" - # FIXME where does the parameter check do? if seed > (1 << 64) - 1: warnings.warn( f"random seed {seed} is too large, truncated into 64 bits.", TruncationWarning, ) - return cls(*synapses, routing_id=routing_id, seed=seed) + return cls(*synapses, routing_id=routing_id, mode=rt_mode, seed=seed) @classmethod def export_core_plm_config(cls, cb: "CoreBlock") -> CoreConfInChip: @@ -377,13 +390,17 @@ def export_core_plm_config(cls, cb: "CoreBlock") -> CoreConfInChip: class CorePlacement(CoreAbstract): - """The divided synapse placed on a single CORE.""" - - WEIGHT_RAM_SHAPE: ClassVar[tuple[int, int]] = ( - HwConfig.N_FANIN_PER_DENDRITE_SNN, - HwConfig.N_DENDRITE_MAX_SNN, - ) - """SNN mode ONLY.""" + parent: CoreBlock + coord: Coord + """Routing coordinate""" + n_neuron: int + raw_weights: list[WeightType] + """The folded weights.""" + neu_segs_of_cplm: NeuSegOfCorePlm + neu_configs: dict[Neuron, NeuronConfig] + + # FIXME Change to HwConfig.ADDR_AXON_MAX(1152) once it is fixed. + WRAM_BASE_SHAPE: ClassVar[tuple[int, int]] = (1152, HwConfig.ADDR_RAM_MAX) def __init__( self, @@ -403,18 +420,13 @@ def __init__( - neu_segs_of_cplm: The segment of the neurons in the physical core. """ super().__init__(name) - self.parent = parent + self.rt_mode = parent.rt_mode self.coord = routing_coord - """Routing coordinate""" - self.n_neuron = n_neuron - - self._weights_folded = self._fold_raw_weights(raw_weights) - """The folded weights.""" - + self.raw_weights = raw_weights self.neu_segs_of_cplm = neu_segs_of_cplm - self.neu_configs: dict[Neuron, NeuronConfig] = dict() + self.neu_configs = dict() @classmethod def build(cls, parent: CoreBlock, idx: int): @@ -429,12 +441,15 @@ def _fold_raw_weights(self, raw_weights: list[WeightType]) -> WeightType: """Fold the weights into LCN-sized blocks.""" w_folded_list = [] w_folded_of_axon_segs = [] - n_fold = self.n_timeslot + # See the note of function `_weight_ram_mapping` below. + n_fold = ( + self.n_timeslot + if self.rt_mode.is_snn + else 1 << (self.dendrite_comb_rate - 3) + ) if self.lcn_ex == LCN_EX.LCN_1X: - w_folded = np.hstack(raw_weights) - w_folded.setflags(write=False) - return w_folded + return np.hstack(raw_weights) # LCN_EX > LCN_1X for raw_weight in raw_weights: @@ -459,24 +474,46 @@ def _fold_raw_weights(self, raw_weights: list[WeightType]) -> WeightType: w_folded = np.vstack(w_folded_of_axon_segs) w_folded_list.append(w_folded) - w_folded = np.hstack(w_folded_list) - w_folded.setflags(write=False) - return w_folded + return np.hstack(w_folded_list) + + def _weight_ram_mapping(self) -> WRAMPackedType: + """Map the raw weights to the weight RAM(WRAM). The mapping is different for both input widths. + + NOTE: When the input width is 8 bits, no neurons need to be mapped to the WRAM when the combination rate of \ + dentrites >= 8, while some neurons need to be mapped to the WRAM when < 8. + + When the input width is 8 bits and with the combination rate of dentrites > 3, the mapping of weights \ + becomes the key to limiting neuron capacity. In this case, if the weight accuracy is less than 8 bits \ + (which may also occur when the weight accuracy is optimized), the weight cannot be folded directly in \ + the fan-in expansion direction, otherwise the column of the WRAM will exceed the upper limit(512). \ + + A portion of the fan-in needs to be expanded to an unfilled portion in the direction of the weight \ + accuracy. At this point, n_fold=n_timeslot/(8/n_weight_bits)=2^(dendrite_comb_rate - 3). For example, \ + for LCN_8X & WP8, the n_fold is 3. For LCN_32X & WP4, the n_fold is 4 (instead of 5). + + TODO Now, in ANN mode, only the mapping of 8-bit weights is supported. The weight accuracy optimization is \ + supposed to disable manually for now. + """ + if not self.rt_mode.is_snn and self.weight_precision < WP.WEIGHT_WIDTH_8BIT: + raise NotSupportedError("only support 8-bit weights in ANN mode.") - def _weight_ram_mapping(self) -> WeightRamType: - row, col = self._weights_folded.shape - w_unpacked = np.zeros(self.WEIGHT_RAM_SHAPE, dtype=np.uint8) + _weights_folded = self._fold_raw_weights(self.raw_weights) + row, col = _weights_folded.shape + # The 1152*512 unpacked weight + w_unpacked = np.zeros(self.WRAM_BASE_SHAPE, dtype=WRAM_UNPACKED_DTYPE) if self.n_weight_bits == 1: - w_unpacked[:row, :col] = self._weights_folded + w_unpacked[:row, :col] = _weights_folded else: - # (N, M) -> (M*N, 1) - w_folded_3d = np.expand_dims(self._weights_folded.T, axis=2).astype( - np.uint8 + # (N, M)(int8) -> (M, N, 1)(uint8) + w_folded_3d = np.expand_dims(_weights_folded.T, axis=2).astype( + WRAM_UNPACKED_DTYPE ) + _n_group_bit = HwConfig.N_FANIN_PER_DENDRITE_ANN + for i in range(col): - # For every column, unpack the array [N*1] -> [N*n_weight_bits] + # For every column, unpack the array (N, 1) -> (N, n_weight_bits) unpacked = np.unpackbits( w_folded_3d[i], axis=1, @@ -484,28 +521,18 @@ def _weight_ram_mapping(self) -> WeightRamType: bitorder=HwConfig.WEIGHT_BITORDER, ) - w_unpacked[ - :row, self.n_weight_bits * i : self.n_weight_bits * (i + 1) - ] = unpacked - - assert np.max(w_unpacked, axis=None) <= np.uint8(1) - assert np.min(w_unpacked, axis=None) >= np.uint8(0) - - # Convert the unpacked weights into a mapping format, - # corresponding to the RAM address, each address contains 18 uint64. - # (1152, 512) -> (512, 1152) -> (512*18, 64)(uint8). - # Reshape to 64 columns to avoid contiguous problem. - w_unpacked_T_rehaped = w_unpacked.T.reshape(-1, 64) - - # (512*18, 64)(uint8) -> (512*18, 8)(uint8) - w_packed_u8 = np.packbits( - w_unpacked_T_rehaped, axis=1, bitorder=HwConfig.WEIGHT_BITORDER - ) - # (512*18, 8)(uint8) -> (512*18, 1)(uint64) -> (512, 18)(uint64) - w_packed_u64 = w_packed_u8.view(np.uint64).reshape(-1, 18) - w_packed_u64.setflags(write=False) + if self.rt_mode.is_snn: + w_unpacked[ + :row, self.n_weight_bits * i : self.n_weight_bits * (i + 1) + ] = unpacked + else: + # In the case of 8-bit input width, the weights are mapped differently + for bit in range(self.n_weight_bits): + w_unpacked[bit * _n_group_bit : bit * _n_group_bit + row, i] = ( + unpacked[:, bit] + ) - return w_packed_u64 + return self._weight_pack(w_unpacked) @staticmethod def _nfold_weight( @@ -522,23 +549,15 @@ def _nfold_weight( if raw_row % n_fold > 0: n_row_padding = n_fold - raw_row % n_fold - - # Check #1 - # assert expected_row * n_fold == raw_row + n_row_padding - _raw_weight = np.append( raw_weight, np.zeros((n_row_padding, raw_col), dtype=WEIGHT_DTYPE), axis=0, ) else: - _raw_weight = raw_weight.copy() + _raw_weight = raw_weight w_splited = np.vsplit(_raw_weight, n_fold) - - # Check #2 - # assert _raw_weight.shape[0] == expected_row * n_fold - w_folded = np.zeros((expected_row, raw_col * n_fold), dtype=WEIGHT_DTYPE) for i, j in np.ndindex((n_fold, raw_col)): @@ -547,8 +566,30 @@ def _nfold_weight( return w_folded + @staticmethod + def _weight_pack(w_unpacked: WRAMUnpackedType) -> WRAMPackedType: + """Convert the unpacked weights into a mapping format, corresponding to the WRAM address, each address \ + contains 18 uint64. + (1152, 512) -> T -> (512*18, 64) -> (512*18, 8) uint8 -> (512*18, 1) uint64 -> (512, 18) uint64. + """ + _n_bit_packed = WRAM_PACKED_DTYPE(1).nbytes * 8 # #N bit of packed dtype + # #N of u64 on each NRAM address + _n_u64_naddr = CorePlacement.WRAM_BASE_SHAPE[0] // _n_bit_packed + + # Reshape to 64 columns to avoid contiguous problem. + w_unpacked_aligned = w_unpacked.T.reshape(-1, _n_bit_packed) + # (512*18, 64) uint8 -> (512*18, 8) uint8 + w_packed_u8 = np.packbits( + w_unpacked_aligned, axis=1, bitorder=HwConfig.WEIGHT_BITORDER + ) + # (512*18, 8) uint8 -> (512*18, 1) uint64 -> (512, 18) uint64 + w_packed_u64 = w_packed_u8.view(WRAM_PACKED_DTYPE).reshape(-1, _n_u64_naddr) + w_packed_u64.setflags(write=False) + + return w_packed_u64 + def export_param_config(self) -> CoreConfig: - _mode_params = self.mode.conf + _mode_params = self.rt_mode.conf # fmt: off cb_config = CoreConfig( @@ -557,7 +598,7 @@ def export_param_config(self) -> CoreConfig: self.lcn_ex, # lcn_extension _mode_params[0], # input_width_format _mode_params[1], # spike_width_format - self.n_dendrite, # num_dendrite + self.n_working_dendrite, # num_dendrite MaxPoolingEnable.DISABLE, # max_pooling_en self.tws, # tick_wait_start self.twe, # tick_wait_end @@ -596,6 +637,7 @@ def export_neu_config( axon_dests[0].axon_segments[neu_seg.target], neu_seg.target.delay_relative, axon_dests[0].n_timeslot, + is_iw8(axon_dests[0].rt_mode), ) # Get all core coordinates and replication ids. @@ -610,6 +652,7 @@ def export_neu_config( ) self.neu_configs[neu_seg.target] = config + return None else: # neu_seg is a part of an output node assert isinstance(output_core_coord, Coord) @@ -639,14 +682,10 @@ def export_core_plm_config(self) -> CorePlmConfig: self.parent.seed, self.weight_ram, core_param, self.neu_configs ) - @property - def mode(self) -> CoreMode: - return self.parent.runtime_mode - @property def shape(self) -> tuple[int, int]: - return (count_unique_elem(self.source), count_unique_elem(self.dest)) - + return (len(self.source), len(self.dest)) + @property def weight_precision(self) -> WP: return self.parent.weight_precision @@ -671,6 +710,10 @@ def lcn_ex(self) -> LCN_EX: def target_lcn(self) -> LCN_EX: return self.parent.target_lcn + @property + def dendrite_comb_rate(self) -> int: + return self.parent.dendrite_comb_rate + @property def tws(self) -> int: return self.parent.tws @@ -680,15 +723,20 @@ def twe(self) -> int: return self.parent.twe @property - def n_dendrite(self) -> int: - return self.n_neuron * neuron_repl_prop(self.n_weight_bits, self.n_timeslot) + def n_working_dendrite(self) -> int: + """The number of actual working dendrites. IN ANN mode, the number of working \ + dendrites N <= 4096. In SNN mode, N <= 512. + + NOTE: n_neuron * (2^comb_rate) = n_neuron << comb_rate + """ + return self.n_neuron << self.dendrite_comb_rate @property def source(self) -> list[SourceNodeType]: return self.parent.source @property - def dest(self): + def dest(self) -> list[DestNodeType]: """The destination nodes within it. NOTE: This attribute is different from the one of its parent. @@ -696,7 +744,7 @@ def dest(self): return [p.target for p in self.neu_segs_of_cplm] @property - def weight_ram(self) -> WeightRamType: + def weight_ram(self) -> WRAMPackedType: return self._weight_ram_mapping() @property @@ -710,13 +758,6 @@ def __len__(self) -> int: class EmptyCorePlacement(CoreAbstract): """Empty core placement.""" - _default_wp: ClassVar[WP] = WP.WEIGHT_WIDTH_1BIT - _default_lcn_ex: ClassVar[LCN_EX] = LCN_EX.LCN_1X - _default_n_dendrite: ClassVar[int] = 0 - _default_tws: ClassVar[int] = 0 - _default_twe: ClassVar[int] = 0 - _default_target_lcn: ClassVar[LCN_EX] = LCN_EX.LCN_1X - def __init__(self, coord: Coord, name: Optional[str] = None) -> None: super().__init__(name) self.coord = coord @@ -727,16 +768,16 @@ def export_param_config(self) -> CoreConfig: # fmt: off cb_config = CoreConfig( self.name, # name of the core - self._default_wp, # weight_precision - self._default_lcn_ex, # lcn_extension + WP.WEIGHT_WIDTH_1BIT, # weight_precision + LCN_EX.LCN_1X, # lcn_extension _mode_params[0], # input_width_format _mode_params[1], # spike_width_format - self._default_n_dendrite, # num_dendrite + 0, # num_dendrite MaxPoolingEnable.DISABLE, # max_pooling_en - self._default_tws, # tick_wait_start - self._default_twe, # tick_wait_end + 0, # tick_wait_start + 0, # tick_wait_end _mode_params[2], # snn_mode_en - self._default_target_lcn, # target_lcn + LCN_EX.LCN_1X, # target_lcn _BACKEND_CONTEXT.test_chip_addr, # test_chip_addr ) # fmt: on @@ -760,9 +801,19 @@ def max_lcn_of_cb(cb: list[CoreBlock]) -> LCN_EX: return max(cb, key=lambda cb: cb.lcn_ex).lcn_ex -def neuron_repl_prop(nbits: int, ntimeslot: int) -> int: - """Get the proportion of neuron replication. - - scale = nbits(1 << wp) * n_timeslot(1 << lcn_ex) - """ - return nbits * ntimeslot +if hasattr(HwConfig, "FANOUT_IW8"): + FANOUT_IW8 = HwConfig.FANOUT_IW8 # type: ignore +else: + # Get the fan-out by the combination rate of dendrites + FANOUT_IW8: list[int] = [ + HwConfig.N_NEURON_MAX_ANN, + 1364, + 876, + 512, + 256, + 128, + 64, + 32, + 16, + 8, + ] From e1631aaf37c939f075bb837da5c772d26665d4ad Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Wed, 17 Jul 2024 19:46:58 +0800 Subject: [PATCH 016/187] =?UTF-8?q?=F0=9F=90=9B=20update=20`=5F=5Fdeepcopy?= =?UTF-8?q?=5F=5F`=20methods=20to=20accept=20`memo`=20parameter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/types.py | 2 +- paibox/components/neuron/base.py | 2 +- paibox/components/synapses/base.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/paibox/backend/types.py b/paibox/backend/types.py index d2fa08cc..3eaf594e 100644 --- a/paibox/backend/types.py +++ b/paibox/backend/types.py @@ -74,7 +74,7 @@ class NodeDegree: def __copy__(self) -> "NodeDegree": return self.__deepcopy__() - def __deepcopy__(self) -> "NodeDegree": + def __deepcopy__(self, memo=None) -> "NodeDegree": return NodeDegree(self.in_degree, self.out_degree) def copy(self) -> "NodeDegree": diff --git a/paibox/components/neuron/base.py b/paibox/components/neuron/base.py index 07ca9aa3..67f87509 100644 --- a/paibox/components/neuron/base.py +++ b/paibox/components/neuron/base.py @@ -514,7 +514,7 @@ def __copy__(self) -> "Neuron": """Same as `__deepcopy__`.""" return self.__deepcopy__() - def __deepcopy__(self) -> "Neuron": + def __deepcopy__(self, memo=None) -> "Neuron": """Deepcopy a neuron. NOTE: It simply reinitializes a neuron with the parameters of the original neuron. diff --git a/paibox/components/synapses/base.py b/paibox/components/synapses/base.py index 4163cdca..fe3f75a4 100644 --- a/paibox/components/synapses/base.py +++ b/paibox/components/synapses/base.py @@ -89,7 +89,7 @@ def reset_state(self, *args, **kwargs) -> None: def __copy__(self) -> "FullConnSyn": return self.__deepcopy__() - def __deepcopy__(self) -> "FullConnSyn": + def __deepcopy__(self, memo=None) -> "FullConnSyn": self._n_copied += 1 return FullConnSyn( From d94c206ab98143705fca11c5a310fce7722e62a9 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Wed, 17 Jul 2024 20:25:34 +0800 Subject: [PATCH 017/187] =?UTF-8?q?=E2=9C=A8=20modified=20the=20logic=20fo?= =?UTF-8?q?r=20generating=20cfg=20frames=20III=20&=20IV?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/conf_template.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/paibox/backend/conf_template.py b/paibox/backend/conf_template.py index 13ed9dca..fe0653ad 100644 --- a/paibox/backend/conf_template.py +++ b/paibox/backend/conf_template.py @@ -385,15 +385,29 @@ def _write_to_f(name: str, array: FrameArrayType) -> None: ) # 3. Iterate all the neuron segments inside the physical core. + # FIXME Unfortunately, at present, only the corresponding NRAM can be written based on + # the neuron configurations, and it cannot handle the case where the NRAM address is >= 512, + # that is, some neurons need to occupy the NRAM, which is inconsistent with the current logic. + # Additional neuron configurations has been written to the NRAM within the CorePlacement. + # NOTE The meaning of 'n_neuron' in function 'gen_config_frame3' is the number of neurons in + # the NRAM. See notes of function '_weight_ram_mapping' of `CorePlacement` in file + # backend/placement.py for details. config_frame_type3 = [] for neu_conf in v.neuron_configs.values(): + # The actual number of neurons placed in NRAM. + _n_neuron_nram = ( + HwConfig.ADDR_RAM_MAX + 1 + if neu_conf.n_neuron > HwConfig.ADDR_RAM_MAX + 1 + else neu_conf.n_neuron + ) + config_frame_type3.append( OfflineFrameGen.gen_config_frame3( chip_coord, core_coord, _RID_UNSET, neu_conf.addr_offset, - neu_conf.n_neuron, + _n_neuron_nram, neu_conf.neuron_attrs, neu_conf.neuron_dest_info, lcn_ex=v.params_reg.lcn_extension, @@ -411,15 +425,16 @@ def _write_to_f(name: str, array: FrameArrayType) -> None: frame3 = np.array([], dtype=FRAME_DTYPE) # 4. Only one config frame type IV for each physical core. - n_addr_write = v.params_reg.num_dendrite # The number of address to write - if n_addr_write > 0: + # NOTE To avoid logical complications, write the entire weights to the WRAM, rather than just the + # valid partial weights, because there are still some neurons configurations in the WRAM. + if v.params_reg.num_dendrite > 0: config_frame_type4 = OfflineFrameGen.gen_config_frame4( chip_coord, core_coord, _RID_UNSET, 0, - 18 * n_addr_write, - v.weight_ram[:n_addr_write], + 18 * (HwConfig.ADDR_RAM_MAX + 1), + v.weight_ram[: HwConfig.ADDR_RAM_MAX + 1], ) else: config_frame_type4 = None From d8bc5fda317ab21f94d812fea5be28bb679cc8e7 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Wed, 17 Jul 2024 23:01:11 +0800 Subject: [PATCH 018/187] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20optimize=20empty?= =?UTF-8?q?=20core=20placement=20cfg=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/conf_template.py | 41 ++++++++++----------------------- paibox/backend/placement.py | 15 +++++------- tests/backend/conftest.py | 6 ----- 3 files changed, 18 insertions(+), 44 deletions(-) diff --git a/paibox/backend/conf_template.py b/paibox/backend/conf_template.py index fe0653ad..34276c8d 100644 --- a/paibox/backend/conf_template.py +++ b/paibox/backend/conf_template.py @@ -4,7 +4,7 @@ from dataclasses import asdict, dataclass from enum import Enum from pathlib import Path -from typing import Any, ClassVar, NamedTuple, TypedDict, Union +from typing import Any, NamedTuple, TypedDict, Union import numpy as np from numpy.typing import NDArray @@ -41,7 +41,7 @@ from paibox.utils import reverse_8bit from .context import _BACKEND_CONTEXT -from .types import AxonCoord, NeuSegment, NodeName +from .types import WRAMPackedType, AxonCoord, NeuSegment, NodeName try: import orjson @@ -260,7 +260,7 @@ class CorePlmConfig(NamedTuple): """Extra parameters for debugging.""" random_seed: int - weight_ram: NDArray[np.uint64] + weight_ram: WRAMPackedType params_reg: ParamsReg neuron_configs: dict[Neuron, NeuronConfig] @@ -268,15 +268,15 @@ class CorePlmConfig(NamedTuple): def encapsulate( cls, random_seed: int, - weight_ram: NDArray[np.uint64], - core_config: CoreConfig, - neuron_configs: dict[Neuron, NeuronConfig], + weight_ram: WRAMPackedType, + core_cfg: CoreConfig, + neuron_cfg: dict[Neuron, NeuronConfig], ): return cls( random_seed, weight_ram, - ParamsReg.model_validate(core_config._asdict(), strict=True), - neuron_configs, + ParamsReg.model_validate(core_cfg._asdict(), strict=True), + neuron_cfg, ) def export(self) -> dict[str, Any]: @@ -287,11 +287,11 @@ def export(self) -> dict[str, Any]: **self.params_reg.model_dump(by_alias=True), } - for neu, neu_config in self.neuron_configs.items(): + for neu, neu_cfg in self.neuron_configs.items(): if _USE_ORJSON: - dict_["neuron_rams"][neu.name] = orjson.loads(neu_config.to_json()) + dict_["neuron_rams"][neu.name] = orjson.loads(neu_cfg.to_json()) else: - dict_["neuron_rams"][neu.name] = json.loads(neu_config.to_json()) + dict_["neuron_rams"][neu.name] = json.loads(neu_cfg.to_json()) return dict_ @@ -305,23 +305,6 @@ def to_json(self) -> dict[str, Any]: return dict_ -class EmptyCorePlmConfig(CorePlmConfig): - _default_seed: ClassVar[int] = 0 - _default_zero_wram: ClassVar[NDArray[np.uint64]] = np.zeros( - (HwConfig.ADDR_RAM_MAX, 18), dtype=np.uint64 - ) - _default_neuron_conf = {} # don't care - - @classmethod - def encapsulate(cls, core_config: CoreConfig): - return cls( - cls._default_seed, - cls._default_zero_wram, - ParamsReg.model_validate(core_config._asdict(), strict=True), - cls._default_neuron_conf, - ) - - InputNodeConf: TypeAlias = dict[NodeName, InputNeuronDest] OutputDestConf: TypeAlias = dict[NodeName, dict[CoordAddr, NeuronDestInfo]] CorePlmConfInChip: TypeAlias = dict[Coord, CorePlmConfig] @@ -436,7 +419,7 @@ def _write_to_f(name: str, array: FrameArrayType) -> None: 18 * (HwConfig.ADDR_RAM_MAX + 1), v.weight_ram[: HwConfig.ADDR_RAM_MAX + 1], ) - else: + else: # empty core placement config_frame_type4 = None if config_frame_type4: diff --git a/paibox/backend/placement.py b/paibox/backend/placement.py index f5893f88..f5e3bd71 100644 --- a/paibox/backend/placement.py +++ b/paibox/backend/placement.py @@ -16,13 +16,7 @@ from paibox.types import WeightType, WEIGHT_DTYPE from paibox.utils import check_attr_same -from .conf_template import ( - CoreConfig, - CoreConfInChip, - CorePlmConfig, - EmptyCorePlmConfig, - NeuronConfig, -) +from .conf_template import CoreConfig, CoreConfInChip, CorePlmConfig, NeuronConfig from .context import _BACKEND_CONTEXT from .segment_utils import aligned_coords, get_axon_segments, get_neu_segments from .types import ( @@ -758,6 +752,8 @@ def __len__(self) -> int: class EmptyCorePlacement(CoreAbstract): """Empty core placement.""" + _EMPTY_WRAM: int = 0 + def __init__(self, coord: Coord, name: Optional[str] = None) -> None: super().__init__(name) self.coord = coord @@ -783,9 +779,10 @@ def export_param_config(self) -> CoreConfig: # fmt: on return cb_config - def export_core_plm_config(self) -> EmptyCorePlmConfig: + def export_core_plm_config(self) -> CorePlmConfig: core_param = self.export_param_config() - return EmptyCorePlmConfig.encapsulate(core_param) + # For empty core placements, we don't care random seed, WRAM & neurons cfg. + return CorePlmConfig.encapsulate(0, self._EMPTY_WRAM, core_param, {}) # type: ignore @classmethod def build(cls, coord: Coord): diff --git a/tests/backend/conftest.py b/tests/backend/conftest.py index 0f3adac2..1063ea88 100644 --- a/tests/backend/conftest.py +++ b/tests/backend/conftest.py @@ -22,7 +22,6 @@ from paibox.backend.conf_template import ( CoreConfig, CorePlmConfig, - EmptyCorePlmConfig, InputNeuronDest, NeuronConfig, NeuronDest, @@ -942,11 +941,6 @@ def MockCorePlmConfig(MockCoreConfigDict, MockNeuronConfig): return cpc -@pytest.fixture -def MockEmptyCorePlmConfig(MockCoreConfigDict): - return EmptyCorePlmConfig.encapsulate(MockCoreConfigDict) - - def packbits_ref(bits: np.ndarray, count: int) -> int: """Pack unsigned bits into a signed integer. From c2e7dd67e74b34142a1aa0ac856dca56bcaca579 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 19 Jul 2024 10:15:39 +0800 Subject: [PATCH 019/187] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20bump=20paicorelib?= =?UTF-8?q?=20>=3D1.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 322 ++++++++++++++++++++++++++----------------------- pyproject.toml | 2 +- 2 files changed, 171 insertions(+), 153 deletions(-) diff --git a/poetry.lock b/poetry.lock index 900fd9a5..50ea8f2f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,13 +2,13 @@ [[package]] name = "annotated-types" -version = "0.6.0" +version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" files = [ - {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, - {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] [package.source] @@ -34,13 +34,13 @@ reference = "tsinghua" [[package]] name = "exceptiongroup" -version = "1.2.1" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, - {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] @@ -119,57 +119,62 @@ reference = "tsinghua" [[package]] name = "orjson" -version = "3.10.3" +version = "3.10.6" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" files = [ - {file = "orjson-3.10.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9fb6c3f9f5490a3eb4ddd46fc1b6eadb0d6fc16fb3f07320149c3286a1409dd8"}, - {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:252124b198662eee80428f1af8c63f7ff077c88723fe206a25df8dc57a57b1fa"}, - {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f3e87733823089a338ef9bbf363ef4de45e5c599a9bf50a7a9b82e86d0228da"}, - {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8334c0d87103bb9fbbe59b78129f1f40d1d1e8355bbed2ca71853af15fa4ed3"}, - {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1952c03439e4dce23482ac846e7961f9d4ec62086eb98ae76d97bd41d72644d7"}, - {file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0403ed9c706dcd2809f1600ed18f4aae50be263bd7112e54b50e2c2bc3ebd6d"}, - {file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:382e52aa4270a037d41f325e7d1dfa395b7de0c367800b6f337d8157367bf3a7"}, - {file = "orjson-3.10.3-cp310-none-win32.whl", hash = "sha256:be2aab54313752c04f2cbaab4515291ef5af8c2256ce22abc007f89f42f49109"}, - {file = "orjson-3.10.3-cp310-none-win_amd64.whl", hash = "sha256:416b195f78ae461601893f482287cee1e3059ec49b4f99479aedf22a20b1098b"}, - {file = "orjson-3.10.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:73100d9abbbe730331f2242c1fc0bcb46a3ea3b4ae3348847e5a141265479700"}, - {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:544a12eee96e3ab828dbfcb4d5a0023aa971b27143a1d35dc214c176fdfb29b3"}, - {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:520de5e2ef0b4ae546bea25129d6c7c74edb43fc6cf5213f511a927f2b28148b"}, - {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccaa0a401fc02e8828a5bedfd80f8cd389d24f65e5ca3954d72c6582495b4bcf"}, - {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7bc9e8bc11bac40f905640acd41cbeaa87209e7e1f57ade386da658092dc16"}, - {file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3582b34b70543a1ed6944aca75e219e1192661a63da4d039d088a09c67543b08"}, - {file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c23dfa91481de880890d17aa7b91d586a4746a4c2aa9a145bebdbaf233768d5"}, - {file = "orjson-3.10.3-cp311-none-win32.whl", hash = "sha256:1770e2a0eae728b050705206d84eda8b074b65ee835e7f85c919f5705b006c9b"}, - {file = "orjson-3.10.3-cp311-none-win_amd64.whl", hash = "sha256:93433b3c1f852660eb5abdc1f4dd0ced2be031ba30900433223b28ee0140cde5"}, - {file = "orjson-3.10.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a39aa73e53bec8d410875683bfa3a8edf61e5a1c7bb4014f65f81d36467ea098"}, - {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0943a96b3fa09bee1afdfccc2cb236c9c64715afa375b2af296c73d91c23eab2"}, - {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e852baafceff8da3c9defae29414cc8513a1586ad93e45f27b89a639c68e8176"}, - {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18566beb5acd76f3769c1d1a7ec06cdb81edc4d55d2765fb677e3eaa10fa99e0"}, - {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bd2218d5a3aa43060efe649ec564ebedec8ce6ae0a43654b81376216d5ebd42"}, - {file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cf20465e74c6e17a104ecf01bf8cd3b7b252565b4ccee4548f18b012ff2f8069"}, - {file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba7f67aa7f983c4345eeda16054a4677289011a478ca947cd69c0a86ea45e534"}, - {file = "orjson-3.10.3-cp312-none-win32.whl", hash = "sha256:17e0713fc159abc261eea0f4feda611d32eabc35708b74bef6ad44f6c78d5ea0"}, - {file = "orjson-3.10.3-cp312-none-win_amd64.whl", hash = "sha256:4c895383b1ec42b017dd2c75ae8a5b862fc489006afde06f14afbdd0309b2af0"}, - {file = "orjson-3.10.3-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:be2719e5041e9fb76c8c2c06b9600fe8e8584e6980061ff88dcbc2691a16d20d"}, - {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0175a5798bdc878956099f5c54b9837cb62cfbf5d0b86ba6d77e43861bcec2"}, - {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:978be58a68ade24f1af7758626806e13cff7748a677faf95fbb298359aa1e20d"}, - {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16bda83b5c61586f6f788333d3cf3ed19015e3b9019188c56983b5a299210eb5"}, - {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ad1f26bea425041e0a1adad34630c4825a9e3adec49079b1fb6ac8d36f8b754"}, - {file = "orjson-3.10.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9e253498bee561fe85d6325ba55ff2ff08fb5e7184cd6a4d7754133bd19c9195"}, - {file = "orjson-3.10.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0a62f9968bab8a676a164263e485f30a0b748255ee2f4ae49a0224be95f4532b"}, - {file = "orjson-3.10.3-cp38-none-win32.whl", hash = "sha256:8d0b84403d287d4bfa9bf7d1dc298d5c1c5d9f444f3737929a66f2fe4fb8f134"}, - {file = "orjson-3.10.3-cp38-none-win_amd64.whl", hash = "sha256:8bc7a4df90da5d535e18157220d7915780d07198b54f4de0110eca6b6c11e290"}, - {file = "orjson-3.10.3-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9059d15c30e675a58fdcd6f95465c1522b8426e092de9fff20edebfdc15e1cb0"}, - {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d40c7f7938c9c2b934b297412c067936d0b54e4b8ab916fd1a9eb8f54c02294"}, - {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4a654ec1de8fdaae1d80d55cee65893cb06494e124681ab335218be6a0691e7"}, - {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:831c6ef73f9aa53c5f40ae8f949ff7681b38eaddb6904aab89dca4d85099cb78"}, - {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99b880d7e34542db89f48d14ddecbd26f06838b12427d5a25d71baceb5ba119d"}, - {file = "orjson-3.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2e5e176c994ce4bd434d7aafb9ecc893c15f347d3d2bbd8e7ce0b63071c52e25"}, - {file = "orjson-3.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b69a58a37dab856491bf2d3bbf259775fdce262b727f96aafbda359cb1d114d8"}, - {file = "orjson-3.10.3-cp39-none-win32.whl", hash = "sha256:b8d4d1a6868cde356f1402c8faeb50d62cee765a1f7ffcfd6de732ab0581e063"}, - {file = "orjson-3.10.3-cp39-none-win_amd64.whl", hash = "sha256:5102f50c5fc46d94f2033fe00d392588564378260d64377aec702f21a7a22912"}, - {file = "orjson-3.10.3.tar.gz", hash = "sha256:2b166507acae7ba2f7c315dcf185a9111ad5e992ac81f2d507aac39193c2c818"}, + {file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eadc8fd310edb4bdbd333374f2c8fec6794bbbae99b592f448d8214a5e4050c0"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61272a5aec2b2661f4fa2b37c907ce9701e821b2c1285d5c3ab0207ebd358d38"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57985ee7e91d6214c837936dc1608f40f330a6b88bb13f5a57ce5257807da143"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:633a3b31d9d7c9f02d49c4ab4d0a86065c4a6f6adc297d63d272e043472acab5"}, + {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1c680b269d33ec444afe2bdc647c9eb73166fa47a16d9a75ee56a374f4a45f43"}, + {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f759503a97a6ace19e55461395ab0d618b5a117e8d0fbb20e70cfd68a47327f2"}, + {file = "orjson-3.10.6-cp310-none-win32.whl", hash = "sha256:95a0cce17f969fb5391762e5719575217bd10ac5a189d1979442ee54456393f3"}, + {file = "orjson-3.10.6-cp310-none-win_amd64.whl", hash = "sha256:df25d9271270ba2133cc88ee83c318372bdc0f2cd6f32e7a450809a111efc45c"}, + {file = "orjson-3.10.6-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b1ec490e10d2a77c345def52599311849fc063ae0e67cf4f84528073152bb2ba"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d43d3feb8f19d07e9f01e5b9be4f28801cf7c60d0fa0d279951b18fae1932b"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3045267e98fe749408eee1593a142e02357c5c99be0802185ef2170086a863"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c27bc6a28ae95923350ab382c57113abd38f3928af3c80be6f2ba7eb8d8db0b0"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d27456491ca79532d11e507cadca37fb8c9324a3976294f68fb1eff2dc6ced5a"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05ac3d3916023745aa3b3b388e91b9166be1ca02b7c7e41045da6d12985685f0"}, + {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1335d4ef59ab85cab66fe73fd7a4e881c298ee7f63ede918b7faa1b27cbe5212"}, + {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4bbc6d0af24c1575edc79994c20e1b29e6fb3c6a570371306db0993ecf144dc5"}, + {file = "orjson-3.10.6-cp311-none-win32.whl", hash = "sha256:450e39ab1f7694465060a0550b3f6d328d20297bf2e06aa947b97c21e5241fbd"}, + {file = "orjson-3.10.6-cp311-none-win_amd64.whl", hash = "sha256:227df19441372610b20e05bdb906e1742ec2ad7a66ac8350dcfd29a63014a83b"}, + {file = "orjson-3.10.6-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ea2977b21f8d5d9b758bb3f344a75e55ca78e3ff85595d248eee813ae23ecdfb"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6f3d167d13a16ed263b52dbfedff52c962bfd3d270b46b7518365bcc2121eed"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f710f346e4c44a4e8bdf23daa974faede58f83334289df80bc9cd12fe82573c7"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7275664f84e027dcb1ad5200b8b18373e9c669b2a9ec33d410c40f5ccf4b257e"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0943e4c701196b23c240b3d10ed8ecd674f03089198cf503105b474a4f77f21f"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:446dee5a491b5bc7d8f825d80d9637e7af43f86a331207b9c9610e2f93fee22a"}, + {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:64c81456d2a050d380786413786b057983892db105516639cb5d3ee3c7fd5148"}, + {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"}, + {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"}, + {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"}, + {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2c116072a8533f2fec435fde4d134610f806bdac20188c7bd2081f3e9e0133f"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6eeb13218c8cf34c61912e9df2de2853f1d009de0e46ea09ccdf3d757896af0a"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:965a916373382674e323c957d560b953d81d7a8603fbeee26f7b8248638bd48b"}, + {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:03c95484d53ed8e479cade8628c9cea00fd9d67f5554764a1110e0d5aa2de96e"}, + {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e060748a04cccf1e0a6f2358dffea9c080b849a4a68c28b1b907f272b5127e9b"}, + {file = "orjson-3.10.6-cp38-none-win32.whl", hash = "sha256:738dbe3ef909c4b019d69afc19caf6b5ed0e2f1c786b5d6215fbb7539246e4c6"}, + {file = "orjson-3.10.6-cp38-none-win_amd64.whl", hash = "sha256:d40f839dddf6a7d77114fe6b8a70218556408c71d4d6e29413bb5f150a692ff7"}, + {file = "orjson-3.10.6-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:697a35a083c4f834807a6232b3e62c8b280f7a44ad0b759fd4dce748951e70db"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd502f96bf5ea9a61cbc0b2b5900d0dd68aa0da197179042bdd2be67e51a1e4b"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f215789fb1667cdc874c1b8af6a84dc939fd802bf293a8334fce185c79cd359b"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2debd8ddce948a8c0938c8c93ade191d2f4ba4649a54302a7da905a81f00b56"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5410111d7b6681d4b0d65e0f58a13be588d01b473822483f77f513c7f93bd3b2"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb1f28a137337fdc18384079fa5726810681055b32b92253fa15ae5656e1dddb"}, + {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bf2fbbce5fe7cd1aa177ea3eab2b8e6a6bc6e8592e4279ed3db2d62e57c0e1b2"}, + {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:79b9b9e33bd4c517445a62b90ca0cc279b0f1f3970655c3df9e608bc3f91741a"}, + {file = "orjson-3.10.6-cp39-none-win32.whl", hash = "sha256:30b0a09a2014e621b1adf66a4f705f0809358350a757508ee80209b2d8dae219"}, + {file = "orjson-3.10.6-cp39-none-win_amd64.whl", hash = "sha256:49e3bc615652617d463069f91b867a4458114c5b104e13b7ae6872e5f79d0844"}, + {file = "orjson-3.10.6.tar.gz", hash = "sha256:e54b63d0a7c6c54a5f5f726bc93a2078111ef060fec4ecbf34c5db800ca3b3a7"}, ] [package.source] @@ -179,13 +184,13 @@ reference = "tsinghua" [[package]] name = "packaging" -version = "24.0" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [package.source] @@ -195,7 +200,7 @@ reference = "tsinghua" [[package]] name = "paicorelib" -version = "1.1.6" +version = "1.3.0a1" description = "Library of PAICORE 2.0" optional = false python-versions = "^3.9" @@ -204,13 +209,13 @@ develop = false [package.dependencies] numpy = "^1.26.0" -pydantic = "^2.0" +pydantic = "^2.0.3" [package.source] type = "git" url = "https://github.com/PAICookers/PAIlib.git" reference = "dev" -resolved_reference = "81eb16bc8bd9a1d2ebc03d0c868784d47574ae9e" +resolved_reference = "041f4451c01c1d6710c51eece9fa3e98edffdac4" [[package]] name = "pluggy" @@ -234,19 +239,22 @@ reference = "tsinghua" [[package]] name = "pydantic" -version = "2.7.1" +version = "2.8.2" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"}, - {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"}, + {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, + {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.18.2" -typing-extensions = ">=4.6.1" +pydantic-core = "2.20.1" +typing-extensions = [ + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, +] [package.extras] email = ["email-validator (>=2.0.0)"] @@ -258,90 +266,100 @@ reference = "tsinghua" [[package]] name = "pydantic-core" -version = "2.18.2" +version = "2.20.1" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"}, - {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"}, - {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"}, - {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"}, - {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"}, - {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"}, - {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"}, - {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"}, - {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"}, - {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"}, - {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"}, - {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"}, - {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"}, - {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"}, - {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"}, - {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"}, - {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"}, - {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"}, - {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"}, - {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"}, - {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"}, - {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"}, - {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"}, - {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"}, - {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"}, - {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"}, - {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"}, - {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"}, - {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"}, - {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"}, - {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"}, - {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"}, - {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, + {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, + {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, + {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, + {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, + {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, + {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, + {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, + {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, + {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, + {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, + {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, + {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, + {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, ] [package.dependencies] @@ -354,13 +372,13 @@ reference = "tsinghua" [[package]] name = "pytest" -version = "8.2.0" +version = "8.2.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, - {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, ] [package.dependencies] @@ -416,13 +434,13 @@ reference = "tsinghua" [[package]] name = "typing-extensions" -version = "4.11.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [package.source] @@ -433,4 +451,4 @@ reference = "tsinghua" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "c4a9fbd1402e36637c204c66ab5fe5defece23de4292189e1c364c0442d345b1" +content-hash = "ddc3c2f447aeb01635bbdaefe64e188ad015b016d082d956a06e1d73b39dd132" diff --git a/pyproject.toml b/pyproject.toml index d0899cf7..adad889e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ include = ["docs/Guide-of-PAIBox.md", "CHANGELOG.md"] python = "^3.9" pydantic = "^2.0.3" numpy = "^1.26.0" -paicorelib = "^1.1.6" +paicorelib = "~1.3" [tool.poetry.group.test] optional = true From acd05acb0d21b565dbd9106db3ea9f123310585b Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 19 Jul 2024 10:21:43 +0800 Subject: [PATCH 020/187] =?UTF-8?q?=F0=9F=9A=9A=20rename=20`WeightWidth`?= =?UTF-8?q?=20&=20related=20ref?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/checker.py | 4 +- paibox/backend/conf_template.py | 4 +- paibox/backend/placement.py | 29 ++++++------ paibox/base.py | 4 +- paibox/components/synapses/base.py | 6 +-- paibox/components/synapses/transforms.py | 25 +++++----- tests/backend/conftest.py | 54 +++++++++++----------- tests/backend/test_mapper.py | 10 ++-- tests/components/synapses/test_synapses.py | 22 ++++----- 9 files changed, 78 insertions(+), 80 deletions(-) diff --git a/paibox/backend/checker.py b/paibox/backend/checker.py index 6439e9e0..b4844e01 100644 --- a/paibox/backend/checker.py +++ b/paibox/backend/checker.py @@ -1,5 +1,5 @@ from paicorelib import LCN_EX -from paicorelib import WeightPrecision as WP +from paicorelib import WeightWidth as WW __all__ = ["ConfigChecker"] @@ -10,7 +10,7 @@ class _Checker: class ConfigChecker(_Checker): @staticmethod - def n_config_estimate(n_neuron: int, wp: WP, lcn_ex: LCN_EX) -> int: + def n_config_estimate(n_neuron: int, wp: WW, lcn_ex: LCN_EX) -> int: _base = n_neuron * (1 << wp) * (1 << lcn_ex) n_total = 3 + 3 + (1 + 4 * _base) + (1 + 18 * _base) diff --git a/paibox/backend/conf_template.py b/paibox/backend/conf_template.py index 34276c8d..f58a5647 100644 --- a/paibox/backend/conf_template.py +++ b/paibox/backend/conf_template.py @@ -25,7 +25,7 @@ RoutingCoord, SNNModeEnable, SpikeWidthFormat, - WeightPrecision, + WeightWidth, get_replication_id, ) from paicorelib.framelib import types as flib_types @@ -98,7 +98,7 @@ class CoreConfig(NamedTuple): """Extra parameters for debugging.""" name: str - weight_precision: WeightPrecision + weight_width: WeightWidth lcn_extension: LCN_EX input_width_format: InputWidthFormat spike_width_format: SpikeWidthFormat diff --git a/paibox/backend/placement.py b/paibox/backend/placement.py index f5e3bd71..793090a2 100644 --- a/paibox/backend/placement.py +++ b/paibox/backend/placement.py @@ -4,7 +4,7 @@ import numpy as np from paicorelib import LCN_EX, ChipCoord, Coord, CoreMode, HwConfig, MaxPoolingEnable -from paicorelib import WeightPrecision as WP +from paicorelib import WeightWidth as WW from paibox.components import FullConnectedSyn, Neuron from paibox.exceptions import ( @@ -189,14 +189,14 @@ def n_core_required(self) -> int: return len(self.neuron_segs_of_cb) @property - def weight_precision(self) -> WP: - # Optimized in `s.weight_precision`. - return max(s.weight_precision for s in self.obj) + def weight_width(self) -> WW: + # `weight_width` is optimized in FullConnectedSyn. + return max(s.weight_width for s in self.obj) @property def n_weight_bits(self) -> int: """Multiple dendrites will be combined to achieve higher precision weights.""" - return 1 << self.weight_precision + return 1 << self.weight_width @property def lcn_ex(self) -> LCN_EX: @@ -219,7 +219,7 @@ def n_timeslot(self) -> int: @property def dendrite_comb_rate(self) -> int: """#N of dendrites will be combined.""" - return self.lcn_ex + self.weight_precision + return self.lcn_ex + self.weight_width @property def tws(self) -> int: @@ -331,7 +331,7 @@ def get_raw_weight_of_coord(self, idx: int) -> list[WeightType]: def n_neuron_repl(self) -> int: """The number of neurons that need to be repeatedly placed into NRAM. - For example, in SNN mode, N[0:3] with LCN_2X & WP8: + For example, in SNN mode, N[0:3] with LCN_2X & WW8: NRAM [0] [1] ... [15] [16] [17] ... [31] ... N[0] N[0] ... N[0] N[1] N[1] ... N[1] ... @@ -483,12 +483,12 @@ def _weight_ram_mapping(self) -> WRAMPackedType: A portion of the fan-in needs to be expanded to an unfilled portion in the direction of the weight \ accuracy. At this point, n_fold=n_timeslot/(8/n_weight_bits)=2^(dendrite_comb_rate - 3). For example, \ - for LCN_8X & WP8, the n_fold is 3. For LCN_32X & WP4, the n_fold is 4 (instead of 5). + for LCN_8X & WW8, the n_fold is 3. For LCN_32X & WW4, the n_fold is 4 (instead of 5). TODO Now, in ANN mode, only the mapping of 8-bit weights is supported. The weight accuracy optimization is \ supposed to disable manually for now. """ - if not self.rt_mode.is_snn and self.weight_precision < WP.WEIGHT_WIDTH_8BIT: + if not self.rt_mode.is_snn and self.weight_width < WW.WEIGHT_WIDTH_8BIT: raise NotSupportedError("only support 8-bit weights in ANN mode.") _weights_folded = self._fold_raw_weights(self.raw_weights) @@ -588,7 +588,7 @@ def export_param_config(self) -> CoreConfig: # fmt: off cb_config = CoreConfig( self.name, # name of the core - self.weight_precision, # weight_precision + self.weight_width, # weight_precision self.lcn_ex, # lcn_extension _mode_params[0], # input_width_format _mode_params[1], # spike_width_format @@ -671,7 +671,6 @@ def export_neu_config( def export_core_plm_config(self) -> CorePlmConfig: core_param = self.export_param_config() - return CorePlmConfig.encapsulate( self.parent.seed, self.weight_ram, core_param, self.neu_configs ) @@ -679,10 +678,10 @@ def export_core_plm_config(self) -> CorePlmConfig: @property def shape(self) -> tuple[int, int]: return (len(self.source), len(self.dest)) - + @property - def weight_precision(self) -> WP: - return self.parent.weight_precision + def weight_width(self) -> WW: + return self.parent.weight_width @property def n_weight_bits(self) -> int: @@ -764,7 +763,7 @@ def export_param_config(self) -> CoreConfig: # fmt: off cb_config = CoreConfig( self.name, # name of the core - WP.WEIGHT_WIDTH_1BIT, # weight_precision + WW.WEIGHT_WIDTH_1BIT, # weight_precision LCN_EX.LCN_1X, # lcn_extension _mode_params[0], # input_width_format _mode_params[1], # spike_width_format diff --git a/paibox/base.py b/paibox/base.py index efe3421a..f0dc8c1a 100644 --- a/paibox/base.py +++ b/paibox/base.py @@ -8,7 +8,7 @@ else: from typing_extensions import TypeAlias -from paicorelib import WeightPrecision as WP +from paicorelib import WeightWidth as WW from .collector import Collector from .mixin import ReceiveInputProj, StatusMemory, TimeRelatedNode @@ -296,7 +296,7 @@ def weights(self) -> WeightType: raise NotImplementedError @property - def weight_precision(self) -> WP: + def weight_width(self) -> WW: raise NotImplementedError @property diff --git a/paibox/components/synapses/base.py b/paibox/components/synapses/base.py index fe3f75a4..52b09ae1 100644 --- a/paibox/components/synapses/base.py +++ b/paibox/components/synapses/base.py @@ -2,7 +2,7 @@ import numpy as np from paicorelib import HwConfig -from paicorelib import WeightPrecision as WP +from paicorelib import WeightWidth as WW from paibox.base import NeuDyn, SynSys from paibox.exceptions import RegisterError, ShapeError @@ -185,8 +185,8 @@ def weights(self) -> WeightType: return self.comm.weights @property - def weight_precision(self) -> WP: - return self.comm._get_wp(self.CFLAG_ENABLE_WP_OPTIMIZATION) + def weight_width(self) -> WW: + return self.comm._get_weight_width(self.CFLAG_ENABLE_WP_OPTIMIZATION) @property def connectivity(self) -> WeightType: diff --git a/paibox/components/synapses/transforms.py b/paibox/components/synapses/transforms.py index 78807f2d..9ae205b6 100644 --- a/paibox/components/synapses/transforms.py +++ b/paibox/components/synapses/transforms.py @@ -3,7 +3,7 @@ from typing import Literal, Optional import numpy as np -from paicorelib import WeightPrecision as WP +from paicorelib import WeightWidth as WW from paibox.exceptions import AutoOptimizationWarning, ShapeError from paibox.types import ( @@ -13,7 +13,7 @@ NeuOutType, SynOutType, WeightType, - VOLTAGE_DTYPE + VOLTAGE_DTYPE, ) from paibox.utils import is_shape, shape2num, typical_round @@ -109,22 +109,21 @@ def _set_coarse_dtype(raw_w: DataArrayType) -> WeightType: return _array.astype(_dtype, casting="same_kind") -def _get_weight_precision(weight: WeightType, enable_wp_opt: bool) -> WP: - """Get the actual weight_precision of the weight.""" - _max = np.max(weight, axis=None) - _min = np.min(weight, axis=None) +def _get_weight_width_inner(weight: WeightType, enable_wp_opt: bool) -> WW: + """Get the actual width of the weight.""" + _max, _min = np.max(weight), np.min(weight) if enable_wp_opt: if _max <= MAX_INT1 and _min >= MIN_INT1: - return WP.WEIGHT_WIDTH_1BIT + return WW.WEIGHT_WIDTH_1BIT elif _max <= MAX_INT2 and _min >= MIN_INT2: - return WP.WEIGHT_WIDTH_2BIT + return WW.WEIGHT_WIDTH_2BIT elif _max <= MAX_INT4 and _min >= MIN_INT4: - return WP.WEIGHT_WIDTH_4BIT + return WW.WEIGHT_WIDTH_4BIT else: - return WP.WEIGHT_WIDTH_8BIT + return WW.WEIGHT_WIDTH_8BIT else: - return WP.WEIGHT_WIDTH_8BIT + return WW.WEIGHT_WIDTH_8BIT class Transform: @@ -140,8 +139,8 @@ def __call__(self, *args, **kwargs) -> SynOutType: "function '__call__' must be implemented in the subclasses." ) - def _get_wp(self, enable_wp_opt: bool) -> WP: - return _get_weight_precision(self.weights, enable_wp_opt) + def _get_weight_width(self, enable_wp_opt: bool) -> WW: + return _get_weight_width_inner(self.weights, enable_wp_opt) @property def connectivity(self) -> WeightType: diff --git a/tests/backend/conftest.py b/tests/backend/conftest.py index 1063ea88..21315b3a 100644 --- a/tests/backend/conftest.py +++ b/tests/backend/conftest.py @@ -15,7 +15,7 @@ RoutingDirection, RoutingLevel, ) -from paicorelib import WeightPrecision as WP +from paicorelib import WeightWidth as WW from paicorelib.reg_model import TICK_WAIT_END_MAX, TICK_WAIT_START_MAX import paibox as pb @@ -811,7 +811,7 @@ def get_mapper() -> pb.Mapper: @pytest.fixture def MockCoreConfigDict() -> CoreConfig: - wp = random.choice(list(WP)) + wp = random.choice(list(WW)) lcn_ex = random.choice(list(LCN_EX)) iwf, swf, sme = random.choice(list(CoreMode)).conf @@ -1276,61 +1276,61 @@ class TestData: ((0, 2), (0, 2)), 1, (np.bool_, np.bool_), - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_1BIT, ), ( ((0, 2), (0, 2)), -1, (np.bool_, np.bool_), - WP.WEIGHT_WIDTH_2BIT, + WW.WEIGHT_WIDTH_2BIT, ), ( ((0, 2), (0, 2)), 1, (np.bool_, np.int8), - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_1BIT, ), ( ((0, 2), (0, 2)), -2, (np.int8, np.bool_), - WP.WEIGHT_WIDTH_2BIT, + WW.WEIGHT_WIDTH_2BIT, ), ( ((0, 2), (0, 2)), 1, (np.int8, np.int8), - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_1BIT, ), ( ((0, 2), (-2, 2)), -8, (np.bool_, np.int8), - WP.WEIGHT_WIDTH_4BIT, + WW.WEIGHT_WIDTH_4BIT, ), ( ((0, 2), (-2, 2)), 7, (np.bool_, np.int8), - WP.WEIGHT_WIDTH_4BIT, + WW.WEIGHT_WIDTH_4BIT, ), ( ((0, 2), (-128, 128)), 127, (np.bool_, np.int8), - WP.WEIGHT_WIDTH_8BIT, + WW.WEIGHT_WIDTH_8BIT, ), ( ((-2, 2), (-8, 8)), 7, (np.int8, np.int8), - WP.WEIGHT_WIDTH_4BIT, + WW.WEIGHT_WIDTH_4BIT, ), ( ((-8, 8), (-8, 8)), -100, (np.int8, np.int8), - WP.WEIGHT_WIDTH_8BIT, + WW.WEIGHT_WIDTH_8BIT, ), ], ) @@ -1342,7 +1342,7 @@ class TestData: ( [_nl[0], _nl[1]], 512, - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_1BIT, LCN_EX.LCN_1X, [ [NeuSegment(_nl[0], slice(0, 300, 1), 0)], @@ -1354,7 +1354,7 @@ class TestData: ( [_nl[0], _nl[1]], 256, - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_1BIT, LCN_EX.LCN_2X, [ [NeuSegment(_nl[0], slice(0, 200, 1), 0, 2)], @@ -1369,7 +1369,7 @@ class TestData: ( [_nl[2]], 200, - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_1BIT, LCN_EX.LCN_2X, [ [NeuSegment(_nl[2], slice(80 * 0, 80 * 1, 1), 0, 2)], @@ -1381,7 +1381,7 @@ class TestData: ( [_nl[0], _nl[2]], 400, - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_1BIT, LCN_EX.LCN_1X, [ [NeuSegment(_nl[0], slice(0, 300, 1), 0)], @@ -1393,7 +1393,7 @@ class TestData: ( [_nl[3], _nl[4]], 240, - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_1BIT, LCN_EX.LCN_2X, [ [NeuSegment(_nl[3], slice(67 * 0, 67 * 1, 1), 0, 2)], @@ -1415,7 +1415,7 @@ class TestData: ( [_nc[0], _nc[1]], 512, - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_1BIT, LCN_EX.LCN_1X, [ [NeuSegment(_nc[0], slice(0, 512, 1), 0)], @@ -1429,7 +1429,7 @@ class TestData: ( [_nc[0], _nc[1]], 256, - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_1BIT, LCN_EX.LCN_2X, [ [NeuSegment(_nc[0], slice(256 * 0, 256 * 1, 1), 0, 2)], @@ -1446,7 +1446,7 @@ class TestData: ( [_nc[3], _nc[4]], 256, - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_1BIT, LCN_EX.LCN_2X, [ # Place the neuron segments with full capacity first @@ -1460,7 +1460,7 @@ class TestData: ( [_nc[5], _nc[6]], 512, - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_1BIT, LCN_EX.LCN_1X, [ [NeuSegment(_nc[6], slice(0, 500, 1), 0, 1)], @@ -1477,7 +1477,7 @@ class TestData: ( [_nb[0], _nb[1]], 512, - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_1BIT, LCN_EX.LCN_1X, [ [NeuSegment(_nb[0], slice(0, 300, 1), 0)], @@ -1489,7 +1489,7 @@ class TestData: ( [_nb[0], _nb[1]], 256, - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_1BIT, LCN_EX.LCN_2X, [ [NeuSegment(_nb[1], slice(0, 200, 1), 0, 2)], @@ -1504,7 +1504,7 @@ class TestData: ( [_nb[2]], 200, - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_1BIT, LCN_EX.LCN_2X, [ [NeuSegment(_nb[2], slice(80 * 0, 80 * 1, 1), 0, 2)], @@ -1516,7 +1516,7 @@ class TestData: ( [_nb[2], _nb[3]], 200, - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_1BIT, LCN_EX.LCN_2X, [ [ @@ -1542,7 +1542,7 @@ class TestData: ( [_nb[2], _nb[3], _nb[4]], 256, - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_1BIT, LCN_EX.LCN_2X, [ [ @@ -1586,7 +1586,7 @@ class TestData: ( [_nb[3], _nb[4]], 240, - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_1BIT, LCN_EX.LCN_2X, [ [ diff --git a/tests/backend/test_mapper.py b/tests/backend/test_mapper.py index 21f3cabbc..73e5ea84 100644 --- a/tests/backend/test_mapper.py +++ b/tests/backend/test_mapper.py @@ -2,7 +2,7 @@ import numpy as np import pytest -from paicorelib import Coord, HwConfig, WeightPrecision as WP +from paicorelib import Coord, HwConfig, WeightWidth as WW import paibox as pb from paibox.base import SynSys @@ -576,15 +576,15 @@ def __init__(self): mapper = pb.Mapper() mapper.build(net) mapper.compile(weight_bit_optimization=False) - assert mapper.core_blocks[0].weight_precision == WP.WEIGHT_WIDTH_8BIT + assert mapper.core_blocks[0].weight_width == WW.WEIGHT_WIDTH_8BIT mapper.clear() mapper.build(net) mapper.compile(weight_bit_optimization=True) - assert mapper.core_blocks[0].weight_precision == max( - s.weight_precision for s in (net.s1, net.s2, net.s3) + assert mapper.core_blocks[0].weight_width == max( + s.weight_width for s in (net.s1, net.s2, net.s3) ) - assert mapper.core_blocks[0].weight_precision == expected_wp_opt + assert mapper.core_blocks[0].weight_width == expected_wp_opt from tests.utils import measure_time diff --git a/tests/components/synapses/test_synapses.py b/tests/components/synapses/test_synapses.py index 6378b4de..506963ac 100644 --- a/tests/components/synapses/test_synapses.py +++ b/tests/components/synapses/test_synapses.py @@ -2,7 +2,7 @@ import numpy as np import pytest -from paicorelib import WeightPrecision as WP +from paicorelib import WeightWidth as WW import paibox as pb from paibox.components import FullConnectedSyn @@ -112,13 +112,13 @@ class TestFullConn: @pytest.mark.parametrize( "n1, n2, scalar_weight, expected_wp", [ - (pb.IF(10, 3), pb.IF(10, 3), 1, WP.WEIGHT_WIDTH_1BIT), - (pb.IF((3, 3), 3), pb.IF((3, 3), 3), 4, WP.WEIGHT_WIDTH_4BIT), - (pb.IF((5,), 3), pb.IF((5,), 3), -1, WP.WEIGHT_WIDTH_2BIT), + (pb.IF(10, 3), pb.IF(10, 3), 1, WW.WEIGHT_WIDTH_1BIT), + (pb.IF((3, 3), 3), pb.IF((3, 3), 3), 4, WW.WEIGHT_WIDTH_4BIT), + (pb.IF((5,), 3), pb.IF((5,), 3), -1, WW.WEIGHT_WIDTH_2BIT), # TODO 3-dimension shape is correct for data flow? - (pb.IF((10, 2, 3), 3), pb.IF((10, 2, 3), 3), 16, WP.WEIGHT_WIDTH_8BIT), - (pb.IF((10, 2), 3), pb.IF((4, 5), 3), -100, WP.WEIGHT_WIDTH_8BIT), - (pb.IF(10, 3), pb.IF((2, 5), 3), 7, WP.WEIGHT_WIDTH_4BIT), + (pb.IF((10, 2, 3), 3), pb.IF((10, 2, 3), 3), 16, WW.WEIGHT_WIDTH_8BIT), + (pb.IF((10, 2), 3), pb.IF((4, 5), 3), -100, WW.WEIGHT_WIDTH_8BIT), + (pb.IF(10, 3), pb.IF((2, 5), 3), 7, WW.WEIGHT_WIDTH_4BIT), ], ) def test_FullConn_One2One_scalar(self, n1, n2, scalar_weight, expected_wp): @@ -131,7 +131,7 @@ def test_FullConn_One2One_scalar(self, n1, n2, scalar_weight, expected_wp): scalar_weight * np.identity(n1.num_out, dtype=WEIGHT_DTYPE), ) assert s1.connectivity.dtype == WEIGHT_DTYPE - assert s1.weight_precision is expected_wp + assert s1.weight_width is expected_wp @pytest.mark.parametrize( "n1, n2", @@ -158,7 +158,7 @@ def test_FullConn_One2One_matrix(self): s1.connectivity, np.array([[2, 0, 0], [0, 3, 0], [0, 0, 4]], dtype=np.int8) ) assert s1.connectivity.dtype == WEIGHT_DTYPE - assert s1.weight_precision is WP.WEIGHT_WIDTH_4BIT + assert s1.weight_width is WW.WEIGHT_WIDTH_4BIT weight = np.array([1, 0, 1, 0], np.int8) s2 = pb.FullConn( @@ -174,7 +174,7 @@ def test_FullConn_One2One_matrix(self): ), ) assert s2.connectivity.dtype == WEIGHT_DTYPE - assert s2.weight_precision is WP.WEIGHT_WIDTH_1BIT + assert s2.weight_width is WW.WEIGHT_WIDTH_1BIT @pytest.mark.parametrize( "n1, n2", @@ -204,7 +204,7 @@ def test_FullConn_All2All_with_weights(self): assert np.array_equal(s1.weights, weight) assert s1.connectivity.dtype == WEIGHT_DTYPE - assert s1.weight_precision is WP.WEIGHT_WIDTH_4BIT + assert s1.weight_width is WW.WEIGHT_WIDTH_4BIT """2. Weights matrix.""" weight = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) From 67054d821819d9059748eb3fd518c223f7897dc9 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 19 Jul 2024 10:30:52 +0800 Subject: [PATCH 021/187] =?UTF-8?q?=E2=9C=85=20add=20tests=20for=20ANN=20p?= =?UTF-8?q?lacement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/backend/test_placement.py | 249 ++++++++++++++++++++------------ 1 file changed, 155 insertions(+), 94 deletions(-) diff --git a/tests/backend/test_placement.py b/tests/backend/test_placement.py index 6febdb32..7be18952 100644 --- a/tests/backend/test_placement.py +++ b/tests/backend/test_placement.py @@ -1,11 +1,13 @@ import numpy as np import pytest -from paicorelib import LCN_EX -from paicorelib import WeightPrecision as WP +from paicorelib import HwConfig, LCN_EX +from paicorelib import WeightWidth as WW import paibox as pb -from paibox.backend.types import NeuSegment +from paibox.backend.placement import CorePlacement +from paibox.backend.types import NeuSegment, WRAMUnpackedType, WRAM_PACKED_DTYPE from paibox.exceptions import ResourceError +from paibox.types import WEIGHT_DTYPE, WeightType def packbits_ref(bits: np.ndarray, count: int) -> int: @@ -21,10 +23,9 @@ def packbits_ref(bits: np.ndarray, count: int) -> int: return result -def test_get_raw_weight_ref(): - rng = np.random.RandomState(seed=1) - w1 = rng.randint(-128, 128, size=(10, 20), dtype=np.int8) - w2 = rng.randint(-128, 128, size=(10, 30), dtype=np.int8) +def test_get_raw_weight_ref(random_fixture): + w1 = np.random.randint(-128, 128, size=(10, 20), dtype=WEIGHT_DTYPE) + w2 = np.random.randint(-128, 128, size=(10, 30), dtype=WEIGHT_DTYPE) w_of_neurons = [w1, w2] @@ -97,9 +98,8 @@ def test_weight_ram_mapping(input, n_col_groups, expected): This is a test of the prototype of the original function. """ cur_shape = input.shape - expected_shape = expected.shape - row, col = expected.shape - o_matrix = np.zeros(expected_shape, dtype=np.int8) + row, _ = expected.shape + o_matrix = np.zeros(expected.shape, dtype=np.int8) for i in range(cur_shape[1]): w_col = input[:, i] @@ -111,8 +111,6 @@ def test_weight_ram_mapping(input, n_col_groups, expected): ] col_group += 1 - print(o_matrix) - o_matrix[:, n_col_groups * i + col_group] = np.pad( w_col[row * col_group :], pad_width=(0, row - n_rest_axon), @@ -120,20 +118,18 @@ def test_weight_ram_mapping(input, n_col_groups, expected): constant_values=0, ) - print(o_matrix) - assert np.array_equal(o_matrix, expected) def test_nfold_weight_ref(): - original_matrix = np.arange(1, 25, dtype=np.int8).reshape(8, 3) + original_matrix = np.arange(1, 25, dtype=WEIGHT_DTYPE).reshape(8, 3) nfold = 3 if original_matrix.shape[0] % nfold > 0: _padding = nfold - original_matrix.shape[0] % nfold w_padding = np.append( original_matrix, - values=np.zeros((_padding, original_matrix.shape[1]), dtype=np.int8), + values=np.zeros((_padding, original_matrix.shape[1]), dtype=WEIGHT_DTYPE), axis=0, ) else: @@ -142,7 +138,8 @@ def test_nfold_weight_ref(): split = np.vsplit(w_padding, nfold) result = np.zeros( - (w_padding.shape[0] // nfold, original_matrix.shape[1] * nfold), dtype=np.int8 + (w_padding.shape[0] // nfold, original_matrix.shape[1] * nfold), + dtype=WEIGHT_DTYPE, ) for i, j in np.ndindex((nfold, original_matrix.shape[1])): @@ -157,7 +154,7 @@ def test_nfold_weight_ref(): [4, 13, 22, 5, 14, 23, 6, 15, 24], [7, 16, 0, 8, 17, 0, 9, 18, 0], ], - dtype=np.int8, + dtype=WEIGHT_DTYPE, ), ) @@ -166,17 +163,15 @@ class TestWeightUnpack: @pytest.mark.parametrize( "wp", [ - WP.WEIGHT_WIDTH_8BIT, - WP.WEIGHT_WIDTH_4BIT, - WP.WEIGHT_WIDTH_2BIT, - WP.WEIGHT_WIDTH_1BIT, + WW.WEIGHT_WIDTH_8BIT, + WW.WEIGHT_WIDTH_4BIT, + WW.WEIGHT_WIDTH_2BIT, + WW.WEIGHT_WIDTH_1BIT, ], ) def test_signed_unpackbits(self, wp): count = 1 << wp - actual_array = np.arange( - -(1 << (count - 1)), (1 << (count - 1)), 1, dtype=np.int8 - ) + actual_array = np.arange(-(1 << (count - 1)), (1 << (count - 1)), dtype=np.int8) for actual_signed in actual_array: unpacked = np.unpackbits( @@ -203,20 +198,26 @@ def test_uint8_unpackbits_scalar(self): assert np.array_equal(y2, np.array([1, 0, 1, 0, 0, 1, 1, 1], dtype=np.uint8)) @pytest.mark.parametrize( - "shape, wp, nfold", + "shape, wp, nfold, is_iw8", [ - ((8, 8), WP.WEIGHT_WIDTH_8BIT, 2), - ((32, 32), WP.WEIGHT_WIDTH_8BIT, 2), - ((16, 16), WP.WEIGHT_WIDTH_4BIT, 4), - ((30, 24), WP.WEIGHT_WIDTH_4BIT, 4), - ((32, 24), WP.WEIGHT_WIDTH_2BIT, 3), - ((32, 24), WP.WEIGHT_WIDTH_1BIT, 3), - ((31, 23), WP.WEIGHT_WIDTH_8BIT, 5), - ((1200, 200), WP.WEIGHT_WIDTH_1BIT, 2), - ((800, 64), WP.WEIGHT_WIDTH_8BIT, 2), + ((8, 8), WW.WEIGHT_WIDTH_8BIT, 2, False), + ((32, 32), WW.WEIGHT_WIDTH_8BIT, 2, False), + ((16, 16), WW.WEIGHT_WIDTH_4BIT, 4, False), + ((30, 24), WW.WEIGHT_WIDTH_4BIT, 4, False), + ((32, 24), WW.WEIGHT_WIDTH_2BIT, 3, False), + ((32, 24), WW.WEIGHT_WIDTH_1BIT, 3, False), + ((31, 23), WW.WEIGHT_WIDTH_8BIT, 5, False), + ((1200, 200), WW.WEIGHT_WIDTH_1BIT, 2, False), + ((800, 64), WW.WEIGHT_WIDTH_8BIT, 2, False), + ((8, 8), WW.WEIGHT_WIDTH_8BIT, 2, True), + ((32, 32), WW.WEIGHT_WIDTH_8BIT, 2, True), + ((16, 16), WW.WEIGHT_WIDTH_4BIT, 4, True), + ((200, 32), WW.WEIGHT_WIDTH_8BIT, 2, True), + ((30, 24), WW.WEIGHT_WIDTH_4BIT, 4, True), + ((32, 24), WW.WEIGHT_WIDTH_2BIT, 3, True), ], ) - def test_weight_ram_mapping(self, shape, wp, nfold): + def test_weight_ram_mapping(self, shape, wp, nfold, is_iw8): nbit = 1 << wp if shape[0] % nfold > 0: @@ -229,37 +230,68 @@ def test_weight_ram_mapping(self, shape, wp, nfold): # Generate the original weight with shape _low = 0 if nbit == 1 else -(1 << (nbit - 1)) _high = 1 << (nbit - 1) - array = np.random.randint(_low, _high, size=shape, dtype=np.int8) + test_weight = np.random.randint(_low, _high, size=shape, dtype=WEIGHT_DTYPE) # 1. Fold, return the folded weight after padding. - w_folded = self._fold_raw_weight_ref(array, expected_shape[0], nfold) + w_folded = self._nfold_weight_ref(test_weight, expected_shape[0], nfold) # 2. Unpack, get the weight ram. - if nbit > 1: - w_unpacked = self._weight_ram_mapping_ref(w_folded, nbit) - else: - w_unpacked = w_folded.astype(np.bool_) - + # The real interval is HwConfig.N_FANIN_PER_DENDRITE_ANN + _fake_interval = w_folded.shape[0] * 2 + w_unpacked = self._weight_ram_mapping_ref( + w_folded, nbit, is_iw8, _fake_interval + ) w_unpacked.setflags(write=False) # 3. Check - for i, j in np.ndindex(shape): - n_in_col = w_folded.shape[0] - now_i = i % n_in_col + self._check( + test_weight, w_folded, w_unpacked, nbit, nfold, is_iw8, _fake_interval + ) - offset_j = i // n_in_col - now_j = offset_j + j * nfold + @staticmethod + def _nfold_weight_ref(raw_weight: WeightType, expected_row: int, nfold: int): + raw_row, raw_col = raw_weight.shape - expected = array[i, j] - wij = w_unpacked[now_i, now_j * nbit : (now_j + 1) * nbit] - packed = packbits_ref(wij, nbit) + if raw_row % nfold > 0: + _padding = nfold - raw_row % nfold + assert expected_row * nfold == raw_row + _padding - assert expected == packed + w_padding = np.append( + raw_weight, + values=np.zeros((_padding, raw_col), dtype=WEIGHT_DTYPE), + axis=0, + ) + else: + w_padding = raw_weight + + split = np.vsplit(w_padding, nfold) + w_folded = np.zeros((expected_row, raw_col * nfold), dtype=WEIGHT_DTYPE) + + for i, j in np.ndindex((nfold, raw_col)): + w_col = split[i][:, j] + w_folded[:, j * nfold + i] = w_col + + return w_folded @staticmethod - def _weight_ram_mapping_ref(folded_weights: np.ndarray, n_bit: int): + def _weight_ram_mapping_ref( + folded_weights: WeightType, + n_bit: int, + is_iw8: bool, + fake_interval: int, + ): row, col = folded_weights.shape - result = np.zeros((row, col * n_bit), dtype=np.uint8) + # if iw = 1, the row of result is the same as the row of folded_weights + if not is_iw8: + result_row = row + else: + result_row = 8 * fake_interval + + result = np.zeros((result_row, col * n_bit), dtype=np.uint8) + + if n_bit == 1: + result[:row, :col] = folded_weights + return result # [N*M] -> [M*N*1] folded_weights_3d = np.expand_dims(folded_weights.T, axis=2).astype(np.uint8) @@ -270,25 +302,68 @@ def _weight_ram_mapping_ref(folded_weights: np.ndarray, n_bit: int): folded_weights_3d[i], axis=1, count=n_bit, bitorder="little" ) - result[:, n_bit * i : n_bit * (i + 1)] = unpacked + if not is_iw8: + result[:row, n_bit * i : n_bit * (i + 1)] = unpacked + else: + for bit in range(n_bit): + result[bit * fake_interval : bit * fake_interval + row, i] = ( + unpacked[:, bit] + ) assert np.max(result, axis=None) <= 1 assert np.min(result, axis=None) >= 0 return result - def test_packbits_to_mapping_form(self): + @staticmethod + def _check( + test_data: WeightType, + w_folded: WeightType, + w_unpacked: WRAMUnpackedType, + nbit: int, + nfold: int, + is_iw8: bool, + fake_interval: int = 0, + ) -> None: + for i, j in np.ndindex(test_data.shape): + n_in_col = w_folded.shape[0] + now_i = i % n_in_col + offset_j = i // n_in_col + now_j = offset_j + j * nfold + + if not is_iw8: + wij = w_unpacked[now_i, now_j * nbit : (now_j + 1) * nbit] + else: + # From LSB to MSB + bits = [ + w_unpacked[i * fake_interval + now_i, now_j] for i in range(nbit) + ] + wij = np.asarray(bits, dtype=np.uint8) + + wij_packed = packbits_ref(wij, nbit) + assert test_data[i, j] == wij_packed + + def test_CorePlacement_weight_pack_shape(self): + # Mock unpacked weight + w_unpacked = np.zeros(CorePlacement.WRAM_BASE_SHAPE, dtype=np.uint8) + w_packed_u64 = CorePlacement._weight_pack(w_unpacked) + + assert w_packed_u64.shape == ( + (HwConfig.ADDR_RAM_MAX + 1), + (HwConfig.ADDR_AXON_MAX + 1) // (WRAM_PACKED_DTYPE(1).nbytes * 8), + ) + + def test_packbits_to_mapping_form(self, random_fixture): def _weight_ram_T(weight_ram_mapped: np.ndarray): _w = weight_ram_mapped.T.reshape(-1, 64) w_packed_u8 = np.packbits(_w, axis=-1, bitorder="little") return w_packed_u8 - rng = np.random.RandomState(42) - w = rng.randint(-8, 8, size=(1152, 64), dtype=np.int8) + w = np.random.randint(-8, 8, size=(1152, 64), dtype=WEIGHT_DTYPE) # 1152 * 512 - w1 = self._weight_ram_mapping_ref(w, 8) + w1 = self._weight_ram_mapping_ref(w, 8, False, 0) # -> 512 * 1152 -> 512 * 144 (uint8) wT = _weight_ram_T(w1) @@ -297,38 +372,11 @@ def _weight_ram_T(weight_ram_mapped: np.ndarray): ww.setflags(write=False) assert 1 - @staticmethod - def _fold_raw_weight_ref(raw_weight: np.ndarray, expected_row: int, nfold: int): - raw_row, raw_col = raw_weight.shape - - if raw_row % nfold > 0: - _padding = nfold - raw_row % nfold - assert expected_row * nfold == raw_row + _padding - - w_padding = np.append( - raw_weight, - values=np.zeros((_padding, raw_col), dtype=np.int8), - axis=0, - ) - else: - w_padding = raw_weight.copy() - - split = np.vsplit(w_padding, nfold) - assert w_padding.shape[0] == expected_row * nfold - - w_folded = np.zeros((expected_row, raw_col * nfold), dtype=np.int8) - - for i, j in np.ndindex((nfold, raw_col)): - w_col = split[i][:, j] - w_folded[:, j * nfold + i] = w_col - - return w_folded - def test_weight_ram_mapping_8bits(self, packbits8): binary_conn = np.zeros((6, 8 * 5), dtype=np.bool_) - wp = WP.WEIGHT_WIDTH_8BIT + wp = WW.WEIGHT_WIDTH_8BIT - array = np.random.randint(-128, 128, size=(4, 4), dtype=np.int8) + array = np.random.randint(-128, 128, size=(4, 4), dtype=WEIGHT_DTYPE) y = np.unpackbits(np.uint8(array), axis=1, bitorder="little") assert y.shape == (4, (1 << wp) * 4) @@ -344,9 +392,9 @@ def test_weight_ram_mapping_8bits(self, packbits8): def test_weight_ram_mapping_4bits(self, packbits4): binary_conn = np.zeros((6, 4 * 5), dtype=np.bool_) - wp = WP.WEIGHT_WIDTH_4BIT + wp = WW.WEIGHT_WIDTH_4BIT - array = np.random.randint(-8, 8, size=(4, 4), dtype=np.int8) + array = np.random.randint(-8, 8, size=(4, 4), dtype=WEIGHT_DTYPE) y = np.zeros((4, 16), dtype=np.uint8) for i in range(4): @@ -367,9 +415,9 @@ def test_weight_ram_mapping_4bits(self, packbits4): def test_weight_ram_mapping_2bits(self, packbits2): binary_conn = np.zeros((6, 4 * 5), dtype=np.bool_) - wp = WP.WEIGHT_WIDTH_2BIT + wp = WW.WEIGHT_WIDTH_2BIT - array = np.random.randint(-2, 2, size=(4, 4), dtype=np.int8) + array = np.random.randint(-2, 2, size=(4, 4), dtype=WEIGHT_DTYPE) y = np.zeros((4, 8), dtype=np.uint8) for i in range(4): @@ -392,8 +440,21 @@ def test_weight_ram_mapping_2bits(self, packbits2): def test_n_axon2lcn_ex(): from .conftest import n_axon2lcn_ex_proto - lcn_ex = n_axon2lcn_ex_proto(1152 * 18 + 1, 1152) + lcn_ex = n_axon2lcn_ex_proto( + HwConfig.N_FANIN_PER_DENDRITE_SNN * 18 + 1, HwConfig.N_FANIN_PER_DENDRITE_SNN + ) assert lcn_ex == LCN_EX.LCN_32X + lcn_ex = n_axon2lcn_ex_proto( + HwConfig.N_FANIN_PER_DENDRITE_ANN * 3 + 20, HwConfig.N_FANIN_PER_DENDRITE_ANN + ) + assert lcn_ex == LCN_EX.LCN_4X + + with pytest.raises(ValueError): + lcn_ex = n_axon2lcn_ex_proto(0, HwConfig.N_FANIN_PER_DENDRITE_SNN) + with pytest.raises(ResourceError): - lcn_ex = n_axon2lcn_ex_proto(1152 * 64 + 1, 1152) + lcn_ex = n_axon2lcn_ex_proto( + HwConfig.N_FANIN_PER_DENDRITE_SNN << LCN_EX.LCN_64X + 1, + HwConfig.N_FANIN_PER_DENDRITE_SNN, + ) From 9fbb3ba1116586c2efecb653c266743d1a83c5d6 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 19 Jul 2024 10:32:24 +0800 Subject: [PATCH 022/187] =?UTF-8?q?=E2=9C=85=20add=20tests=20for=20ANN=20m?= =?UTF-8?q?apping=20&=20remove=20unused=20weight4=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/backend/test_mapper.py | 108 +++++------------------------------ tests/shared_networks.py | 58 ++++++++++++++++++- 2 files changed, 71 insertions(+), 95 deletions(-) diff --git a/tests/backend/test_mapper.py b/tests/backend/test_mapper.py index 73e5ea84..516e89b6 100644 --- a/tests/backend/test_mapper.py +++ b/tests/backend/test_mapper.py @@ -122,15 +122,26 @@ def test_nested_net_L2_compile(self, get_mapper, build_Nested_Net_level_2): assert len(mapper.graph_info["output"]) == 1 def test_nested_net_L3_compile(self, get_mapper, build_Nested_Net_level_3): - net2 = build_Nested_Net_level_3 + net = build_Nested_Net_level_3 mapper: pb.Mapper = get_mapper - mapper.build(net2) + mapper.build(net) mapper.compile() assert len(mapper.graph.edges.keys()) == 5 assert len(mapper.graph_info["input"]) == 2 assert len(mapper.graph_info["output"]) == 1 + def test_ANN_network_compile( + self, get_mapper, build_ANN_Network_1, ensure_dump_dir + ): + net = build_ANN_Network_1 + mapper: pb.Mapper = get_mapper + mapper.build(net) + mapper.compile() + mapper.export(fp=ensure_dump_dir, export_core_params=True) + + assert 1 + class TestMapperDeployment: def test_build_graph(self, get_mapper, build_example_net1, build_example_net2): @@ -331,104 +342,13 @@ def test_export_empty_cplm(self, build_example_net4_large_scale, ensure_dump_dir assert len(mapper.routing_groups[1].wasted_coords) == 2 -class TestMapper_Weight4: - @pytest.mark.skipif( - hasattr(SynSys, "CFLAG_ENABLE_WP_OPTIMIZATION"), reason="Breaking change" - ) - def test_mapper_weight4( - self, monkeypatch, ensure_dump_dir, build_network_with_branches_4bit, packbits8 - ): - # Use monkey patch to change the settings of `HwConfig` when running the test. - monkeypatch.setattr(HwConfig, "N_DENDRITE_MAX_SNN", 8 * 8) - monkeypatch.setattr(HwConfig, "N_FANIN_PER_DENDRITE_SNN", 6) - - net = build_network_with_branches_4bit - - mapper = pb.Mapper() - mapper.build(net) - mapper.compile() - - configs = mapper.export(write_to_file=False, fp=ensure_dump_dir, format="npy") - - assert mapper.n_core_required == 11 - - from paibox.backend.checker import ConfigChecker - - cplm00 = mapper.core_blocks[0].core_placements[Coord(0, 0)] - cplm01 = mapper.core_blocks[0].core_placements[Coord(0, 1)] - cplm10 = mapper.core_blocks[0].core_placements[Coord(1, 0)] - - n_config_core00 = ConfigChecker.n_config_estimate( - cplm00.n_neuron, cplm00.weight_precision, cplm00.lcn_ex - ) - n_config_core01 = ConfigChecker.n_config_estimate( - cplm01.n_neuron, cplm01.weight_precision, cplm01.lcn_ex - ) - n_config_core10 = ConfigChecker.n_config_estimate( - cplm10.n_neuron, cplm10.weight_precision, cplm10.lcn_ex - ) - - assert n_config_core00 == configs[Coord(0, 0)].size - assert n_config_core01 == configs[Coord(0, 1)].size - assert n_config_core10 == configs[Coord(1, 0)].size - - # The #N of config frames of each core. - - original_w1 = net.s1.connectivity - original_w2 = net.s2.connectivity - original_w3 = net.s3.connectivity - original_w4 = net.s4.connectivity - original_w5 = net.s5.connectivity - - # Folded weight of s1 - w11_folded = mapper.core_blocks[0].core_placements[Coord(0, 0)]._weights_folded - w12_folded = mapper.core_blocks[0].core_placements[Coord(0, 1)]._weights_folded - w13_folded = mapper.core_blocks[0].core_placements[Coord(1, 0)]._weights_folded - - # Splited & folded weight of s2 & s3 - w21_folded = mapper.core_blocks[1].core_placements[Coord(2, 0)]._weights_folded - w22_folded = mapper.core_blocks[1].core_placements[Coord(2, 1)]._weights_folded - w23_folded = mapper.core_blocks[1].core_placements[Coord(3, 0)]._weights_folded - w24_folded = mapper.core_blocks[1].core_placements[Coord(3, 1)]._weights_folded - w25_folded = mapper.core_blocks[1].core_placements[Coord(2, 2)]._weights_folded - w26_folded = mapper.core_blocks[1].core_placements[Coord(2, 3)]._weights_folded - - # Splited & folded weight of s4 & 5 - w31_folded = mapper.core_blocks[2].core_placements[Coord(0, 2)]._weights_folded - w32_folded = mapper.core_blocks[2].core_placements[Coord(0, 3)]._weights_folded - - # Unpacked weight of s1 - w11_unpacked = mapper.core_blocks[0].core_placements[Coord(0, 0)].weight_ram - w12_unpacked = mapper.core_blocks[0].core_placements[Coord(0, 1)].weight_ram - w13_unpacked = mapper.core_blocks[0].core_placements[Coord(1, 0)].weight_ram - - for i in range(10): - for j in range(4): - n_in_col = w11_folded.shape[0] - now_i = i % n_in_col - - offset_j = i // n_in_col - now_j = offset_j + j * 2 - - expected = original_w1[i, j] - wij = w11_folded[now_i, now_j] - - assert expected == wij - - # wij = w11_folded[now_i, now_j * 8 : (now_j + 1) * 8] - # packed = packbits8(wij) - # assert expected == packed - - print("OK") - - class TestMapper_Compile: def test_grouping_optim_latency( self, monkeypatch, build_Network_8bit_dense, ensure_dump_dir ): from paibox.backend.conf_template import export_core_plm_conf_json - monkeypatch.setattr(HwConfig, "N_DENDRITE_MAX_SNN", 8 * 8) + monkeypatch.setattr(HwConfig, "N_NEURON_MAX_SNN", 8 * 8) monkeypatch.setattr(HwConfig, "N_FANIN_PER_DENDRITE_SNN", 6) net = build_Network_8bit_dense diff --git a/tests/shared_networks.py b/tests/shared_networks.py index 0c1d5d30..876a95a3 100644 --- a/tests/shared_networks.py +++ b/tests/shared_networks.py @@ -1,5 +1,5 @@ from typing import Literal - +import numpy as np import pytest import paibox as pb @@ -216,6 +216,57 @@ def __init__(self, shape, axes): self.probe2 = pb.Probe(self.n2, "spike") +class ANNNetwork(pb.Network): + def __init__(self): + super().__init__() + self.inp1 = pb.InputProj(input=_out_bypass1, shape_out=(32, 32)) + + n1_bias = np.random.randint(-128, 128, size=(4,), dtype=np.int8) + self.n1 = pb.LIF( + (4, 30, 30), + 100, + bias=n1_bias, + tick_wait_start=1, + input_width=8, + spike_width=8, + snn_en=False, + ) + n2_bias = np.random.randint(-128, 128, size=(4,), dtype=np.int8) + self.n2 = pb.LIF( + (4, 28, 28), + 50, + bias=n2_bias, + tick_wait_start=2, + input_width=8, + spike_width=8, + snn_en=False, + ) + self.n3 = pb.LIF( + (2, 26, 26), + 20, + bias=1, + tick_wait_start=3, + input_width=8, + spike_width=8, + snn_en=False, + ) + self.n4 = pb.IF( + (100,), 10, tick_wait_start=4, input_width=8, spike_width=8, snn_en=False + ) + + kernel_1 = np.random.randint(-128, 128, size=(4, 1, 3, 3), dtype=np.int8) + self.conv2d_1 = pb.Conv2d(self.inp1, self.n1, kernel_1) + + kernel_2 = np.random.randint(-128, 128, size=(4, 4, 3, 3), dtype=np.int8) + self.conv2d_2 = pb.Conv2d(self.n1, self.n2, kernel_2) + + kernel_3 = np.random.randint(-128, 128, size=(2, 4, 3, 3), dtype=np.int8) + self.conv2d_3 = pb.Conv2d(self.n2, self.n3, kernel_3) + + w4 = np.random.randint(-128, 128, size=(2 * 26 * 26, 100), dtype=np.int8) + self.fc1 = pb.FullConn(self.n3, self.n4, w4) + + @pytest.fixture(scope="class") def build_BitwiseAND_Net(): return FunctionalModule_2to1_Net("and") @@ -264,3 +315,8 @@ def build_FModule_ConnWithModule_Net(): @pytest.fixture(scope="class") def build_FModule_ConnWithFModule_Net(): return FModule_ConnWithFModule_Net() + + +@pytest.fixture(scope="class") +def build_ANN_Network_1(): + return ANNNetwork() From d2e50835f4f57ecbfc189abd119a25d855c6337d Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 19 Jul 2024 10:32:55 +0800 Subject: [PATCH 023/187] =?UTF-8?q?=E2=9C=85=20update=20tests=20with=20rea?= =?UTF-8?q?son=20for=20deprecation=20skip=20&=20fix=20random=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/components/test_functional.py | 8 ++++---- tests/simulator/test_encoder.py | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/components/test_functional.py b/tests/components/test_functional.py index 9913fb11..b50ee82a 100644 --- a/tests/components/test_functional.py +++ b/tests/components/test_functional.py @@ -551,7 +551,7 @@ def test_SpikingPool2dWithV_mapping(self, ensure_dump_dir): mapper.compile() mapper.export(fp=ensure_dump_dir) - @pytest.mark.skipif(hasattr(pb.Transpose2d, "__deprecated__")) + @pytest.mark.skipif(hasattr(pb.Transpose2d, "__deprecated__"), reason="deprecated") @pytest.mark.parametrize("shape", [(32, 16), (1, 32), (64,), (128, 1), 48]) def test_Transpose2d(self, shape): from tests.shared_networks import TransposeModule_T2d_Net @@ -582,7 +582,7 @@ def test_Transpose2d(self, shape): expected = inpa[i - 2].T.ravel() assert np.array_equal(sim1.data[net1.probe2][i], expected) - @pytest.mark.skipif(hasattr(pb.Transpose2d, "__deprecated__")) + @pytest.mark.skipif(hasattr(pb.Transpose2d, "__deprecated__"), reason="deprecated") def test_Transpose2d_mapping(self, ensure_dump_dir): from tests.shared_networks import TransposeModule_T2d_Net @@ -593,7 +593,7 @@ def test_Transpose2d_mapping(self, ensure_dump_dir): mapper.compile() mapper.export(fp=ensure_dump_dir) - @pytest.mark.skipif(hasattr(pb.Transpose2d, "__deprecated__")) + @pytest.mark.skipif(hasattr(pb.Transpose2d, "__deprecated__"), reason="deprecated") @pytest.mark.parametrize( "shape, axes", [ @@ -635,7 +635,7 @@ def test_Transpose3d(self, shape, axes): expected = inpa[i - 2].transpose(axes).ravel() assert np.array_equal(sim1.data[net1.probe2][i], expected) - @pytest.mark.skipif(hasattr(pb.Transpose2d, "__deprecated__")) + @pytest.mark.skipif(hasattr(pb.Transpose2d, "__deprecated__"), reason="deprecated") def test_Transpose3d_mapping(self, ensure_dump_dir): from tests.shared_networks import TransposeModule_T3d_Net diff --git a/tests/simulator/test_encoder.py b/tests/simulator/test_encoder.py index 4821d262..259e8ccb 100644 --- a/tests/simulator/test_encoder.py +++ b/tests/simulator/test_encoder.py @@ -36,10 +36,9 @@ def test_LatencyEncoder(self): out_spike2[t] = le2(x) assert 1 - def test_PoissonEncoder(self): + def test_PoissonEncoder(self, random_fixture): seed = 1 - rng = np.random.RandomState(seed=seed) - x = rng.rand(10, 10).astype(np.float32) + x = np.random.rand(10, 10).astype(np.float32) pe = pb.simulator.PoissonEncoder(seed=seed) out_spike = np.full((20, 10, 10), 0) for t in range(20): From b0bf42604a6ed0361daf09fc207d4e51f802910b Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 19 Jul 2024 10:37:26 +0800 Subject: [PATCH 024/187] =?UTF-8?q?=F0=9F=8E=A8=20update=20references=20ba?= =?UTF-8?q?sed=20on=20paicorelib=20~1.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/conf_template.py | 20 +++++--------------- paibox/backend/placement.py | 21 ++++++--------------- paibox/backend/types.py | 4 ++-- 3 files changed, 13 insertions(+), 32 deletions(-) diff --git a/paibox/backend/conf_template.py b/paibox/backend/conf_template.py index f58a5647..c7482275 100644 --- a/paibox/backend/conf_template.py +++ b/paibox/backend/conf_template.py @@ -22,6 +22,7 @@ ) from paicorelib import ReplicationId as RId from paicorelib import ( + NeuronConf, RoutingCoord, SNNModeEnable, SpikeWidthFormat, @@ -29,7 +30,7 @@ get_replication_id, ) from paicorelib.framelib import types as flib_types -from paicorelib.framelib.frame_gen import OfflineFrameGen +from paicorelib.framelib import OfflineFrameGen from paicorelib.framelib.utils import _mask, np2bin, np2npy, np2txt if sys.version_info >= (3, 10): @@ -175,16 +176,6 @@ class OutputNeuronDest(NamedTuple): end: AxonCoord -try: - from paicorelib.ram_model import NeuronConf as _NeuronConf -except ImportError: - from pydantic import BaseModel - - class _NeuronConf(BaseModel): - attrs: NeuronAttrs - dest_info: NeuronDestInfo - - class NeuronConfig(NamedTuple): _extra_params = ( "n_neuron", @@ -239,8 +230,8 @@ def encapsulate( neu_seg.n_neuron, neu_seg.addr_ram, neu_seg.offset, attrs, neuron_dest_info ) - def export(self) -> _NeuronConf: - return _NeuronConf(attrs=self.neuron_attrs, dest_info=self.neuron_dest_info) + def export(self) -> NeuronConf: + return NeuronConf(attrs=self.neuron_attrs, dest_info=self.neuron_dest_info) def to_json(self) -> Union[str, bytes]: """Dump the configs into json for debugging.""" @@ -393,8 +384,7 @@ def _write_to_f(name: str, array: FrameArrayType) -> None: _n_neuron_nram, neu_conf.neuron_attrs, neu_conf.neuron_dest_info, - lcn_ex=v.params_reg.lcn_extension, - weight_precision=v.params_reg.weight_precision, + v.params_reg.n_repeat_nram, ) ) diff --git a/paibox/backend/placement.py b/paibox/backend/placement.py index 793090a2..d0d59af8 100644 --- a/paibox/backend/placement.py +++ b/paibox/backend/placement.py @@ -393,8 +393,10 @@ class CorePlacement(CoreAbstract): neu_segs_of_cplm: NeuSegOfCorePlm neu_configs: dict[Neuron, NeuronConfig] - # FIXME Change to HwConfig.ADDR_AXON_MAX(1152) once it is fixed. - WRAM_BASE_SHAPE: ClassVar[tuple[int, int]] = (1152, HwConfig.ADDR_RAM_MAX) + WRAM_BASE_SHAPE: ClassVar[tuple[int, int]] = ( + HwConfig.ADDR_AXON_MAX + 1, + HwConfig.ADDR_RAM_MAX + 1, + ) def __init__( self, @@ -797,19 +799,8 @@ def max_lcn_of_cb(cb: list[CoreBlock]) -> LCN_EX: return max(cb, key=lambda cb: cb.lcn_ex).lcn_ex +# Get the fan-out by the combination rate of dendrites if hasattr(HwConfig, "FANOUT_IW8"): FANOUT_IW8 = HwConfig.FANOUT_IW8 # type: ignore else: - # Get the fan-out by the combination rate of dendrites - FANOUT_IW8: list[int] = [ - HwConfig.N_NEURON_MAX_ANN, - 1364, - 876, - 512, - 256, - 128, - 64, - 32, - 16, - 8, - ] + FANOUT_IW8 = [HwConfig.N_NEURON_MAX_ANN, 1364, 876, 512, 256, 128, 64, 32, 16, 8] diff --git a/paibox/backend/types.py b/paibox/backend/types.py index 3eaf594e..bed63c20 100644 --- a/paibox/backend/types.py +++ b/paibox/backend/types.py @@ -125,9 +125,9 @@ def addr_ram(self) -> list[int]: def addr_max(self) -> int: if ( _addr_max := self.offset + self.repeat * self.n_neuron - ) > HwConfig.ADDR_RAM_MAX: + ) > HwConfig.ADDR_RAM_MAX + 1: raise ValueError( - f"neuron RAM address out of range {HwConfig.ADDR_RAM_MAX} ({_addr_max})." + f"neuron RAM address out of range {HwConfig.ADDR_RAM_MAX + 1} ({_addr_max})." ) return _addr_max From 10a5090bb4c9c4ebeba55e042bf19da73928a924 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Fri, 19 Jul 2024 10:45:50 +0800 Subject: [PATCH 025/187] =?UTF-8?q?=F0=9F=93=9D=20add=20the=20description?= =?UTF-8?q?=20of=20the=20ANN=20parameters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Guide-of-PAIBox.md | 54 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/docs/Guide-of-PAIBox.md b/docs/Guide-of-PAIBox.md index 70826bab..1301133d 100644 --- a/docs/Guide-of-PAIBox.md +++ b/docs/Guide-of-PAIBox.md @@ -1,3 +1,13 @@ + +
# PAIBox使用指南 @@ -9,7 +19,7 @@ python = "^3.9" pydantic = "^2.0.3" numpy = "^1.26.0" -paicorelib = "^1.1.6" +paicorelib = "~1.3" ``` 可选依赖: @@ -67,11 +77,49 @@ n1 = pb.IF(shape=10, threshold=127, reset_v=0, neg_threshold=-100, keep_shape=Fa - `delay`:设定神经元输出的延迟。默认为1,即本时间步的计算结果,**下一时间步**传递至后继节点。 - `tick_wait_start`:设定神经元启动时间。神经元将在第 `T` 个时间步时启动。0表示不启动。默认为1。 - `tick_wait_end`:设定神经元持续工作时长。神经元将持续工作 `T` 个时间步。0表示**持续工作**。默认为0。 -- `unrolling_factor`:该参数与后端流程相关。展开因子表示神经元将被展开,部署至更多的物理核上,以降低延迟并提高吞吐率。 +- `unrolling_factor`:展开因子表示神经元将被展开,部署至更多的物理核上,以降低延迟并提高吞吐率。该参数仅与后端流程相关。默认为1。 - `overflow_strict`:溢出严格模式。用于设置是否严格检查运算过程中神经元膜电位出现溢出的情况。若启用,遇到溢出将报错,否则将遵循硬件行为进行处理。默认为 `False`。 - `keep_shape`:是否在仿真记录数据时保持尺寸信息,默认为 `True`。实际进行运算的尺寸仍视为一维。 - `name`:神经元的名称。可选参数。 +神经元的部分行为由芯片计算核的某些配置项决定:输入数据位数、输出数据位数、SNN使能。芯片计算核的工作模式即由这些参数决定。例如,SNN模式则是输入数据、输出数据位数均为1bit,SNN使能为1。对应关系如下表所列: + +

计算核配置项与工作模式对应表

+
+ +| 模式 | `input_width` | `spike_width` | `snn_en` | +| :-----------------------: | :-------------: | :-------------: | :--------: | +| BANN | 0 | 0 | 0 | +| SNN | 0 | 0 | 1 | +| BANN/SNN to ANN | 0 | 1 | 0 | +| BANN/SNN to SNN with values | 0 | 1 | 1 | +| ANN to BANN/SNN | 1 | 0 | 0 | +| BANN | 1 | 1 | 0 | +| Undefined | 1 | 0/1 | 1 | + +
+ +- `input_width`:处理核输入数据位数,1或8。为1表示该处理核的输入数据为脉冲,反之为 8bit 无符号数。默认为1。 +- `spike_width`:神经元输出数据位数,1或8。为1表示该处理核输出数据(从神经元输出)为脉冲,反之为 8bit 无符号数。默认为1。 +- `snn_en`:SNN 模式使能。当开启时,神经元内的计算保留上一时刻膜电平信息,反之不保留(ANN 计算模式不需要上一时刻膜电平信息)。默认为 `True`。 +- `bit_truncation`:神经元输出的 8bit 无符号数的截断位置。默认为8,该参数仅在 `spike_width=8` 时生效。由于膜电平为 30bit 有符号数,因此需要截取 8bit 作为神经元最终的输出。若膜电平最高有效位大于所截取的位置,则输出255。该截断操作类似于有上限的斜率可调的 Relu 操作。`bit_truncation` 与截取位置的对应关系如下表所列: + +

截取位置对应表

+
+ +| `bit_truncation` | 截取位置 | +| :----------------: | :-----------: | +| 0 | 8'h0 | +| 1 | {[0], 7'h0} | +| 2 | {[1:0], 6'h0} | +| …… | …… | +| 8 | [7:0] | +| 9 | [8:1] | +| …… | …… | +| 29 | [28:21] | + +
+ #### LIF LIF 神经元实现了“泄露-积分-发射”神经元模型,其调用方式及参数如下: @@ -912,7 +960,7 @@ mapper.clear() - `input`:输入节点信息字典。 - `output`:输出目的地信息字典。 -- `memebers`:中间层所在物理核的配置项字典。 +- `members`:中间层所在物理核的配置项字典。 - `inherent_timestep`:网络的最长时间步。 - `n_core_required`:网络**需要**的物理核数目。 - `n_core_occupied`:网络**实际占用**的物理核数目。 From 68fda25ab22901cc87f6fecac8374a7d00ece5da Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 22 Jul 2024 10:52:39 +0800 Subject: [PATCH 026/187] =?UTF-8?q?=F0=9F=94=96=20v1.2.0a1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 12 ++++++++---- poetry.lock | 4 ++-- pyproject.toml | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c766b1b..bf8d663e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ ## v1.0.0a6 -- 新增 `Always1Neuron` 神经元,该神经元将在工作期间持续输出1,不得单独存在,需存在前向突触与其连接。 +- 新增 `Always1Neuron` 神经元,该神经元将在工作期间持续输出1,不得单独存在,需存在前向突触与其连接 ## v1.0.0a7 @@ -58,16 +58,20 @@ 2. 负阈值,默认为硬件允许的最小负整数 3. LIF 支持设置偏置,偏置可为数组形式 4. LIF 支持同时设置泄露与偏置,将叠加处理 - - 支持神经元的随机突触整合、随机阈值、随机泄露配置的设置,但不支持仿真 - 支持多芯片部署 -- 重构路由算法,现在的算法不会出现路由死锁 +- 重构路由算法,现在路由不会出现死锁 - 行为变更: 1. 子网络现在直接在主网络内部 `self.subnet=...` 例化即可 2. 编译选项现在直接通过 `paibox.Mapper.compile(...)` 传入,默认配置不变 - 3. 在 `paibox.Mapper.export()` 中使用 `split_by_chip` 指定配置帧文件是否以芯片分割,默认不分割。原 `split_by_coord` 弃用。 + 3. 在 `paibox.Mapper.export()` 中使用 `split_by_chip` 指定配置帧文件是否以芯片分割,默认不分割。原 `split_by_coord` 弃用 ## v1.1.1 - 修复对权重RAM错误的配置 + +## v1.2.0a1 + +- 提高 `paicorelib` 依赖版本至 `~1.3` +- 支持 ANN 网络的构建与部署 diff --git a/poetry.lock b/poetry.lock index 50ea8f2f..4645ad5b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -200,7 +200,7 @@ reference = "tsinghua" [[package]] name = "paicorelib" -version = "1.3.0a1" +version = "1.3.0" description = "Library of PAICORE 2.0" optional = false python-versions = "^3.9" @@ -215,7 +215,7 @@ pydantic = "^2.0.3" type = "git" url = "https://github.com/PAICookers/PAIlib.git" reference = "dev" -resolved_reference = "041f4451c01c1d6710c51eece9fa3e98edffdac4" +resolved_reference = "5cedc5fb1f66bc21e1c442a87bc804517a6555c2" [[package]] name = "pluggy" diff --git a/pyproject.toml b/pyproject.toml index adad889e..75cc1f5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "paibox" -version = "1.1.1" +version = "1.2.0a1" description = "Toolchain of PAICORE 2.0" authors = ["Ziru Pan "] maintainers = [ From 36210ac7d14e489745a7c56d7a9cbf0a6f465a9c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 02:53:08 +0000 Subject: [PATCH 027/187] :rotating_light: auto fix by pre-commit hooks --- CHANGELOG.md | 1 + docs/Guide-of-PAIBox.md | 40 ++++++++++++------------ paibox/backend/conf_template.py | 8 ++--- paibox/backend/placement.py | 12 +++---- paibox/backend/segment_utils.py | 4 +-- paibox/components/modules.py | 4 +-- paibox/components/neuron/base.py | 8 ++--- paibox/components/neuron/utils.py | 5 ++- paibox/components/synapses/transforms.py | 2 +- tests/backend/test_mapper.py | 3 +- tests/backend/test_placement.py | 4 +-- tests/components/neuron/test_neurons.py | 4 +-- tests/shared_networks.py | 1 + tests/test_utils.py | 2 +- 14 files changed, 50 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf8d663e..8274561c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ 2. 负阈值,默认为硬件允许的最小负整数 3. LIF 支持设置偏置,偏置可为数组形式 4. LIF 支持同时设置泄露与偏置,将叠加处理 + - 支持神经元的随机突触整合、随机阈值、随机泄露配置的设置,但不支持仿真 - 支持多芯片部署 - 重构路由算法,现在路由不会出现死锁 diff --git a/docs/Guide-of-PAIBox.md b/docs/Guide-of-PAIBox.md index 1301133d..94bd1039 100644 --- a/docs/Guide-of-PAIBox.md +++ b/docs/Guide-of-PAIBox.md @@ -1,5 +1,5 @@ - -
# PAIBox使用指南 -
- ## 安装 ```toml @@ -60,12 +47,12 @@ PAIBox 提供了多种类型的神经元模型,能够实现各种特殊的功 #### IF -IF 神经元实现了经典的“积分发射”模型,其调用方式及参数如下: +IF 神经元实现了经典的“积分-发射”模型,其调用方式及参数如下: ```python import paibox as pb -n1 = pb.IF(shape=10, threshold=127, reset_v=0, neg_threshold=-100, keep_shape=False, delay=1, tick_wait_start=1, tick_wait_end=0, name='n1') +n1 = pb.IF(shape=10, threshold=127, reset_v=0, neg_threshold=-100, keep_shape=True, delay=1, tick_wait_start=1, tick_wait_end=0, name='n1') ``` 其中: @@ -84,9 +71,6 @@ n1 = pb.IF(shape=10, threshold=127, reset_v=0, neg_threshold=-100, keep_shape=Fa 神经元的部分行为由芯片计算核的某些配置项决定:输入数据位数、输出数据位数、SNN使能。芯片计算核的工作模式即由这些参数决定。例如,SNN模式则是输入数据、输出数据位数均为1bit,SNN使能为1。对应关系如下表所列: -

计算核配置项与工作模式对应表

-
- | 模式 | `input_width` | `spike_width` | `snn_en` | | :-------------------------: | :-----------: | :-----------: | :------: | | BANN | 0 | 0 | 0 | @@ -97,16 +81,11 @@ n1 = pb.IF(shape=10, threshold=127, reset_v=0, neg_threshold=-100, keep_shape=Fa | BANN | 1 | 1 | 0 | | Undefined | 1 | 0/1 | 1 | -
- - `input_width`:处理核输入数据位数,1或8。为1表示该处理核的输入数据为脉冲,反之为 8bit 无符号数。默认为1。 - `spike_width`:神经元输出数据位数,1或8。为1表示该处理核输出数据(从神经元输出)为脉冲,反之为 8bit 无符号数。默认为1。 - `snn_en`:SNN 模式使能。当开启时,神经元内的计算保留上一时刻膜电平信息,反之不保留(ANN 计算模式不需要上一时刻膜电平信息)。默认为 `True`。 - `bit_truncation`:神经元输出的 8bit 无符号数的截断位置。默认为8,该参数仅在 `spike_width=8` 时生效。由于膜电平为 30bit 有符号数,因此需要截取 8bit 作为神经元最终的输出。若膜电平最高有效位大于所截取的位置,则输出255。该截断操作类似于有上限的斜率可调的 Relu 操作。`bit_truncation` 与截取位置的对应关系如下表所列: -

截取位置对应表

-
- | `bit_truncation` | 截取位置 | | :--------------: | :-----------: | | 0 | 8'h0 | @@ -118,15 +97,13 @@ n1 = pb.IF(shape=10, threshold=127, reset_v=0, neg_threshold=-100, keep_shape=Fa | …… | …… | | 29 | [28:21] | -
- #### LIF LIF 神经元实现了“泄露-积分-发射”神经元模型,其调用方式及参数如下: ```python -n1 = pb.LIF(shape=128, threshold=127, reset_v=0, leak_v=-1, neg_threshold=0, keep_shape=False, name='n1') -n2 = pb.LIF(shape=128, threshold=10, reset_v=1, bias=-1, keep_shape=True, name='n2') +n1 = pb.LIF(shape=128, threshold=127, reset_v=0, leak_v=-1, neg_threshold=0, name='n1') +n2 = pb.LIF(shape=128, threshold=10, reset_v=1, bias=-1, name='n2') ``` - `leak_v`:泄露,有符号数。 @@ -138,7 +115,7 @@ n2 = pb.LIF(shape=128, threshold=10, reset_v=1, bias=-1, keep_shape=True, name=' Tonic Spiking 神经元可以实现对持续脉冲刺激的周期性反应。 ```python -n1 = pb.TonicSpiking(shape=128, fire_step=3, keep_shape=False, name='n1') +n1 = pb.TonicSpiking(shape=128, fire_step=3, name='n1') ``` - `fire_step`:发放时间,每接收到 `N` 次刺激后发放脉冲。 @@ -148,16 +125,44 @@ n1 = pb.TonicSpiking(shape=128, fire_step=3, keep_shape=False, name='n1') Phasic Spiking 神经元可以实现,在接受一定数量脉冲后发放,然后保持静息状态,不再发放。 ```python -n1 = pb.PhasicSpiking(shape=128, fire_step=3, neg_floor=-10, keep_shape=False, name='n1') +n1 = pb.PhasicSpiking(shape=128, fire_step=3, neg_floor=-10, name='n1') ``` - `fire_step`:发放时间,每接收到 `N` 次刺激后发放脉冲。 - `neg_floor`:地板阈值,有符号负数。当发放脉冲后,膜电位将永远保持在地板阈值。 +#### Bypass Neuron + +正阈值为1,负阈值、复位电平、泄露均为0的神经元。它的输出等于输入。 + +```python +n1 = pb.BypassNeuron(shape=128, name='n1') +``` + #### Spiking Relu +⚠️ 即将弃用,请使用 `BypassNeuron` + SNN 模式下,具有 Relu 功能的神经元。当输入为1,则输出为1;输入为非正整数,输出为0。 +#### ANN Neuron + +`LIF` 的子类,在 ANN 模式下调用。`bit_truncation=8`,且预设 `input_width=8`,`spike_width=8` 以及 `snn_en=False`。 + +```python +n1 = pb.ANNNeuron(shape=128, bias=1, bit_trunc=9, name='n1') +``` + +其中,`bias` 与 `bit_trunc` 的含义参见前述。 + +#### ANN Bypass Neuron + +`ANNNeuron` 的子类,在 ANN 模式下调用,可作为直通神经元使用。`bias=0`,`bit_truncation=8`。 + +```python +n1 = pb.ANNBypassNeuron(shape=128, name='n1') +``` + ### 突触 #### 全连接 FullConn @@ -618,9 +623,9 @@ print(output) 功能模块均支持 `delay`,`tick_wait_start`,`tick_wait_end`,`keep_shape` 参数。 -### 逻辑运算 +### 逻辑位运算 -逻辑运算模块实现了 `numpy` 中的位逻辑运算操作(例如 `&` 与 `numpy.bitwise_and` 等),可对接收到的一或多个输出脉冲进行逻辑运算,并产生脉冲输出。PAIBox 提供了逻辑与、或、非、异或:`BitwiseAND`,`BitwiseOR`,`BitwiseNOT`,`BitwiseXOR`。以位与为例: +逻辑位运算模块实现了 `numpy` 中的位逻辑运算操作(例如 `&` 与 `numpy.bitwise_and` 等),可对接收到的一或多个输出脉冲进行逻辑运算,并产生脉冲输出。PAIBox 提供了位与、或、非、异或:`BitwiseAND`,`BitwiseOR`,`BitwiseNOT`,`BitwiseXOR`。以位与为例: ```python import paibox as pb @@ -647,21 +652,6 @@ class Net(pb.DynSysGroup): ⚠️ 模块的属性 `external_delay` 用于表示其相对于外部的内部固有延迟。这是由具体的后端构建形式决定的,不可更改。上述示例中,位与计算结果将输出至 `n3` 中。默认情况下,`n3` 将在位与计算结果输出后启动,因此其启动时间为 `and1` 的启动时间+固有延迟+1。 -### 延迟链 - -用于实现神经元延迟输出。使用方式如下: - -```python -n1 = pb.IF((10,), 1, 0, delay=1, tick_wait_start=1) -n1_delay_out = pb.DelayChain(n1, chain_level=5, delay=1, tick_wait_start=2) -n2 = pb.SpikingRelu((10,), delay=1, tick_wait_start=n1_delay_out.tick_wait_start + n1_delay_out.external_delay) -``` - -其中: - -- `neuron`:进行延迟输出的神经元。 -- `chain_level`:延迟链的级数,即延迟的时间步。注意,这与 `delay` 含义不同:延迟链内部会建立多级神经元(类似buffer),以实现数据的延迟传递,而 `delay` 会使得神经元输出寄存的位置延后,后继节点的启动时间需要提前,这将导致其在前级**有效输出**前就进行了计算。 - ### 2D平均/最大池化 目前仅提供2D池化:`SpikingAvgPool2d`、`SpikingMaxPool2d`。以最大池化为例: @@ -687,10 +677,18 @@ s3 = pb.FullConn(p2d, n2, conn_type=pb.SynConnType.One2One) - `threshold`:平均池化的比较阈值,芯片需要通过神经元的阈值比较间接地实现除法。当不指定时,阈值为 $\text{round}(\text{kernel\_size}/2)$。池化窗口的输入做累加后与该阈值进行比较,可等价于平均池化的操作,即 $o_j=\sum^{k-1}_{i=0}x_{ij} \ge V_{th,pos}$,其中 $k$ 为池化窗口尺寸,$x_{ij}$ 为每个池化窗口内的输入特征图元素,$o_j$ 为第 $j$ 个输出特征图元素。 -### \*2D平均池化(与膜电位相关) +### 2D平均池化(膜电位相关) 这是 `SpikingAvgPool2d` 的另一种实现形式。`SpikingAvgPool2d` 在每个时间步上的运算**不会造成膜电位积累**(当未发放时),因此,可以说它与时间步无关。而该平均池化实现,当未发放时,**会造成膜电位积累**,因此与时间步相关。调用 `SpikingAvgPool2dWithV`,参数与前述 `SpikingAvgPool2d` 相同。 +### 1D平均/最大池化 + +请参阅2D平均/最大池化。 + +### 1D平均池化(膜电位相关) + +请参阅2D平均池化(膜电位相关)。 + ### 脉冲加、减 脉冲加减法与数的加减法存在差异。对脉冲进行加减,运算结果将在较长时间步上体现。例如,在 `T=1` 时刻两神经元均输出1,则将在 `T=2,3` 时刻产生输出脉冲。以下为脉冲加减法运算示例。其中,输入为 `T=12` 脉冲序列,输出为 `T=20` 脉冲序列。 diff --git a/docs/Guide-of-Test.md b/docs/Guide-of-Test.md index 8146b0b4..5263ea9d 100644 --- a/docs/Guide-of-Test.md +++ b/docs/Guide-of-Test.md @@ -21,7 +21,7 @@ pytest = "^8.0.0" ## 常用测试夹具 -几个常用的与测试环境相关的夹具介绍。请将这些夹具加入到**测试项目所在目录**的 `conftest.py` 内。 +几个常用的与测试环境相关的夹具介绍。可直接在 `tests` 目录下的测试项目中使用这些夹具。 1. 指定测试项目的文件输出目录,例如,输出调试日志等信息。该夹具确保创建一个目录,并返回。若目录已存在,则清空目录(可选)。 From 46dfe06a85ac9968553f4811b04c6d254988e6ab Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Wed, 20 Nov 2024 16:11:23 +0800 Subject: [PATCH 135/187] =?UTF-8?q?=F0=9F=94=96=20v1.2.0a2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 +++++++- pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8274561c..5473a4ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,9 +70,15 @@ ## v1.1.1 -- 修复对权重RAM错误的配置 +- 修复对权重 RAM 错误的配置 ## v1.2.0a1 - 提高 `paicorelib` 依赖版本至 `~1.3` - 支持 ANN 网络的构建与部署 + +## v1.2.0a2 + +- 提高 `paicorelib` 依赖版本至 `>=1.3.1` +- 支持1D脉冲平均/最大池化算子 +- 重构路由算法,支持嵌套路由 diff --git a/pyproject.toml b/pyproject.toml index 3788a3d3..2a0bd1bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "paibox" -version = "1.2.0a1" +version = "1.2.0a2" description = "Toolchain of PAICORE 2.0" authors = ["Ziru Pan "] maintainers = [ From 1c441eea6627ed264781c5d8f7d96310cd205384 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Thu, 21 Nov 2024 15:31:59 +0800 Subject: [PATCH 136/187] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(synapses)?= =?UTF-8?q?:=20rename=20`MaxPool2dSemiFoldedSyn`=20to=20`MaxPoolSyn`,=20th?= =?UTF-8?q?e=20weight=20of=20max=20pooling=20syn=20must=20be=20a=20mask?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/components/functional.py | 26 ++++++++++---------- paibox/components/synapses/__init__.py | 7 +----- paibox/components/synapses/base.py | 8 +++--- paibox/components/synapses/transforms.py | 25 +++++++++++++------ tests/components/synapses/test_transforms.py | 6 ++--- 5 files changed, 39 insertions(+), 33 deletions(-) diff --git a/paibox/components/functional.py b/paibox/components/functional.py index 9438d839..5abcd7f1 100644 --- a/paibox/components/functional.py +++ b/paibox/components/functional.py @@ -37,7 +37,7 @@ from .neuron.neurons import * from .neuron.utils import vjt_overflow from .projection import InputProj -from .synapses import ConnType, Conv2dSemiFoldedSyn, FullConnSyn, MaxPool2dSemiFoldedSyn +from .synapses import ConnType, Conv2dSemiFoldedSyn, FullConnSyn, MaxPoolSyn from .synapses.conv_types import _Size1Type, _Size2Type from .synapses.conv_utils import _pair @@ -941,7 +941,7 @@ def build( syn1 = FullConnSyn( self.module_intf.operands[0], neuron, - weights=_delay_mapping(in_h, in_ch), + weights=_delay_mapping_mask(in_h, in_ch), conn_type=ConnType.All2All, name=f"s{i}_delay_{self.name}", ) @@ -1089,7 +1089,7 @@ def build( syn1 = FullConnSyn( self.module_intf.operands[0], neuron, - weights=_delay_mapping(in_h, cin), + weights=_delay_mapping_mask(in_h, cin), conn_type=ConnType.All2All, name=f"s{i}_delay_{self.name}", ) @@ -1124,7 +1124,7 @@ def build( syn1 = FullConnSyn( self.module_intf.operands[0], neuron, - weights=_delay_mapping(in_h, cin), + weights=_delay_mapping_mask(in_h, cin), conn_type=ConnType.All2All, name=f"s{p}_pad_{self.name}", ) @@ -1255,15 +1255,15 @@ def build( syn1 = FullConnSyn( self.module_intf.operands[0], neuron, - weights=_delay_mapping(in_h, cin), + weights=_delay_mapping_mask(in_h, cin), conn_type=ConnType.All2All, name=f"s{i}_delay_{self.name}", ) s_delays.append(syn1) - syn2 = MaxPool2dSemiFoldedSyn( + syn2 = MaxPoolSyn( neuron, pool2d, - weights=_poo2d_semifolded_mapping( + weights=_poo2d_semifolded_mapping_mask( cin, in_h, self.shape_out[1], @@ -1395,7 +1395,7 @@ def build( syn1 = FullConnSyn( self.module_intf.operands[0], neuron, - weights=_delay_mapping(in_h, cin), + weights=_delay_mapping_mask(in_h, cin), conn_type=ConnType.All2All, name=f"s{i}_delay_{self.name}", ) @@ -1403,7 +1403,7 @@ def build( syn2 = FullConnSyn( neuron, pool2d, - weights=_poo2d_semifolded_mapping( + weights=_poo2d_semifolded_mapping_mask( cin, in_h, out_h, kh, self.stride, self.padding ), conn_type=ConnType.All2All, @@ -1427,7 +1427,7 @@ def build( syn1 = FullConnSyn( self.module_intf.operands[0], neuron, - weights=_delay_mapping(in_h, cin), + weights=_delay_mapping_mask(in_h, cin), conn_type=ConnType.All2All, name=f"s{p}_pad_{self.name}", ) @@ -1436,7 +1436,7 @@ def build( syn2 = FullConnSyn( neuron, pool2d, - weights=-_poo2d_semifolded_mapping( + weights=-_poo2d_semifolded_mapping_mask( cin, in_h, out_h, kh, self.stride, self.padding ), conn_type=ConnType.All2All, @@ -1556,11 +1556,11 @@ def _transpose3d_mapping( return mt -def _delay_mapping(h: int, cin: int) -> WeightType: +def _delay_mapping_mask(h: int, cin: int) -> WeightType: return np.eye(cin * h, dtype=WEIGHT_DTYPE) -def _poo2d_semifolded_mapping( +def _poo2d_semifolded_mapping_mask( cin: int, ih: int, oh: int, diff --git a/paibox/components/synapses/__init__.py b/paibox/components/synapses/__init__.py index 7459cce0..bb75d14e 100644 --- a/paibox/components/synapses/__init__.py +++ b/paibox/components/synapses/__init__.py @@ -1,7 +1,2 @@ -from .base import ( - Conv2dSemiFoldedSyn, - FullConnectedSyn, - FullConnSyn, - MaxPool2dSemiFoldedSyn, -) +from .base import Conv2dSemiFoldedSyn, FullConnectedSyn, FullConnSyn, MaxPoolSyn from .transforms import ConnType diff --git a/paibox/components/synapses/base.py b/paibox/components/synapses/base.py index a444023a..5ef6da87 100644 --- a/paibox/components/synapses/base.py +++ b/paibox/components/synapses/base.py @@ -25,7 +25,7 @@ MaskedLinear, OneToOne, Transform, - _CompareMax, + CompareMax, ) RIGISTER_MASTER_KEY_FORMAT = "{0}.output" @@ -488,7 +488,9 @@ def __init__( ) -class MaxPool2dSemiFoldedSyn(FullConnectedSyn): +class MaxPoolSyn(FullConnectedSyn): + """Max pooling synapses. Only used when input width is 8-bit.""" + def __init__( self, source: Union[NeuDyn, InputProj], @@ -497,4 +499,4 @@ def __init__( name: Optional[str] = None, ) -> None: super().__init__(source, dest, name) - self.comm = _CompareMax((self.num_in, self.num_out), weights) + self.comm = CompareMax((self.num_in, self.num_out), weights) diff --git a/paibox/components/synapses/transforms.py b/paibox/components/synapses/transforms.py index df9c694a..d8448fef 100644 --- a/paibox/components/synapses/transforms.py +++ b/paibox/components/synapses/transforms.py @@ -44,6 +44,7 @@ "Conv2dSemiFoldedForward", "ConvTranspose1dForward", "ConvTranspose2dForward", + "CompareMax", ] @@ -583,7 +584,18 @@ def connectivity(self): ) -class _CompareMax(AllToAll): +class CompareMax(AllToAll): + def __init__(self, conn_size: Size2Type, mask: DataType) -> None: + """A transformation that finds the maximum of the input vector according to each column of the \ + mask matrix. + + NOTE: the value of mask matrix must be either 0 or 1. + """ + if not np.all((mask == 0) | (mask == 1)): + raise ValueError("the mask must be 0 or 1.") + + super().__init__(conn_size, mask) + def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: """The maximum value of the input corresponding to the non-zero columns of the weight matrix is \ taken as the output. @@ -592,13 +604,12 @@ def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: y = (y1, y2, ..., ym) """ if self.weights.ndim == 0: - output = self.weights * np.full( - (self.conn_size[1],), np.max(x, axis=None), dtype=VOLTAGE_DTYPE + output = np.full( + (self.conn_size[1],), + self.weights * np.max(x, axis=None), + dtype=VOLTAGE_DTYPE, ) else: - output = np.zeros((self.conn_size[1],), dtype=VOLTAGE_DTYPE) - for col in range(self.conn_size[1]): - col_result = x * self.weights[:, col].astype(VOLTAGE_DTYPE) - output[col] = np.max(col_result) + output = np.max(x[:, None] * self.weights, axis=0).astype(VOLTAGE_DTYPE) return output diff --git a/tests/components/synapses/test_transforms.py b/tests/components/synapses/test_transforms.py index 6eecdb4b..2171cf79 100644 --- a/tests/components/synapses/test_transforms.py +++ b/tests/components/synapses/test_transforms.py @@ -707,18 +707,16 @@ def test_ConvTranspose2dForward( @pytest.mark.parametrize("n_compare, n_group", [(4, 8), (9, 12), (25, 1)]) def test_CompareMax(self, n_compare, n_group): - from paibox.components.synapses.transforms import _CompareMax - n = n_compare * n_group w = np.zeros((n, n_group), dtype=np.int8) for i in range(n_group): w[n_compare * i : n_compare * (i + 1), i] = 1 - f = _CompareMax((n, n_group), w) + f = tfm.CompareMax((n, n_group), w) x = np.random.randint(0, 256, size=(n_compare, n_group), dtype=np.uint8) y1 = f(x.ravel(order="F")) # flatten in column-major order - expected = np.zeros((n_group,), dtype=np.int32) + expected = np.zeros((n_group,), dtype=np.uint8) for i in range(n_group): expected[i] = np.max(x[:, i]) From 9ea7301f4d14b0f19509f9503f77fc68d24ad1d8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 21 Nov 2024 07:32:47 +0000 Subject: [PATCH 137/187] :rotating_light: auto fix by pre-commit hooks --- docs/Support-Ops.md | 74 +++++++++++++++--------------- paibox/components/synapses/base.py | 2 +- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/docs/Support-Ops.md b/docs/Support-Ops.md index c1b16d62..29a06bd8 100644 --- a/docs/Support-Ops.md +++ b/docs/Support-Ops.md @@ -6,21 +6,21 @@ 芯片所支持的神经元配置项如下表所列: -| 支持功能 | 可写 | 取值 | 功能描述 | -| :-----------------------: | :--: | :------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 复位模式 | ✅ | 硬复位/软复位/不复位 | 硬复位,膜电平重置为正/负阈值
软复位,膜电平将减正阈值/加负阈值(若负阈值模式为复位模式)
不复位,膜电平保持不变 | -| 复位电平 | ✅ | 30比特有符号数 | 可配置复位电平 | -| 比较前后泄露 | ✅ | 前/后 | 阈值比较发生在泄露前/后 | -| 正阈值 | ✅ | 29比特无符号数 | 可配置正阈值 | -| 负阈值 | ✅ | 29比特无符号数 | 可配置负阈值 | -| 泄露电平 | ✅ | 30比特有符号数 | 可配置泄露幅值 | -| 反向泄露 | ✅ | 开启/关闭 | 若开启,泄露与当前膜电平符号相关:
当泄露值为正,膜电平向0收敛
当泄露值为负,膜电平偏离0发散 | -| 负阈值模式 | ✅ | 复位/饱和 | 当膜电平低于负阈值时:
为复位模式,根据复位模式复位
为饱和模式,膜电平重置为负阈值 | +| 支持功能 | 可写 | 取值 | 功能描述 | +| :-----------------------: | :--: | :------------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 复位模式 | ✅ | 硬复位/软复位/不复位 | 硬复位,膜电平重置为正/负阈值
软复位,膜电平将减正阈值/加负阈值(若负阈值模式为复位模式)
不复位,膜电平保持不变 | +| 复位电平 | ✅ | 30比特有符号数 | 可配置复位电平 | +| 比较前后泄露 | ✅ | 前/后 | 阈值比较发生在泄露前/后 | +| 正阈值 | ✅ | 29比特无符号数 | 可配置正阈值 | +| 负阈值 | ✅ | 29比特无符号数 | 可配置负阈值 | +| 泄露电平 | ✅ | 30比特有符号数 | 可配置泄露幅值 | +| 反向泄露 | ✅ | 开启/关闭 | 若开启,泄露与当前膜电平符号相关:
当泄露值为正,膜电平向0收敛
当泄露值为负,膜电平偏离0发散 | +| 负阈值模式 | ✅ | 复位/饱和 | 当膜电平低于负阈值时:
为复位模式,根据复位模式复位
为饱和模式,膜电平重置为负阈值 | | 膜电平截取位(仅ANN模式) | ✅ | [0,29] | 输出膜电平的截取位置T,30比特有符号膜电平需截取8比特作为输出:
T<8,截取[T-1:0],低位补0 
T=8,截取[7:0]
T≤29,截取[T-1:T-8]
膜电平大于窗口最高位则截断处理 | -| 随机轴突整合 | ✅ | 开启/关闭 | 若开启,神经元根据硬件生成的随机数\*过滤一些轴突上的输入,进行选择性累加 | -| 随机泄露 | ✅ | 开启/关闭 | 若开启,如果泄露幅值小于硬件生成的随机数\*,则此次泄露为0 | -| 阈值掩码 | ✅ | [0,29] | 若开启,硬件生成的随机数\*将和它求与后得到一个0\~29比特随机阈值,并加至神经元的正、负阈值上 | -| 膜电平 | ❌ | 0 | 只读寄存器,初始值为0 | +| 随机轴突整合 | ✅ | 开启/关闭 | 若开启,神经元根据硬件生成的随机数\*过滤一些轴突上的输入,进行选择性累加 | +| 随机泄露 | ✅ | 开启/关闭 | 若开启,如果泄露幅值小于硬件生成的随机数\*,则此次泄露为0 | +| 阈值掩码 | ✅ | [0,29] | 若开启,硬件生成的随机数\*将和它求与后得到一个0\~29比特随机阈值,并加至神经元的正、负阈值上 | +| 膜电平 | ❌ | 0 | 只读寄存器,初始值为0 | \*硬件生成的随机数均为无符号数。 @@ -34,26 +34,26 @@ | 算子类型 | ANN | SNN | 备注 | | :----------------------: | :-: | :-: | :----------: | -| 全连接 | ✅ | ✅ | | -| 2D矩阵乘法 | ✅ | ✅ | | -| 1D卷积 | ✅ | ✅ | 全展开形式 | -| 2D卷积 | ✅ | ✅ | 全展开形式 | -| 1D转置卷积 | ✅ | ✅ | 全展开形式 | -| 2D转置卷积 | ✅ | ✅ | 全展开形式 | -| 位与 | ❌ | ✅ | | -| 位或 | ❌ | ✅ | | -| 位非 | ❌ | ✅ | | -| 位异或 | ❌ | ✅ | | -| 1D平均池化 | ❌ | ✅ | 脉冲化 | -| 1D平均池化(膜电位相关) | ❌ | ✅ | 脉冲化 | -| 1D最大池化 | ❌ | ✅ | 脉冲化 | -| 2D平均池化 | ❌ | ✅ | 脉冲化 | -| 2D平均池化(膜电位相关) | ❌ | ✅ | 脉冲化 | -| 2D最大池化 | ❌ | ✅ | 脉冲化 | -| 脉冲加 | ❌ | ✅ | 针对脉冲序列 | -| 脉冲减 | ❌ | ✅ | 针对脉冲序列 | -| 线性层 | ✅ | ❌ | | -| 2D卷积 | ✅ | ❌ | 半折叠形式 | -| 2D最大池化 | ✅ | ❌ | 半折叠形式 | -| 2D平均池化 | ✅ | ❌ | 半折叠形式 | -| 线性层 | ✅ | ❌ | 半折叠形式 | +| 全连接 | ✅ | ✅ | | +| 2D矩阵乘法 | ✅ | ✅ | | +| 1D卷积 | ✅ | ✅ | 全展开形式 | +| 2D卷积 | ✅ | ✅ | 全展开形式 | +| 1D转置卷积 | ✅ | ✅ | 全展开形式 | +| 2D转置卷积 | ✅ | ✅ | 全展开形式 | +| 位与 | ❌ | ✅ | | +| 位或 | ❌ | ✅ | | +| 位非 | ❌ | ✅ | | +| 位异或 | ❌ | ✅ | | +| 1D平均池化 | ❌ | ✅ | 脉冲化 | +| 1D平均池化(膜电位相关) | ❌ | ✅ | 脉冲化 | +| 1D最大池化 | ❌ | ✅ | 脉冲化 | +| 2D平均池化 | ❌ | ✅ | 脉冲化 | +| 2D平均池化(膜电位相关) | ❌ | ✅ | 脉冲化 | +| 2D最大池化 | ❌ | ✅ | 脉冲化 | +| 脉冲加 | ❌ | ✅ | 针对脉冲序列 | +| 脉冲减 | ❌ | ✅ | 针对脉冲序列 | +| 线性层 | ✅ | ❌ | | +| 2D卷积 | ✅ | ❌ | 半折叠形式 | +| 2D最大池化 | ✅ | ❌ | 半折叠形式 | +| 2D平均池化 | ✅ | ❌ | 半折叠形式 | +| 线性层 | ✅ | ❌ | 半折叠形式 | diff --git a/paibox/components/synapses/base.py b/paibox/components/synapses/base.py index 5ef6da87..01fd912f 100644 --- a/paibox/components/synapses/base.py +++ b/paibox/components/synapses/base.py @@ -15,6 +15,7 @@ from .conv_utils import _fm_ndim1_check, _fm_ndim2_check from .transforms import ( AllToAll, + CompareMax, ConnType, Conv1dForward, Conv2dForward, @@ -25,7 +26,6 @@ MaskedLinear, OneToOne, Transform, - CompareMax, ) RIGISTER_MASTER_KEY_FORMAT = "{0}.output" From a49c074106017209825dc31dcfd7e2c3496d65f3 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Tue, 26 Nov 2024 15:30:59 +0800 Subject: [PATCH 138/187] =?UTF-8?q?=F0=9F=94=A8=20use=20`.source`=20to=20g?= =?UTF-8?q?et=20the=20input=20info=20of=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/components/_modules.py | 10 +++---- paibox/components/functional.py | 47 ++++++++++++++++++--------------- paibox/components/modules.py | 23 +++++++--------- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/paibox/components/_modules.py b/paibox/components/_modules.py index 4ec40ed1..a2059b95 100644 --- a/paibox/components/_modules.py +++ b/paibox/components/_modules.py @@ -125,7 +125,7 @@ def build(self, network: "DynSysGroup", **build_options) -> BuiltComponentType: n_delaychain.append(n_out) # Must append to the last. syn_in = FullConnSyn( - self.module_intf.operands[0], + self.source[0], n_delaychain[0], 1, conn_type=ConnType.One2One, @@ -312,7 +312,7 @@ def build(self, network: "DynSysGroup", **build_options) -> BuiltComponentType: ) syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], n1_p1d, weights=self.tfm.connectivity.astype(np.bool_), conn_type=ConnType.All2All, @@ -391,7 +391,7 @@ def build(self, network: "DynSysGroup", **build_options) -> BuiltComponentType: ) syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], n1_p1d, weights=self.tfm.connectivity.astype(np.bool_), conn_type=ConnType.All2All, @@ -482,7 +482,7 @@ def build(self, network: "DynSysGroup", **build_options) -> BuiltComponentType: ) syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], n1_p2d, weights=self.tfm.connectivity.astype(np.bool_), conn_type=ConnType.All2All, @@ -566,7 +566,7 @@ def build(self, network: "DynSysGroup", **build_options) -> BuiltComponentType: ) syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], n1_p2d, weights=self.tfm.connectivity.astype(np.bool_), conn_type=ConnType.All2All, diff --git a/paibox/components/functional.py b/paibox/components/functional.py index 5abcd7f1..e39cbd0e 100644 --- a/paibox/components/functional.py +++ b/paibox/components/functional.py @@ -123,14 +123,14 @@ def build(self, network: "DynSysGroup", **build_options) -> BuiltComponentType: ) syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], n1_and, 1, conn_type=ConnType.One2One, name=f"s0_{self.name}", ) syn2 = FullConnSyn( - self.module_intf.operands[1], + self.source[1], n1_and, 1, conn_type=ConnType.One2One, @@ -193,7 +193,7 @@ def build(self, network: "DynSysGroup", **build_options) -> BuiltComponentType: ) syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], n1_not, weights=-1, conn_type=ConnType.One2One, @@ -244,14 +244,14 @@ def build(self, network: "DynSysGroup", **build_options) -> BuiltComponentType: ) syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], n1_or, 1, conn_type=ConnType.One2One, name=f"s0_{self.name}", ) syn2 = FullConnSyn( - self.module_intf.operands[1], + self.source[1], n1_or, 1, conn_type=ConnType.One2One, @@ -308,7 +308,7 @@ def build(self, network: "DynSysGroup", **build_options) -> BuiltComponentType: identity = np.identity(self.num_out, dtype=np.int8) # weight of syn1, (-1*(N,), 1*(N,)) syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], n1_aux, weights=np.hstack([-1 * identity, identity], casting="safe", dtype=np.int8), conn_type=ConnType.All2All, @@ -316,7 +316,7 @@ def build(self, network: "DynSysGroup", **build_options) -> BuiltComponentType: ) # weight of syn2, (1*(N,), -1*(N,)) syn2 = FullConnSyn( - self.module_intf.operands[1], + self.source[1], n1_aux, weights=np.hstack([identity, -1 * identity], casting="safe", dtype=np.int8), conn_type=ConnType.All2All, @@ -415,14 +415,14 @@ def build(self, network: "DynSysGroup", **build_options) -> BuiltComponentType: ) syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], n1_sadd, self.factor_a, conn_type=ConnType.One2One, name=f"s0_{self.name}", ) syn2 = FullConnSyn( - self.module_intf.operands[1], + self.source[1], n1_sadd, self.factor_b, conn_type=ConnType.One2One, @@ -712,14 +712,14 @@ def build(self, network: "DynSysGroup", **build_options) -> BuiltComponentType: ) syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], n1_ssub, self.factor_a, conn_type=ConnType.One2One, name=f"s0_{self.name}", ) syn2 = FullConnSyn( - self.module_intf.operands[1], + self.source[1], n1_ssub, self.factor_b, conn_type=ConnType.One2One, @@ -781,7 +781,7 @@ def build(self, network: "DynSysGroup", **build_options) -> BuiltComponentType: ) syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], n1_t2d, weights=_transpose2d_mapping(self.shape_in), conn_type=ConnType.All2All, @@ -848,7 +848,7 @@ def build(self, network: "DynSysGroup", **build_options) -> BuiltComponentType: ) syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], n1_t3d, weights=_transpose3d_mapping(self.shape_in, self.axes), conn_type=ConnType.All2All, @@ -886,7 +886,7 @@ def build(self, network: "DynSysGroup", **build_options) -> BuiltComponentType: name=f"nd_{self.name}", ) syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], neuron_d, weights=self.weights, conn_type=ConnType.All2All, @@ -974,6 +974,7 @@ def __init__( padding: _Size2Type = 0, bias: DataType = 0, bit_trunc: int = 8, + *, keep_shape: bool = False, name: Optional[str] = None, **kwargs, @@ -981,7 +982,7 @@ def __init__( """2d semi-folded convolution for ANN mode. Args: - neuron_s: source neuron. The dimensions need to be expressed explicitly as (C,H,W). + neuron_s: source neuron. The dimensions need to be expressed explicitly as (C,H) or (C,W). kernel: convolution kernel in (O,I,H,W) order. stride: the step size of the kernel sliding. It can be a scalar or a tuple of 2 integers. padding: the amount of zero-padding applied to the input. It can be a scalar or a tuple of 2 integers. @@ -1036,8 +1037,8 @@ def build( ts_first_valid_inp: int, **build_options, ) -> BuiltComponentType: - assert len(self.module_intf.operands[0].shape_out) == 2 - # if len(self.module_intf.operands[0].shape_out) != 2: + assert len(self.source[0].shape_out) == 2 + # if len(self.source[0].shape_out) != 2: # in_ch, in_h, in_w = _fm_ndim2_check( # self.module_intf.operands[0].shape_out, "CHW" # ) @@ -1087,7 +1088,7 @@ def build( n_delays.append(neuron) # delay synapses syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], neuron, weights=_delay_mapping_mask(in_h, cin), conn_type=ConnType.All2All, @@ -1122,7 +1123,7 @@ def build( n_neg_padding.append(neuron) # delay synapses syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], neuron, weights=_delay_mapping_mask(in_h, cin), conn_type=ConnType.All2All, @@ -1162,6 +1163,7 @@ def __init__( neuron_s: Union[NeuDyn, InputProj], kernel_size: _Size2Type, stride: Optional[_Size2Type] = None, + *, keep_shape: bool = False, name: Optional[str] = None, **kwargs, @@ -1253,7 +1255,7 @@ def build( n_delays.append(neuron) # delay synapses syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], neuron, weights=_delay_mapping_mask(in_h, cin), conn_type=ConnType.All2All, @@ -1290,6 +1292,7 @@ def __init__( kernel_size: _Size2Type, stride: Optional[_Size2Type] = None, padding: _Size2Type = 0, + *, keep_shape: bool = False, name: Optional[str] = None, **kwargs, @@ -1393,7 +1396,7 @@ def build( n_delays.append(neuron) # delay synapses syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], neuron, weights=_delay_mapping_mask(in_h, cin), conn_type=ConnType.All2All, @@ -1425,7 +1428,7 @@ def build( n_neg_padding.append(neuron) # delay synapses syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], neuron, weights=_delay_mapping_mask(in_h, cin), conn_type=ConnType.All2All, diff --git a/paibox/components/modules.py b/paibox/components/modules.py index 09bdf247..41e06fdc 100644 --- a/paibox/components/modules.py +++ b/paibox/components/modules.py @@ -4,7 +4,7 @@ from collections.abc import Sequence from dataclasses import dataclass, field from functools import partial -from typing import ClassVar, Literal, Optional, TypeVar, Union +from typing import Callable, ClassVar, Literal, Optional, TypeVar, Union import numpy as np from paicorelib import TM, CoreMode, HwConfig, SNNModeEnable, get_core_mode @@ -100,6 +100,7 @@ def __init__( tick_wait_start: int, tick_wait_end: int, unrolling_factor: int, + keep_shape: bool, name: Optional[str] = None, ) -> None: super().__init__(name) @@ -108,6 +109,7 @@ def __init__( self._tws = tick_wait_start self._twe = tick_wait_end self._uf = unrolling_factor + self.keep_shape = keep_shape def __call__(self, *args, **kwargs): return self.update(*args, **kwargs) @@ -179,9 +181,7 @@ def __init__( op.register_output(self) - super().__init__(**kwargs, name=name) - - self.keep_shape = keep_shape + super().__init__(**kwargs, keep_shape=keep_shape, name=name) self._shape_out = shape_out self.register_operand(*operands) @@ -198,12 +198,7 @@ def __init__( # Set a deque for the `synin` to implement the delay of `inherent_delay` for the module. if self.inherent_delay > 0: _init_synin = [ - self.n_op - * [ - np.zeros( - self.module_intf.operands[0].num_out, dtype=NEUOUT_U8_DTYPE - ) - ] + self.n_op * [np.zeros(self.source[0].num_out, dtype=NEUOUT_U8_DTYPE)] ] else: _init_synin = [] @@ -215,7 +210,7 @@ def __init__( def get_inputs(self) -> None: synin = [] - for op in self.module_intf.operands: + for op in self.source: # Retrieve the spike at index `timestamp` of the dest neurons if self.is_working(): if isinstance(op, InputProj): @@ -255,7 +250,7 @@ def _rebuild_out_intf( ) -> None: from .synapses import FullConnectedSyn - for out in self.module_intf.output: + for out in self.target: if isinstance(out, FullConnectedSyn): out.source = out_neuron else: @@ -443,7 +438,9 @@ def __init__( _T = TypeVar("_T", bound=NeuModule) -def set_rt_mode(input_width: L[1, 8], spike_width: L[1, 8], snn_en: L[0, 1]): +def set_rt_mode( + input_width: L[1, 8], spike_width: L[1, 8], snn_en: L[0, 1] +) -> Callable[[type[_T]], type[_T]]: def wrapper(cls: type[_T]) -> type[_T]: iw = _input_width_format(input_width) sw = _spike_width_format(spike_width) From cf939cff919f522e41ac8eee535f30c24a42ebd7 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Tue, 26 Nov 2024 16:10:54 +0800 Subject: [PATCH 139/187] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(PAIGraph)?= =?UTF-8?q?:=20building=20process=20of=20modules,especially=20semi-folded?= =?UTF-8?q?=20ops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/graphs.py | 36 +++++-- paibox/components/_modules.py | 50 +++++++--- paibox/components/functional.py | 172 ++++++++++++++------------------ paibox/network.py | 119 +++++++++++++--------- 4 files changed, 210 insertions(+), 167 deletions(-) diff --git a/paibox/backend/graphs.py b/paibox/backend/graphs.py index b0c1950d..335b95cf 100644 --- a/paibox/backend/graphs.py +++ b/paibox/backend/graphs.py @@ -8,6 +8,7 @@ from paibox.collector import Collector from paibox.components import FullConnectedSyn, InputProj, NeuModule, Neuron +from paibox.components.functional import LinearSemiFolded from paibox.exceptions import GraphBuildError, GraphConnectionError, NotSupportedError from paibox.network import DynSysGroup from paibox.utils import check_elem_unique @@ -113,9 +114,32 @@ def _pre_build(self, **build_options) -> None: # Check the hardware resource limits of operators in the network during the build phase. build_options.setdefault("check_before_compile", True) - # Build functional modules in the subnets - for subnet in self._raw_networks: - DynSysGroup.build_fmodule(subnet, **build_options) + # Build functional modules for each network. + for network in self._raw_networks: + if network.is_composed_of_semi_folded_ops(): + modules = network.components.subset(NeuModule) + succ_dg_semi_ops = { + name: [t.name for t in op.target] for name, op in modules.items() + } + pred_dg_semi_ops = reverse_edges(succ_dg_semi_ops) + + # XXX Networks consisting entirely of semi-folded operators require some additional topology + # checks. These additional checks may be removed as more network structures will be supported. + + # Currently, `LinearSemiFolded` is at the end of the network, since it will change the form of + # the input data stream, and its effective output is at the same time. + semi_linears = modules.subset(LinearSemiFolded) + if not all( + len(succ_dg_semi_ops[linear]) == 0 for linear in semi_linears + ): + raise NotSupportedError( + "currently, the semi-folded linear can only be used as output of the network." + ) + + ordered_nodes = [modules[name] for name in toposort(succ_dg_semi_ops)] + network.build_modules(pred_dg_semi_ops, ordered_nodes, **build_options) + else: + network.build_modules(**build_options) def _update_graph(self, **build_options) -> None: self.clear(total=False) @@ -125,7 +149,7 @@ def _update_graph(self, **build_options) -> None: self.succ_dg[node] = dict() self.pred_dg[node] = dict() - for syn in self._raw_edges.values(): + for name, syn in self._raw_edges.items(): u, v = syn.source.name, syn.dest.name if u not in self._raw_nodes: raise GraphConnectionError( @@ -138,6 +162,7 @@ def _update_graph(self, **build_options) -> None: ) _edge_attr = EdgeAttr(edge=syn, distance=syn.source.delay_relative) + self.edges[name] = _edge_attr self.succ_dg[u][v] = _edge_attr self.pred_dg[v][u] = _edge_attr @@ -158,9 +183,6 @@ def _update_graph(self, **build_options) -> None: degree=self.degree_of_nodes[name], ) - for name, syn in self._raw_edges.items(): - self.edges[name] = EdgeAttr(edge=syn, distance=syn.source.delay_relative) - self.ordered_nodes = toposort(self.succ_dg) self.has_built = True diff --git a/paibox/components/_modules.py b/paibox/components/_modules.py index a2059b95..e20b6aa6 100644 --- a/paibox/components/_modules.py +++ b/paibox/components/_modules.py @@ -1,6 +1,7 @@ +from dataclasses import dataclass import math import typing -from typing import Literal, Optional, Protocol, Union +from typing import Literal, Optional, Union import numpy as np from paicorelib import TM, HwConfig @@ -57,6 +58,7 @@ "_SpikingPool2dWithV", "_SemiFoldedModule", "_LinearBase", + "SemiFoldedStreamAttr", ] @@ -159,24 +161,44 @@ class _DelayChainANN(_DelayChainBase): pass -class _HasSemiFoldedIntf(Protocol): - """The front of this module has replication & delay interface for semi-folded operators.""" +@dataclass(frozen=True) +class SemiFoldedStreamAttr: + """Details of transmission of valid data in semi-folded form data stream.""" + + t_1st_vld: int + """The time of the first valid data, relative to `t_1st_vld` of the external input.""" + interval: int + """The interval of the output data stream.""" + n_data: int = 0 + """The number of valid output data.""" + + def t_at(self, n: int) -> int: + """The time of the n-th valid data.""" + if self.n_data > 0: + assert 1 <= n <= self.n_data + + return self.t_1st_vld + (n - 1) * self.interval + + @property + def t_last_vld(self) -> int: + """The time of the last valid data.""" + assert self.n_data > 0 + return self.t_at(self.n_data) + + +@set_rt_mode_ann() +class _SemiFoldedModule(FunctionalModule): + """Functional modules with interfaces in semi-folded form. Use `build()` of class `HasSemiFoldedIntf`.""" + + ostream_attr: SemiFoldedStreamAttr def build( self, network: "DynSysGroup", - valid_interval: int, - ts_first_valid_inp: int, + incoming_stream_attr: SemiFoldedStreamAttr, **build_options, - ) -> BuiltComponentType: ... - - -@set_rt_mode_ann() -class _SemiFoldedModule(FunctionalModule, _HasSemiFoldedIntf): - valid_interval: int = 1 - """The interval of valid output data.""" - ts_1st_valid_out: int = 0 - """The timestamp of the first valid output data.""" + ) -> BuiltComponentType: + raise NotImplementedError def _input_buffer_len_check( self, in_channels: int, in_h: int, kw: int, valid_interval: int diff --git a/paibox/components/functional.py b/paibox/components/functional.py index e39cbd0e..423af93c 100644 --- a/paibox/components/functional.py +++ b/paibox/components/functional.py @@ -902,16 +902,16 @@ def build(self, network: "DynSysGroup", **build_options) -> BuiltComponentType: class LinearSemiFolded(_LinearBase, _SemiFoldedModule): "This operator is used on the first fully-connected layer after the semi-folded convolution." - def spike_func(self, x1: NeuOutType, **kwargs) -> NeuOutType: - raise NotImplementedError - def build( - self, network: "DynSysGroup", valid_interval: int, **build_options + self, + network: "DynSysGroup", + incoming_stream_attr: SemiFoldedStreamAttr, + **build_options, ) -> BuiltComponentType: - assert len(self.module_intf.operands[0].shape_out) == 2 - self.valid_interval = valid_interval + assert len(self.source[0].shape_out) == 2 + self.ostream_attr = incoming_stream_attr - in_ch, in_h = self.module_intf.operands[0].shape_out + ich, ih = self.source[0].shape_out n_delays = NodeList() s_delays = NodeList() s_weight = NodeList() @@ -927,10 +927,10 @@ def build( name=f"nd_{self.name}", ) - for i in range(in_h): + for i in range(ih): neuron = ANNBypassNeuron( - shape=(in_ch, in_h), - delay=valid_interval * i + 1, + shape=(ich, ih), + delay=incoming_stream_attr.interval * i + 1, tick_wait_start=self.tick_wait_start, tick_wait_end=self.tick_wait_end, keep_shape=self.keep_shape, @@ -939,15 +939,15 @@ def build( n_delays.append(neuron) # Delay synapses syn1 = FullConnSyn( - self.module_intf.operands[0], + self.source[0], neuron, - weights=_delay_mapping_mask(in_h, in_ch), + weights=_delay_mapping_mask(ih, ich), conn_type=ConnType.All2All, name=f"s{i}_delay_{self.name}", ) s_delays.append(syn1) - w = self.weights[in_h - i - 1 :: in_h, :] + w = self.weights[ih - i - 1 :: ih, :] syn2 = FullConnSyn( neuron, n_fc, @@ -1027,37 +1027,31 @@ def __init__( neuron_s, shape_out=_shape_out, keep_shape=keep_shape, name=name, **kwargs ) - def spike_func(self, x1: NeuOutType, **kwargs) -> NeuOutType: - raise NotImplementedError - def build( self, network: "DynSysGroup", - valid_interval: int, - ts_first_valid_inp: int, + incoming_stream_attr: SemiFoldedStreamAttr, **build_options, ) -> BuiltComponentType: assert len(self.source[0].shape_out) == 2 # if len(self.source[0].shape_out) != 2: # in_ch, in_h, in_w = _fm_ndim2_check( - # self.module_intf.operands[0].shape_out, "CHW" + # self.source[0].shape_out, "CHW" # ) - # self.module_intf.operands[0].shape_change((in_ch, in_h)) - self.valid_interval = valid_interval - _, in_h = self.module_intf.operands[0].shape_out + # self.source[0].shape_change((in_ch, in_h)) + _, ih = self.source[0].shape_out _, cin, _, kw = self.kernel.shape + _, ow = self.shape_out - self.ts_1st_valid_out = ( - ts_first_valid_inp + (kw - 1 - self.padding[0]) * valid_interval - ) - twe = ( - 1 - + self.ts_1st_valid_out - + (self.shape_out[1] - 1) * valid_interval * self.stride[1] + self.ostream_attr = SemiFoldedStreamAttr( + incoming_stream_attr.t_at(kw - self.padding[0]), + incoming_stream_attr.interval * self.stride[1], + ow, ) + twe = 1 + self.ostream_attr.t_last_vld if build_options.get("check_before_compile"): - self._input_buffer_len_check(cin, in_h, kw, valid_interval) + self._input_buffer_len_check(cin, ih, kw, incoming_stream_attr.interval) n_delays = NodeList() n_neg_padding = NodeList() @@ -1078,11 +1072,10 @@ def build( for i in range(kw): neuron = ANNBypassNeuron( - (cin, in_h), - delay=valid_interval * i + 1, + (cin, ih), + delay=incoming_stream_attr.interval * i + 1, tick_wait_start=self.tick_wait_start, tick_wait_end=twe, - keep_shape=self.keep_shape, name=f"n{i}_delay_{self.name}", ) n_delays.append(neuron) @@ -1090,7 +1083,7 @@ def build( syn1 = FullConnSyn( self.source[0], neuron, - weights=_delay_mapping_mask(in_h, cin), + weights=_delay_mapping_mask(ih, cin), conn_type=ConnType.All2All, name=f"s{i}_delay_{self.name}", ) @@ -1108,15 +1101,15 @@ def build( s_kernel.append(syn2) # Add additional negative padding layer to eliminate the incorrect output - # NOTE: ts_first_valid_inp = 0 & padding[0] > 0 means the previous layer is + # NOTE: `t_1st_vld` = 0 & `padding[0]` > 0 means the previous layer is # an input node. No need to add negative padding layer for this case. - if ts_first_valid_inp > 0: + if incoming_stream_attr.t_1st_vld > 0: for p in range(self.padding[0]): neuron = ANNBypassNeuron( - (cin, in_h), - delay=valid_interval * (kw - 1 - p) + 1, + (cin, ih), + delay=1 + incoming_stream_attr.interval * (kw - 1 - p), tick_wait_start=self.tick_wait_start, - tick_wait_end=ts_first_valid_inp, + tick_wait_end=incoming_stream_attr.t_1st_vld, keep_shape=self.keep_shape, name=f"n{p}_pad_{self.name}", ) @@ -1125,7 +1118,7 @@ def build( syn1 = FullConnSyn( self.source[0], neuron, - weights=_delay_mapping_mask(in_h, cin), + weights=_delay_mapping_mask(ih, cin), conn_type=ConnType.All2All, name=f"s{p}_pad_{self.name}", ) @@ -1198,37 +1191,31 @@ def __init__( **kwargs, ) - def spike_func(self, x1: NeuOutType, **kwargs) -> NeuOutType: - raise NotImplementedError - def build( self, network: "DynSysGroup", - valid_interval: int, - ts_first_valid_inp: int, + incoming_stream_attr: SemiFoldedStreamAttr, **build_options, ) -> BuiltComponentType: - assert len(self.module_intf.operands[0].shape_out) == 2 - # if len(self.module_intf.operands[0].shape_out) != 2: + assert len(self.source[0].shape_out) == 2 + # if len(self.source[0].shape_out) != 2: # in_ch, in_h, in_w = _fm_ndim2_check( - # self.module_intf.operands[0].shape_out, "CHW" + # self.source[0].shape_out, "CHW" # ) - # self.module_intf.operands[0].shape_change((in_ch, in_h)) - self.valid_interval = valid_interval - - in_ch, in_h = self.module_intf.operands[0].shape_out - cin = in_ch - _, kw = self.kernel_size - - self.ts_1st_valid_out = ts_first_valid_inp + (kw - 1) * valid_interval - twe = ( - 1 - + self.ts_1st_valid_out - + (self.shape_out[1] - 1) * valid_interval * self.stride[1] + # self.source[0].shape_change((in_ch, in_h)) + cin, ih = self.source[0].shape_out + kh, kw = self.kernel_size + _, ow = self.shape_out + + self.ostream_attr = SemiFoldedStreamAttr( + incoming_stream_attr.t_at(kw), + incoming_stream_attr.interval * self.stride[1], + ow, ) + twe = 1 + self.ostream_attr.t_last_vld if build_options.get("check_before_compile"): - self._input_buffer_len_check(cin, in_h, kw, valid_interval) + self._input_buffer_len_check(cin, ih, kw, incoming_stream_attr.interval) n_delays = NodeList() s_delays = NodeList() @@ -1245,8 +1232,8 @@ def build( for i in range(kw): neuron = ANNBypassNeuron( - (cin, in_h), - delay=valid_interval * i + 1, + (cin, ih), + delay=incoming_stream_attr.interval * i + 1, tick_wait_start=self.tick_wait_start, tick_wait_end=twe, keep_shape=self.keep_shape, @@ -1257,7 +1244,7 @@ def build( syn1 = FullConnSyn( self.source[0], neuron, - weights=_delay_mapping_mask(in_h, cin), + weights=_delay_mapping_mask(ih, cin), conn_type=ConnType.All2All, name=f"s{i}_delay_{self.name}", ) @@ -1266,12 +1253,7 @@ def build( neuron, pool2d, weights=_poo2d_semifolded_mapping_mask( - cin, - in_h, - self.shape_out[1], - self.kernel_size[0], - self.stride, - (0, 0), + cin, ih, ow, kh, self.stride, (0, 0) ), name=f"s{i}_{self.name}", ) @@ -1327,35 +1309,31 @@ def __init__( **kwargs, ) - def spike_func(self, x1: NeuOutType, **kwargs) -> NeuOutType: - raise NotImplementedError - def build( self, network: "DynSysGroup", - valid_interval: int, - ts_first_valid_inp: int, + incoming_stream_attr: SemiFoldedStreamAttr, **build_options, ) -> BuiltComponentType: - assert len(self.module_intf.operands[0].shape_out) == 2 - # if len(self.module_intf.operands[0].shape_out) != 2: + assert len(self.source[0].shape_out) == 2 + # if len(self.source[0].shape_out) != 2: # in_ch, in_h, in_w = _fm_ndim2_check( - # self.module_intf.operands[0].shape_out, "CHW" + # self.source[0].shape_out, "CHW" # ) - # self.module_intf.operands[0].shape_change((in_ch, in_h)) - self.valid_interval = valid_interval - in_ch, in_h = self.module_intf.operands[0].shape_out - cin = in_ch + # self.source[0].shape_change((in_ch, in_h)) + cin, ih = self.source[0].shape_out kh, kw = self.kernel_size - out_h = self.shape_out[1] + _, ow = self.shape_out - self.ts_1st_valid_out = ( - ts_first_valid_inp + (kw - 1 - self.padding[0]) * valid_interval + self.ostream_attr = SemiFoldedStreamAttr( + incoming_stream_attr.t_at(kw - self.padding[0]), + incoming_stream_attr.interval * self.stride[1], + ow, ) - twe = 1 + self.ts_1st_valid_out + (out_h - 1) * valid_interval * self.stride[1] + twe = 1 + self.ostream_attr.t_last_vld if build_options.get("check_before_compile"): - self._input_buffer_len_check(cin, in_h, kw, valid_interval) + self._input_buffer_len_check(cin, ih, kw, incoming_stream_attr.interval) # NOTE: Division is achieved with the help of output truncation. # TODO Since division with a divisor that is an integer power of 2 can only be implemented by @@ -1386,8 +1364,8 @@ def build( ) for i in range(kw): neuron = ANNBypassNeuron( - (cin, in_h), - delay=valid_interval * i + 1, + (cin, ih), + delay=incoming_stream_attr.interval * i + 1, tick_wait_start=self.tick_wait_start, tick_wait_end=twe, keep_shape=self.keep_shape, @@ -1398,7 +1376,7 @@ def build( syn1 = FullConnSyn( self.source[0], neuron, - weights=_delay_mapping_mask(in_h, cin), + weights=_delay_mapping_mask(ih, cin), conn_type=ConnType.All2All, name=f"s{i}_delay_{self.name}", ) @@ -1407,7 +1385,7 @@ def build( neuron, pool2d, weights=_poo2d_semifolded_mapping_mask( - cin, in_h, out_h, kh, self.stride, self.padding + cin, ih, ow, kh, self.stride, self.padding ), conn_type=ConnType.All2All, name=f"s{i}_{self.name}", @@ -1415,13 +1393,13 @@ def build( s_delays.append(syn2) # Add additional negative padding layer to eliminate the incorrect output - if ts_first_valid_inp > 0: + if incoming_stream_attr.t_1st_vld > 0: for p in range(self.padding[0]): neuron = ANNBypassNeuron( - (cin, in_h), - delay=valid_interval * (kw - 1 - p) + 1, + (cin, ih), + delay=1 + incoming_stream_attr.interval * (kw - 1 - p), tick_wait_start=self.tick_wait_start, - tick_wait_end=ts_first_valid_inp, + tick_wait_end=incoming_stream_attr.t_1st_vld, keep_shape=self.keep_shape, name=f"n{p}_pad_{self.name}", ) @@ -1430,7 +1408,7 @@ def build( syn1 = FullConnSyn( self.source[0], neuron, - weights=_delay_mapping_mask(in_h, cin), + weights=_delay_mapping_mask(ih, cin), conn_type=ConnType.All2All, name=f"s{p}_pad_{self.name}", ) @@ -1440,7 +1418,7 @@ def build( neuron, pool2d, weights=-_poo2d_semifolded_mapping_mask( - cin, in_h, out_h, kh, self.stride, self.padding + cin, ih, ow, kh, self.stride, self.padding ), conn_type=ConnType.All2All, name=f"neg_s{i}_{self.name}", diff --git a/paibox/network.py b/paibox/network.py index e23defd1..4f25a48c 100644 --- a/paibox/network.py +++ b/paibox/network.py @@ -1,17 +1,15 @@ +from collections.abc import Sequence import sys from typing import Optional, Union import numpy as np +from .exceptions import NotSupportedError + from .base import DynamicSys, SynSys from .collector import Collector +from .components._modules import _SemiFoldedModule, SemiFoldedStreamAttr from .components import NeuModule, Neuron, Projection -from .components.functional import ( - AvgPool2dSemiFolded, - Conv2dSemiFolded, - LinearSemiFolded, - MaxPool2dSemiFolded, -) from .components.modules import BuiltComponentType from .mixin import Container from .node import NodeDict, NodeList @@ -81,39 +79,65 @@ def reset_state(self) -> None: def __call__(self, **kwargs) -> None: return self.update(**kwargs) - @classmethod - def build_fmodule( - cls, network: "DynSysGroup", **build_options + def build_modules( + self, + pred_dg_semi_ops: Optional[dict[str, list[str]]] = None, + ordered_semi_ops: Optional[list[NeuModule]] = None, + **build_options, ) -> dict[NeuModule, BuiltComponentType]: + """Build the functional modules in the network. + + Args: + pred_dg_semi_ops (dict[str, list[str]], None): The predecessor directed graph of semi-folded operators. + ordered_semi_ops (list[NeuModule], None): The ordered semi-folded operators. + + Returns: + built_components (dict[NeuModule, BuiltComponentType]): The dictionary of generated basic components after building. + """ + if pred_dg_semi_ops is not None and ordered_semi_ops is not None: + # It is the network composed of all semi-folded operators. + modules = ordered_semi_ops + else: + # It is the network composed of general operators. + modules = list(self.components.subset(NeuModule).unique().values()) + generated = dict() - modules = network.nodes().subset(NeuModule).unique() - - # Valid interval for semi-folded components - # If the input data is input continuously on the W-axis, the initial - # valid interval for the first semi-folded component is 1. - semi_valid_interval = 1 - ts_1st_valid_out = 0 - - for module in modules.values(): - if isinstance( - module, (Conv2dSemiFolded, MaxPool2dSemiFolded, AvgPool2dSemiFolded) - ): - generated[module] = module.build( - network, semi_valid_interval, ts_1st_valid_out, **build_options - ) - semi_valid_interval *= module.stride[1] - ts_1st_valid_out = module.ts_1st_valid_out - elif isinstance(module, LinearSemiFolded): - generated[module] = module.build( - network, semi_valid_interval, **build_options - ) - else: - generated[module] = module.build(network, **build_options) - network._remove_modules_from_containers(network, modules) + # For external input stream info: + # 1. The start time is 1 + # 2. The interval is 1 + # 3. The #N of data is -1 since it dosen't effect the subsequent output stream. + # TODO Reserve an interface for setting the properties of external input from `FRONTEND_ENV`? + last_vld_output_attr = SemiFoldedStreamAttr(0, 1) + + for m in modules: + # TODO for the case of the ResBlock, the `pred_dg_semi_ops` will be used. + if isinstance(m, _SemiFoldedModule): + generated[m] = m.build(self, last_vld_output_attr, **build_options) + last_vld_output_attr = m.ostream_attr + else: + generated[m] = m.build(self, **build_options) + self._remove_modules(modules) return generated + def is_composed_of_semi_folded_ops(self) -> bool: + """Check if the network consists entirely or not of semi-folded operators. Return true if all the \ + components are semi-folded operators. Return false if all the components are not semi-folded. \ + In other cases, an exception will be raised. + """ + if all(isinstance(cpn, _SemiFoldedModule) for cpn in self.components.values()): + return True + elif not all( + isinstance(cpn, _SemiFoldedModule) for cpn in self.components.values() + ): + return False + else: + # XXX It seems that there will be no network mixed with semi-folded operators at present. + raise NotSupportedError( + "mixed semi-folded & normal operators in the network is not supported." + ) + def _add_components(self, *implicit: DynamicSys, **explicit: DynamicSys) -> None: """Add new components. When the component is passed in explicitly, its tag name can \ be specified. When passing in implicitly, its attribute `.name` will be used. @@ -141,22 +165,19 @@ def _ignore_components(self, *components: DynamicSys) -> None: if cpn in self.__dict__.values(): cpn.__gh_build_ignore__ = True - @staticmethod - def _remove_modules_from_containers( - network: "DynSysGroup", modules: Collector[str, NeuModule] - ) -> None: - """Remove the built modules from the node containers of the network.""" - node_lists = [v for v in network.__dict__.values() if isinstance(v, NodeList)] - node_dicts = [v for v in network.__dict__.values() if isinstance(v, NodeDict)] - - for module in modules.values(): - for lst in node_lists: - if module in lst: - lst.remove(module) - - for dct in node_dicts: - if module in dct.values(): - dct.pop(module) + def _remove_modules(self, modules: Sequence[NeuModule]) -> None: + """Remove the built modules from the network.""" + node_lst = [v for v in self.__dict__.values() if isinstance(v, NodeList)] + node_dct = [v for v in self.__dict__.values() if isinstance(v, NodeDict)] + + for m in modules: + for lst in node_lst: + if m in lst: + lst.remove(m) + + for dct in node_dct: + if m in dct.values(): + dct.pop(m) @property def components(self) -> Collector[str, DynamicSys]: From 2498cae93e8ce01707b674198a268b29bf50c096 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Tue, 26 Nov 2024 16:16:15 +0800 Subject: [PATCH 140/187] =?UTF-8?q?=E2=9C=85=20test(functional):=20sync=20?= =?UTF-8?q?changes=20of=20semi-folded=20ops=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/components/test_functional.py | 102 ++++++++++++++-------------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/tests/components/test_functional.py b/tests/components/test_functional.py index 15df0663..c2c63a82 100644 --- a/tests/components/test_functional.py +++ b/tests/components/test_functional.py @@ -31,7 +31,7 @@ def _assert_build_fmodule( assert len(nodes) == n_node_bef_build # Construct the functional modules - DynSysGroup.build_fmodule(network) + network.build_modules() # Must exclude `NeuModule`, because it may be in the `__dict__` of probe nodes = network.nodes().subset(DynamicSys).exclude(NeuModule).unique() @@ -106,7 +106,7 @@ def test_BitwiseAND(self): net2 = FunctionalModule_2to1_Net("and") bitwise = net1.bitwise func = net2.func_node - generated = DynSysGroup.build_fmodule(net2) + generated = net2.build_modules() sim1 = pb.Simulator(net1, start_time_zero=False) sim2 = pb.Simulator(net2, start_time_zero=False) @@ -147,7 +147,7 @@ def test_BitwiseNOT(self): net2 = FunctionalModule_1to1_Net("not") bitwise = net1.bitwise func = net2.func_node - generated = DynSysGroup.build_fmodule(net2) + generated = net2.build_modules() sim1 = pb.Simulator(net1, start_time_zero=False) sim2 = pb.Simulator(net2, start_time_zero=False) @@ -187,7 +187,7 @@ def test_BitwiseOR(self): net2 = FunctionalModule_2to1_Net("or") bitwise = net1.bitwise func = net2.func_node - generated = DynSysGroup.build_fmodule(net2) + generated = net2.build_modules() sim1 = pb.Simulator(net1, start_time_zero=False) sim2 = pb.Simulator(net2, start_time_zero=False) @@ -228,7 +228,7 @@ def test_BitwiseXOR(self): net2 = FunctionalModule_2to1_Net("xor") bitwise = net1.bitwise func = net2.func_node - generated = DynSysGroup.build_fmodule(net2) + generated = net2.build_modules() sim1 = pb.Simulator(net1, start_time_zero=False) sim2 = pb.Simulator(net2, start_time_zero=False) @@ -269,7 +269,7 @@ def test_DelayChain(self): net2 = FunctionalModule_1to1_Net("delay") bitwise = net1.bitwise func = net2.func_node - generated = DynSysGroup.build_fmodule(net2) + generated = net2.build_modules() sim1 = pb.Simulator(net1, start_time_zero=False) sim2 = pb.Simulator(net2, start_time_zero=False) @@ -309,7 +309,7 @@ def test_SpikingAdd(self): net1 = FunctionalModule_2to1_Net("add") net2 = FunctionalModule_2to1_Net("add") func = net2.func_node - generated = DynSysGroup.build_fmodule(net2) + generated = net2.build_modules() sim1 = pb.Simulator(net1, start_time_zero=False) sim2 = pb.Simulator(net2, start_time_zero=False) @@ -360,7 +360,7 @@ def test_SpikingSub(self): net1 = FunctionalModule_2to1_Net("sub") net2 = FunctionalModule_2to1_Net("sub") func = net2.func_node - generated = DynSysGroup.build_fmodule(net2) + generated = net2.build_modules() sim1 = pb.Simulator(net1, start_time_zero=False) sim2 = pb.Simulator(net2, start_time_zero=False) @@ -448,7 +448,7 @@ def test_SpikingPool1d( net1 = SpikingPool1d_Net(fm_shape, ksize, stride, padding, threshold, pool_type) net2 = SpikingPool1d_Net(fm_shape, ksize, stride, padding, threshold, pool_type) p1d = net2.pool - generated = DynSysGroup.build_fmodule(net2) + generated = net2.build_modules() sim1 = pb.Simulator(net1, start_time_zero=False) sim2 = pb.Simulator(net2, start_time_zero=False) @@ -558,7 +558,7 @@ def test_SpikingPool2d( net1 = SpikingPool2d_Net(fm_shape, ksize, stride, padding, threshold, pool_type) net2 = SpikingPool2d_Net(fm_shape, ksize, stride, padding, threshold, pool_type) p2d = net2.pool - generated = DynSysGroup.build_fmodule(net2) + generated = net2.build_modules() sim1 = pb.Simulator(net1, start_time_zero=False) sim2 = pb.Simulator(net2, start_time_zero=False) @@ -642,7 +642,7 @@ def test_SpikingAvgPool1dWithV( net1 = SpikingPool1d_Net(fm_shape, ksize, stride, padding, threshold, "avgv") net2 = SpikingPool1d_Net(fm_shape, ksize, stride, padding, threshold, "avgv") p1d = net2.pool - generated = DynSysGroup.build_fmodule(net2) + generated = net2.build_modules() sim1 = pb.Simulator(net1, start_time_zero=False) sim2 = pb.Simulator(net2, start_time_zero=False) @@ -704,7 +704,7 @@ def test_SpikingAvgPool2dWithV( net1 = SpikingPool2d_Net(fm_shape, ksize, stride, padding, threshold, "avgv") net2 = SpikingPool2d_Net(fm_shape, ksize, stride, padding, threshold, "avgv") p2d = net2.pool - generated = DynSysGroup.build_fmodule(net2) + generated = net2.build_modules() sim1 = pb.Simulator(net1, start_time_zero=False) sim2 = pb.Simulator(net2, start_time_zero=False) @@ -742,7 +742,7 @@ def test_Transpose2d(self, shape): net1 = TransposeModule_T2d_Net(shape) net2 = TransposeModule_T2d_Net(shape) t2d = net2.t2d - generated = DynSysGroup.build_fmodule(net2) + generated = net2.build_modules() sim1 = pb.Simulator(net1, start_time_zero=False) sim2 = pb.Simulator(net2, start_time_zero=False) @@ -792,7 +792,7 @@ def test_Transpose3d(self, shape, axes): net1 = TransposeModule_T3d_Net(shape, axes) net2 = TransposeModule_T3d_Net(shape, axes) t3d = net2.t3d - generated = DynSysGroup.build_fmodule(net2) + generated = net2.build_modules() sim1 = pb.Simulator(net1, start_time_zero=False) sim2 = pb.Simulator(net2, start_time_zero=False) @@ -830,7 +830,6 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): mapper.export(fp=ensure_dump_dir) @pytest.mark.parametrize( - # NOTE: Only support padding in the first semi-folded conv2d for now. "ishape_chw, n_conv, kshape_oihw, stride, padding, out_features", [ # n_conv = 1 @@ -965,7 +964,7 @@ def test_Conv2dSemiFolded_FC_ChainNet( # `net1.conv_list` will be removed in `build_fmodule` conv2d_list = net1.conv_list.copy() linear = net1.linear1 - generated = DynSysGroup.build_fmodule(net1) + generated = net1.build_modules() sim1 = pb.Simulator(net1, start_time_zero=False) probe_conv_list = [] @@ -978,20 +977,23 @@ def test_Conv2dSemiFolded_FC_ChainNet( sim1.add_probe(probe_linear) semi_folded_modules = [*conv2d_list, linear] - semi_valid_interval = [] - for m in semi_folded_modules: - semi_valid_interval.append(m.valid_interval) - - ts_1st_valid = [0] * n_conv + # The interval & the time o the first valid data of the external input data stream + semi_vld_out_intv0 = 1 + t_1st_vld_data0 = 0 + # The interval & the time of the first valid data of the current layers + semi_vld_out_intv = [m.ostream_attr.interval for m in semi_folded_modules] + t_1st_vld_data = [0] * n_conv for i in range(n_conv): if i == 0: - ts_1st_valid[i] = ( - kshape_oihw[0][-1] - paddings[0][0] - ) * semi_valid_interval[0] + t_1st_vld_data[i] = ( + t_1st_vld_data0 + + (kshape_oihw[0][-1] - paddings[0][0]) * semi_vld_out_intv0 + ) else: - ts_1st_valid[i] = ( - ts_1st_valid[i - 1] - + (kshape_oihw[i][-1] - 1 - paddings[i][0]) * semi_valid_interval[i] + t_1st_vld_data[i] = ( + t_1st_vld_data[i - 1] + + (kshape_oihw[i][-1] - 1 - paddings[i][0]) + * semi_vld_out_intv[i - 1] ) n_test = 3 # can be more @@ -1034,8 +1036,8 @@ def test_Conv2dSemiFolded_FC_ChainNet( x[:, :, i].ravel(), sim1.data[probe_conv_list[i_conv]][ conv2d_list[i_conv].tick_wait_start - + ts_1st_valid[i_conv] - + i * semi_valid_interval[i_conv + 1] + + t_1st_vld_data[i_conv] + + i * semi_vld_out_intv[i_conv] - 1 ], ) @@ -1047,10 +1049,7 @@ def test_Conv2dSemiFolded_FC_ChainNet( assert np.array_equal( expected_fc_t, sim1.data[probe_linear][ - linear.tick_wait_start - + ts_1st_valid[-1] - + (ows[-1] - 1) * semi_valid_interval[-1] - - 1 + linear.tick_wait_start + linear.ostream_attr.t_last_vld ], ) @@ -1160,7 +1159,7 @@ def test_Pool2dSemiFolded_FC_ChainNet( # `net1.pool_list` will be removed in `build_fmodule` pool2d_list = net1.pool_list.copy() linear = net1.linear1 - generated = DynSysGroup.build_fmodule(net1) + generated = net1.build_modules() sim1 = pb.Simulator(net1, start_time_zero=False) probe_pool_list = [] @@ -1173,20 +1172,22 @@ def test_Pool2dSemiFolded_FC_ChainNet( sim1.add_probe(probe_linear) semi_folded_modules = [*pool2d_list, linear] - semi_valid_interval = [] - for m in semi_folded_modules: - semi_valid_interval.append(m.valid_interval) - - ts_1st_valid = [0] * n_pool + # The interval & the time o the first valid data of the external input data stream + semi_vld_out_intv0 = 1 + t_1st_vld_data0 = 0 + # The interval & the time of the first valid data of the current layers + semi_vld_out_intv = [m.ostream_attr.interval for m in semi_folded_modules] + t_1st_vld_data = [0] * n_pool for i in range(n_pool): if i == 0: - ts_1st_valid[i] = ( - ksizes[0][-1] - paddings[0][0] - ) * semi_valid_interval[0] + t_1st_vld_data[i] = ( + t_1st_vld_data0 + + (ksizes[i][-1] - paddings[i][0]) * semi_vld_out_intv0 + ) else: - ts_1st_valid[i] = ( - ts_1st_valid[i - 1] - + (ksizes[i][-1] - 1 - paddings[i][0]) * semi_valid_interval[i] + t_1st_vld_data[i] = ( + t_1st_vld_data[i - 1] + + (ksizes[i][-1] - 1 - paddings[i][0]) * semi_vld_out_intv[i - 1] ) n_test = 3 # can be more @@ -1217,8 +1218,8 @@ def test_Pool2dSemiFolded_FC_ChainNet( x[:, :, i].ravel(), sim1.data[probe_pool_list[i_pool]][ pool2d_list[i_pool].tick_wait_start - + ts_1st_valid[i_pool] - + i * semi_valid_interval[i_pool + 1] + + t_1st_vld_data[i_pool] + + i * semi_vld_out_intv[i_pool] - 1 ], ) @@ -1230,10 +1231,7 @@ def test_Pool2dSemiFolded_FC_ChainNet( assert np.array_equal( expected_fc_t, sim1.data[probe_linear][ - linear.tick_wait_start - + ts_1st_valid[-1] - + (ows[-1] - 1) * semi_valid_interval[-1] - - 1 + linear.tick_wait_start + linear.ostream_attr.t_last_vld ], ) @@ -1250,7 +1248,7 @@ def test_Linear(self, shape, weight): net1 = Linear_Net(shape, weight) net2 = Linear_Net(shape, weight) linear = net2.linear1 - generated = pb.DynSysGroup.build_fmodule(net2) + generated = net2.build_modules() sim1 = pb.Simulator(net1, start_time_zero=False) sim2 = pb.Simulator(net2, start_time_zero=False) From 3d5b37a4160fdda369d4b1de7e4c32a99250ee65 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Tue, 26 Nov 2024 16:16:47 +0800 Subject: [PATCH 141/187] =?UTF-8?q?=E2=9C=85=20test(onboard):=20sync=20cha?= =?UTF-8?q?nges=20for=20on-board=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/__init__.py | 4 -- tests/onboard/README.md | 40 ++++++++-------- tests/onboard/test_onboard.py | 88 +++++++++++++++++------------------ 3 files changed, 64 insertions(+), 68 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 9cbd67a8..e69de29b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +0,0 @@ -import os -import sys - -sys.path.append(os.getcwd()) diff --git a/tests/onboard/README.md b/tests/onboard/README.md index 65e86190..2d1f0104 100644 --- a/tests/onboard/README.md +++ b/tests/onboard/README.md @@ -2,8 +2,8 @@ ## ANN权重映射 -| 测试项目 | 结果 | 备注 | -| :-----------------------: | :--: | :--------: | +| 测试项目 | 结果 | 备注 | +| :--------------------: | :--: | :--------: | | [001 单层](#001-单层w8e1) | ✅ | | | [002 单层](#002-单层w8e4) | ✅ | | | [003 单层](#003-单层w2e2) | ✅ | | @@ -124,8 +124,8 @@ ## SNN算子 -| 测试项目 | 结果 | 备注 | -| :-----------------------: | :--: | :--: | +| 测试项目 | 结果 | 备注 | +| :--------------------: | :--: | :--: | | [001 Conv1d](#001-conv1d) | ✅ | | ### Conv1d @@ -145,22 +145,22 @@ ## 半折叠算子 -| 测试项目 | 结果 | 备注 | -| :-------------------------------------------------: | :--: | :--------: | -| [001 Conv2dSemiFolded](#001-conv2dsemifolded) | ✅ | | -| [002 Conv2dSemiFolded](#002-conv2dsemifolded) | ❌ | 不完全相等 | -| [003 Conv2dSemiFolded](#003-conv2dsemifolded) | | | -| [004 Conv2dSemiFolded](#004-conv2dsemifolded) | | | -| [005 Conv2dSemiFolded](#005-conv2dsemifolded) | | | -| [006 Conv2dSemiFolded](#006-conv2dsemifolded) | ❌ | 不完全相等 | -| [007 Conv2dSemiFolded](#007-conv2dsemifolded) | | | -| [008 Conv2dSemiFolded](#008-conv2dsemifolded) | | | -| [009 Conv2dSemiFolded](#009-conv2dsemifolded) | | | -| [010 MaxPool2dSemiFolded](#010-maxpool2dsemifolded) | | | -| [011 AvgPool2dSemiFolded](#011-avgpool2dsemifolded) | ✅ | | -| [012 Conv2dSemiFoldedNet](#012-conv2dsemifoldednet) | | | -| [013 Conv2dSemiFoldedNet](#013-conv2dsemifoldednet) | | | -| [014 CNNSemiFoldedNet](#014-cnnsemifoldednet) | | | +| 测试项目 | 结果 | 备注 | +| :-----------------------------------------------: | :--: | :----------------------------------: | +| [001 Conv2dSemiFolded](#001-conv2dsemifolded) | ✅ | | +| [002 Conv2dSemiFolded](#002-conv2dsemifolded) | ✅ | | +| [003 Conv2dSemiFolded](#003-conv2dsemifolded) | ✅ | | +| [004 Conv2dSemiFolded](#004-conv2dsemifolded) | ✅ | 仅错在前2、3时间步,本身就是无效数据 | +| [005 Conv2dSemiFolded](#005-conv2dsemifolded) | ✅ | | +| [006 Conv2dSemiFolded](#006-conv2dsemifolded) | ✅ | | +| [007 Conv2dSemiFolded](#007-conv2dsemifolded) | ✅ | | +| [008 Conv2dSemiFolded](#008-conv2dsemifolded) | ✅ | | +| [009 Conv2dSemiFolded](#009-conv2dsemifolded) | ✅ | | +| [010 MaxPool2dSemiFolded](#010-maxpool2dsemifolded) | ❌ | | +| [011 AvgPool2dSemiFolded](#011-avgpool2dsemifolded) | ✅ | | +| [012 Conv2dSemiFoldedNet](#012-conv2dsemifoldednet) | ❌ | | +| [013 Conv2dSemiFoldedNet](#013-conv2dsemifoldednet) | ✅ | | +| [014 CNNSemiFoldedNet](#014-cnnsemifoldednet) | ✅ | | ### 单层 diff --git a/tests/onboard/test_onboard.py b/tests/onboard/test_onboard.py index d18f094f..9e51b0f2 100644 --- a/tests/onboard/test_onboard.py +++ b/tests/onboard/test_onboard.py @@ -792,7 +792,7 @@ def __init__(self, w1, w2): class TestOnBoard_SpikingOp: - def test_Conv1d_001(self): + def test_001_Conv1d(self): class Net001(pb.Network): def __init__(self, w1): super().__init__() @@ -803,7 +803,7 @@ def __init__(self, w1): self.p1 = pb.Probe(self.n1, "feature_map") USE_EXISTING_DATA = False - TEST_NAME = self.test_Conv1d_001.__name__ + TEST_NAME = self.test_001_Conv1d.__name__ TEST_CASE_DIR = DATA_DIR / TEST_NAME CONFIG_CASE_DIR = CONFIG_DIR / TEST_NAME if not TEST_CASE_DIR.exists(): @@ -880,7 +880,7 @@ def __init__(self, w1): class TestOnBoard_SemiFoldedOp: - def test_Conv2dSemiFolded_001(self): + def test_001_Conv2dSemiFolded(self): class Net001(pb.DynSysGroup): def __init__(self, w1): super().__init__() @@ -888,7 +888,7 @@ def __init__(self, w1): self.conv1 = pb.Conv2dSemiFolded(self.i1, w1, 1, 0, tick_wait_start=1) USE_EXISTING_DATA = False - TEST_NAME = self.test_Conv2dSemiFolded_001.__name__ + TEST_NAME = self.test_001_Conv2dSemiFolded.__name__ TEST_CASE_DIR = DATA_DIR / TEST_NAME CONFIG_CASE_DIR = CONFIG_DIR / TEST_NAME if not TEST_CASE_DIR.exists(): @@ -929,7 +929,7 @@ def __init__(self, w1): network = Net001(weight1) conv2d = network.conv1 - generated = pb.DynSysGroup.build_fmodule(network) + generated = network.build_modules() sim = pb.Simulator(network, start_time_zero=False) probe = pb.Probe(generated[conv2d][0], "output") sim.add_probe(probe) @@ -959,7 +959,7 @@ def __init__(self, w1): # 对比test002-005系列 # weight正常 - def test_Conv2dSemiFolded_002(self): + def test_002_Conv2dSemiFolded(self): class Net002(pb.DynSysGroup): def __init__(self, w2): super().__init__() @@ -967,7 +967,7 @@ def __init__(self, w2): self.conv1 = pb.Conv2dSemiFolded(self.i1, w2, 2, 0, tick_wait_start=1) USE_EXISTING_DATA = False - TEST_NAME = self.test_Conv2dSemiFolded_002.__name__ + TEST_NAME = self.test_002_Conv2dSemiFolded.__name__ TEST_CASE_DIR = DATA_DIR / TEST_NAME CONFIG_CASE_DIR = CONFIG_DIR / TEST_NAME if not TEST_CASE_DIR.exists(): @@ -1009,7 +1009,7 @@ def __init__(self, w2): network = Net002(weight1) conv2d = network.conv1 - generated = pb.DynSysGroup.build_fmodule(network) + generated = network.build_modules() sim = pb.Simulator(network, start_time_zero=False) probe = pb.Probe(generated[conv2d][0], "output") sim.add_probe(probe) @@ -1038,7 +1038,7 @@ def __init__(self, w2): print(f"Test {TEST_NAME} end") # weight全为1 - def test_Conv2dSemiFolded_003(self): + def test_003_Conv2dSemiFolded(self): class Net003(pb.DynSysGroup): def __init__(self, w2): super().__init__() @@ -1046,7 +1046,7 @@ def __init__(self, w2): self.conv1 = pb.Conv2dSemiFolded(self.i1, w2, 2, 0, tick_wait_start=1) USE_EXISTING_DATA = False - TEST_NAME = self.test_Conv2dSemiFolded_003.__name__ + TEST_NAME = self.test_003_Conv2dSemiFolded.__name__ TEST_CASE_DIR = DATA_DIR / TEST_NAME CONFIG_CASE_DIR = CONFIG_DIR / TEST_NAME if not TEST_CASE_DIR.exists(): @@ -1088,7 +1088,7 @@ def __init__(self, w2): network = Net003(weight1) conv2d = network.conv1 - generated = pb.DynSysGroup.build_fmodule(network) + generated = network.build_modules() sim = pb.Simulator(network, start_time_zero=False) probe = pb.Probe(generated[conv2d][0], "output") sim.add_probe(probe) @@ -1117,7 +1117,7 @@ def __init__(self, w2): print(f"Test {TEST_NAME} end") # 扇入扩展, weight全正1 - def test_Conv2dSemiFolded_004(self): + def test_004_Conv2dSemiFolded(self): class Net004(pb.DynSysGroup): def __init__(self, w2): super().__init__() @@ -1125,7 +1125,7 @@ def __init__(self, w2): self.conv1 = pb.Conv2dSemiFolded(self.i1, w2, 2, 0, tick_wait_start=1) USE_EXISTING_DATA = False - TEST_NAME = self.test_Conv2dSemiFolded_004.__name__ + TEST_NAME = self.test_004_Conv2dSemiFolded.__name__ TEST_CASE_DIR = DATA_DIR / TEST_NAME CONFIG_CASE_DIR = CONFIG_DIR / TEST_NAME if not TEST_CASE_DIR.exists(): @@ -1168,7 +1168,7 @@ def __init__(self, w2): network = Net004(weight1) conv2d = network.conv1 - generated = pb.DynSysGroup.build_fmodule(network) + generated = network.build_modules() sim = pb.Simulator(network, start_time_zero=False) probe = pb.Probe(generated[conv2d][0], "output") sim.add_probe(probe) @@ -1197,7 +1197,7 @@ def __init__(self, w2): print(f"Test {TEST_NAME} end") # 扇入扩展 - def test_Conv2dSemiFolded_005(self): + def test_005_Conv2dSemiFolded(self): class Net005(pb.DynSysGroup): def __init__(self, w2): super().__init__() @@ -1205,7 +1205,7 @@ def __init__(self, w2): self.conv1 = pb.Conv2dSemiFolded(self.i1, w2, 2, 0, tick_wait_start=1) USE_EXISTING_DATA = False - TEST_NAME = self.test_Conv2dSemiFolded_005.__name__ + TEST_NAME = self.test_005_Conv2dSemiFolded.__name__ TEST_CASE_DIR = DATA_DIR / TEST_NAME CONFIG_CASE_DIR = CONFIG_DIR / TEST_NAME if not TEST_CASE_DIR.exists(): @@ -1248,7 +1248,7 @@ def __init__(self, w2): network = Net005(weight1) conv2d = network.conv1 - generated = pb.DynSysGroup.build_fmodule(network) + generated = network.build_modules() sim = pb.Simulator(network, start_time_zero=False) probe = pb.Probe(generated[conv2d][0], "output") sim.add_probe(probe) @@ -1277,7 +1277,7 @@ def __init__(self, w2): print(f"Test {TEST_NAME} end") # 对比006-009 - def test_Conv2dSemiFolded_006(self): + def test_006_Conv2dSemiFolded(self): class Net006(pb.DynSysGroup): def __init__(self, w2): super().__init__() @@ -1285,7 +1285,7 @@ def __init__(self, w2): self.conv1 = pb.Conv2dSemiFolded(self.i1, w2, 1, 1, tick_wait_start=1) USE_EXISTING_DATA = False - TEST_NAME = self.test_Conv2dSemiFolded_006.__name__ + TEST_NAME = self.test_006_Conv2dSemiFolded.__name__ TEST_CASE_DIR = DATA_DIR / TEST_NAME CONFIG_CASE_DIR = CONFIG_DIR / TEST_NAME if not TEST_CASE_DIR.exists(): @@ -1328,7 +1328,7 @@ def __init__(self, w2): network = Net006(weight1) conv2d = network.conv1 - generated = pb.DynSysGroup.build_fmodule(network) + generated = network.build_modules() sim = pb.Simulator(network, start_time_zero=False) probe = pb.Probe(generated[conv2d][0], "output") sim.add_probe(probe) @@ -1356,7 +1356,7 @@ def __init__(self, w2): print(f"Test {TEST_NAME} end") - def test_Conv2dSemiFolded_007(self): + def test_007_Conv2dSemiFolded(self): class Net007(pb.DynSysGroup): def __init__(self, w2): super().__init__() @@ -1364,7 +1364,7 @@ def __init__(self, w2): self.conv1 = pb.Conv2dSemiFolded(self.i1, w2, 1, 1, tick_wait_start=1) USE_EXISTING_DATA = False - TEST_NAME = self.test_Conv2dSemiFolded_007.__name__ + TEST_NAME = self.test_007_Conv2dSemiFolded.__name__ TEST_CASE_DIR = DATA_DIR / TEST_NAME CONFIG_CASE_DIR = CONFIG_DIR / TEST_NAME if not TEST_CASE_DIR.exists(): @@ -1408,7 +1408,7 @@ def __init__(self, w2): network = Net007(weight1) conv2d = network.conv1 - generated = pb.DynSysGroup.build_fmodule(network) + generated = network.build_modules() sim = pb.Simulator(network, start_time_zero=False) probe = pb.Probe(generated[conv2d][0], "output") sim.add_probe(probe) @@ -1436,7 +1436,7 @@ def __init__(self, w2): print(f"Test {TEST_NAME} end") - def test_Conv2dSemiFolded_008(self): + def test_008_Conv2dSemiFolded(self): class Net008(pb.DynSysGroup): def __init__(self, w2): super().__init__() @@ -1444,7 +1444,7 @@ def __init__(self, w2): self.conv1 = pb.Conv2dSemiFolded(self.i1, w2, 1, 1, tick_wait_start=1) USE_EXISTING_DATA = False - TEST_NAME = self.test_Conv2dSemiFolded_008.__name__ + TEST_NAME = self.test_008_Conv2dSemiFolded.__name__ TEST_CASE_DIR = DATA_DIR / TEST_NAME CONFIG_CASE_DIR = CONFIG_DIR / TEST_NAME if not TEST_CASE_DIR.exists(): @@ -1488,7 +1488,7 @@ def __init__(self, w2): network = Net008(weight1) conv2d = network.conv1 - generated = pb.DynSysGroup.build_fmodule(network) + generated = network.build_modules() sim = pb.Simulator(network, start_time_zero=False) probe = pb.Probe(generated[conv2d][0], "output") sim.add_probe(probe) @@ -1516,7 +1516,7 @@ def __init__(self, w2): print(f"Test {TEST_NAME} end") - def test_Conv2dSemiFolded_009(self): + def test_009_Conv2dSemiFolded(self): class Net009(pb.DynSysGroup): def __init__(self, w2): super().__init__() @@ -1524,7 +1524,7 @@ def __init__(self, w2): self.conv1 = pb.Conv2dSemiFolded(self.i1, w2, 1, 1, tick_wait_start=1) USE_EXISTING_DATA = False - TEST_NAME = self.test_Conv2dSemiFolded_009.__name__ + TEST_NAME = self.test_009_Conv2dSemiFolded.__name__ TEST_CASE_DIR = DATA_DIR / TEST_NAME CONFIG_CASE_DIR = CONFIG_DIR / TEST_NAME if not TEST_CASE_DIR.exists(): @@ -1565,7 +1565,7 @@ def __init__(self, w2): network = Net009(weight1) conv2d = network.conv1 - generated = pb.DynSysGroup.build_fmodule(network) + generated = network.build_modules() sim = pb.Simulator(network, start_time_zero=False) probe = pb.Probe(generated[conv2d][0], "output") sim.add_probe(probe) @@ -1593,7 +1593,7 @@ def __init__(self, w2): print(f"Test {TEST_NAME} end") - def test_MaxPool2dSemiFolded_010(self): + def test_010_MaxPool2dSemiFolded(self): class Net010(pb.DynSysGroup): def __init__(self, ksize): super().__init__() @@ -1603,7 +1603,7 @@ def __init__(self, ksize): ) USE_EXISTING_DATA = False - TEST_NAME = self.test_MaxPool2dSemiFolded_010.__name__ + TEST_NAME = self.test_010_MaxPool2dSemiFolded.__name__ TEST_CASE_DIR = DATA_DIR / TEST_NAME CONFIG_CASE_DIR = CONFIG_DIR / TEST_NAME if not TEST_CASE_DIR.exists(): @@ -1643,7 +1643,7 @@ def __init__(self, ksize): network = Net010(ksize) pool = network.pool1 - generated = pb.DynSysGroup.build_fmodule(network) + generated = network.build_modules() sim = pb.Simulator(network, start_time_zero=False) probe = pb.Probe(generated[pool][0], "output") sim.add_probe(probe) @@ -1669,7 +1669,7 @@ def __init__(self, ksize): print(f"Test {TEST_NAME} end") - def test_AvgPool2dSemiFolded_011(self): + def test_011_AvgPool2dSemiFolded(self): class Net011(pb.DynSysGroup): def __init__(self, ksize): super().__init__() @@ -1679,7 +1679,7 @@ def __init__(self, ksize): ) USE_EXISTING_DATA = False - TEST_NAME = self.test_AvgPool2dSemiFolded_011.__name__ + TEST_NAME = self.test_011_AvgPool2dSemiFolded.__name__ TEST_CASE_DIR = DATA_DIR / TEST_NAME CONFIG_CASE_DIR = CONFIG_DIR / TEST_NAME if not TEST_CASE_DIR.exists(): @@ -1719,7 +1719,7 @@ def __init__(self, ksize): network = Net011(ksize) pool = network.pool1 - generated = pb.DynSysGroup.build_fmodule(network) + generated = network.build_modules() sim = pb.Simulator(network, start_time_zero=False) probe = pb.Probe(generated[pool][0], "output") sim.add_probe(probe) @@ -1748,7 +1748,7 @@ def __init__(self, ksize): @pytest.mark.xfail( reason="A ValidationError will be raised due to the backend not support." ) - def test_Conv2dSemiFoldedNet_012(self): + def test_012_Conv2dSemiFoldedNet(self): class Net012(pb.DynSysGroup): def __init__(self, w1, w2, w3): super().__init__() @@ -1764,7 +1764,7 @@ def __init__(self, w1, w2, w3): ) USE_EXISTING_DATA = False - TEST_NAME = self.test_Conv2dSemiFoldedNet_012.__name__ + TEST_NAME = self.test_012_Conv2dSemiFoldedNet.__name__ TEST_CASE_DIR = DATA_DIR / TEST_NAME CONFIG_CASE_DIR = CONFIG_DIR / TEST_NAME if not TEST_CASE_DIR.exists(): @@ -1811,7 +1811,7 @@ def __init__(self, w1, w2, w3): conv2d1 = network.conv1 conv2d2 = network.conv2 linear = network.linear1 - generated = pb.DynSysGroup.build_fmodule(network) + generated = network.build_modules() sim = pb.Simulator(network, start_time_zero=False) probe1 = pb.Probe(generated[conv2d1][0], "output") probe2 = pb.Probe(generated[conv2d2][0], "output") @@ -1849,7 +1849,7 @@ def __init__(self, w1, w2, w3): print(f"Test {TEST_NAME} end") - def test_Conv2dSemiFoldedNet_013(self): + def test_013_Conv2dSemiFoldedNet(self): class Net013(pb.DynSysGroup): def __init__(self, w1, w2, w3): super().__init__() @@ -1863,7 +1863,7 @@ def __init__(self, w1, w2, w3): ) USE_EXISTING_DATA = False - TEST_NAME = self.test_Conv2dSemiFoldedNet_013.__name__ + TEST_NAME = self.test_013_Conv2dSemiFoldedNet.__name__ TEST_CASE_DIR = DATA_DIR / TEST_NAME CONFIG_CASE_DIR = CONFIG_DIR / TEST_NAME if not TEST_CASE_DIR.exists(): @@ -1909,7 +1909,7 @@ def __init__(self, w1, w2, w3): conv2d1 = network.conv1 conv2d2 = network.conv2 linear = network.linear1 - generated = pb.DynSysGroup.build_fmodule(network) + generated = network.build_modules() sim = pb.Simulator(network, start_time_zero=False) probe1 = pb.Probe(generated[conv2d1][0], "output") probe2 = pb.Probe(generated[conv2d2][0], "output") @@ -1947,7 +1947,7 @@ def __init__(self, w1, w2, w3): print(f"Test {TEST_NAME} end") - def test_CNNSemiFoldedNet_014(self): + def test_014_CNNSemiFoldedNet(self): class Net014(pb.DynSysGroup): def __init__(self, w1, w2, w3): super().__init__() @@ -1968,7 +1968,7 @@ def __init__(self, w1, w2, w3): ) USE_EXISTING_DATA = False - TEST_NAME = self.test_CNNSemiFoldedNet_014.__name__ + TEST_NAME = self.test_014_CNNSemiFoldedNet.__name__ TEST_CASE_DIR = DATA_DIR / TEST_NAME CONFIG_CASE_DIR = CONFIG_DIR / TEST_NAME if not TEST_CASE_DIR.exists(): @@ -2015,7 +2015,7 @@ def __init__(self, w1, w2, w3): conv2d1 = network.conv1 conv2d2 = network.conv2 linear = network.linear1 - generated = pb.DynSysGroup.build_fmodule(network) + generated = network.build_modules() sim = pb.Simulator(network, start_time_zero=False) probe1 = pb.Probe(generated[conv2d1][0], "output") probe2 = pb.Probe(generated[conv2d2][0], "output") From b6a7ac63ec66fa268913b561d1f41409d6ad3e91 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 08:57:56 +0000 Subject: [PATCH 142/187] :rotating_light: auto fix by pre-commit hooks --- paibox/components/_modules.py | 2 +- paibox/network.py | 7 +++---- tests/onboard/README.md | 12 ++++++------ 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/paibox/components/_modules.py b/paibox/components/_modules.py index e20b6aa6..ea22fbe3 100644 --- a/paibox/components/_modules.py +++ b/paibox/components/_modules.py @@ -1,6 +1,6 @@ -from dataclasses import dataclass import math import typing +from dataclasses import dataclass from typing import Literal, Optional, Union import numpy as np diff --git a/paibox/network.py b/paibox/network.py index 4f25a48c..4f0a4c12 100644 --- a/paibox/network.py +++ b/paibox/network.py @@ -1,16 +1,15 @@ -from collections.abc import Sequence import sys +from collections.abc import Sequence from typing import Optional, Union import numpy as np -from .exceptions import NotSupportedError - from .base import DynamicSys, SynSys from .collector import Collector -from .components._modules import _SemiFoldedModule, SemiFoldedStreamAttr from .components import NeuModule, Neuron, Projection +from .components._modules import SemiFoldedStreamAttr, _SemiFoldedModule from .components.modules import BuiltComponentType +from .exceptions import NotSupportedError from .mixin import Container from .node import NodeDict, NodeList diff --git a/tests/onboard/README.md b/tests/onboard/README.md index 2d1f0104..49ccf3e5 100644 --- a/tests/onboard/README.md +++ b/tests/onboard/README.md @@ -2,8 +2,8 @@ ## ANN权重映射 -| 测试项目 | 结果 | 备注 | -| :--------------------: | :--: | :--------: | +| 测试项目 | 结果 | 备注 | +| :-----------------------: | :--: | :--------: | | [001 单层](#001-单层w8e1) | ✅ | | | [002 单层](#002-单层w8e4) | ✅ | | | [003 单层](#003-单层w2e2) | ✅ | | @@ -124,8 +124,8 @@ ## SNN算子 -| 测试项目 | 结果 | 备注 | -| :--------------------: | :--: | :--: | +| 测试项目 | 结果 | 备注 | +| :-----------------------: | :--: | :--: | | [001 Conv1d](#001-conv1d) | ✅ | | ### Conv1d @@ -145,8 +145,8 @@ ## 半折叠算子 -| 测试项目 | 结果 | 备注 | -| :-----------------------------------------------: | :--: | :----------------------------------: | +| 测试项目 | 结果 | 备注 | +| :-------------------------------------------------: | :--: | :----------------------------------: | | [001 Conv2dSemiFolded](#001-conv2dsemifolded) | ✅ | | | [002 Conv2dSemiFolded](#002-conv2dsemifolded) | ✅ | | | [003 Conv2dSemiFolded](#003-conv2dsemifolded) | ✅ | | From 4cdb394e89c503fac88b6451fdc8741aac2d3eec Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Tue, 26 Nov 2024 21:30:35 +0800 Subject: [PATCH 143/187] =?UTF-8?q?=F0=9F=A4=96=20ci:=20rename=20workflow?= =?UTF-8?q?=20&=20add=20code=20coverage=20report=20for=20dev=20&=20master?= =?UTF-8?q?=20branches?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/codecov.yml | 61 ++++++++++++++++++ .github/workflows/pytest-ci.yml | 45 -------------- README.md | 3 + poetry.lock | 107 +++++++++++++++++++++++++++++++- pyproject.toml | 2 + 5 files changed, 172 insertions(+), 46 deletions(-) create mode 100644 .github/workflows/codecov.yml delete mode 100644 .github/workflows/pytest-ci.yml diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml new file mode 100644 index 00000000..05f9ce7b --- /dev/null +++ b/.github/workflows/codecov.yml @@ -0,0 +1,61 @@ +name: Pytest & code coverage + +on: + push: + branches: + - master + pull_request: + branches: + - master + - dev + paths: + - "paibox/**" + - "tests/**" + - ".github/workflows/codecov.yml" + - "pyproject.toml" + - "poetry.lock" + +permissions: + contents: read + +jobs: + pytest: + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install poetry + uses: abatilo/actions-poetry@v3 + + - name: Install test dependencies + run: | + poetry install --with test --sync + + - name: Run pytest + run: | + poetry run pytest --cov-append --cov-report=xml --junitxml=junit.xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + fail_ci_if_error: true + flags: unittests + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/test-results-action@v1 + with: + fail_ci_if_error: true + flags: unittests + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/pytest-ci.yml b/.github/workflows/pytest-ci.yml deleted file mode 100644 index 533bcf76..00000000 --- a/.github/workflows/pytest-ci.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Python CI with pytest - -on: - pull_request: - branches: - - master - - dev - types: [opened, synchronize, reopened] - -permissions: - contents: read - -jobs: - pytest-ci: - strategy: - matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] - os: [ubuntu-latest, windows-latest] - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Install poetry - uses: abatilo/actions-poetry@v2 - - - name: Install test dependencies - run: | - poetry install --with test --sync - - - name: Run pytest - uses: pavelzw/pytest-action@v2 - with: - verbose: false - emoji: false - job-summary: true - custom-arguments: "-q" - custom-pytest: "poetry run pytest" - click-to-expand: true - report-title: "Test Report" diff --git a/README.md b/README.md index d4b2dbfa..10b29e47 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ pre-commit.ci status + + +

👉 [用户使用指南](docs/Guide-of-PAIBox.md) diff --git a/poetry.lock b/poetry.lock index f87c950d..a0edf08e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -32,6 +32,88 @@ type = "legacy" url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "tsinghua" +[[package]] +name = "coverage" +version = "7.6.8" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50"}, + {file = "coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed"}, + {file = "coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e"}, + {file = "coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a"}, + {file = "coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4"}, + {file = "coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0"}, + {file = "coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801"}, + {file = "coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9"}, + {file = "coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee"}, + {file = "coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443"}, + {file = "coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad"}, + {file = "coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4"}, + {file = "coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb"}, + {file = "coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b"}, + {file = "coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146"}, + {file = "coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28"}, + {file = "coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d"}, + {file = "coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b"}, + {file = "coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71"}, + {file = "coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc"}, + {file = "coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e"}, + {file = "coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea"}, + {file = "coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e"}, + {file = "coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076"}, + {file = "coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce"}, + {file = "coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + [[package]] name = "exceptiongroup" version = "1.2.2" @@ -405,6 +487,29 @@ type = "legacy" url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "tsinghua" +[[package]] +name = "pytest-cov" +version = "6.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, +] + +[package.dependencies] +coverage = {version = ">=7.5", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + [[package]] name = "pytest-md" version = "0.2.0" @@ -459,4 +564,4 @@ reference = "tsinghua" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "25cfd043004050d36aa3b7c7bede80ae551fda620af4c9d8600ffde29b8f8c61" +content-hash = "b5d36748eeaeb04ded544fd4992e7a16a4b33efbd4099390b93965543e502631" diff --git a/pyproject.toml b/pyproject.toml index 2a0bd1bf..71124dab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ optional = true [tool.poetry.group.test.dependencies] pytest = "^8.0.0" pytest-md = "^0.2.0" +pytest-cov = "^6.0.0" paicorelib = {git = "https://github.com/PAICookers/PAIlib.git", rev = "dev"} orjson = "^3.10.0" @@ -59,6 +60,7 @@ orjson = "^3.10.0" [tool.pytest.ini_options] minversion = "8.0.0" testpaths = ["tests"] +addopts = "--cov=paibox --cov-report=term" [[tool.poetry.source]] From e0fbae794380a3b7a8766c490766e8862a7f29fa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:31:03 +0000 Subject: [PATCH 144/187] :rotating_light: auto fix by pre-commit hooks --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 10b29e47..cd6e6ae5 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ pre-commit.ci status - - + +

From 52c523119b94c89dd5942d11ec65640ab40331f4 Mon Sep 17 00:00:00 2001 From: birdswimming <72957950+birdswimming@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:39:06 +0800 Subject: [PATCH 145/187] bugfix(backend): fix some bugs in axons set (#136) * fix bug in coreblock axons * add test for ordered axons * :rotating_light: auto fix by pre-commit hooks --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- paibox/backend/graphs.py | 74 +++++++++++++++++ paibox/backend/mapper.py | 67 ++++++++++++--- paibox/backend/placement.py | 33 +++++--- paibox/backend/routing.py | 157 +++++++++++++++++++++++++++-------- paibox/backend/types.py | 18 ++++ tests/backend/conftest.py | 66 +++++++++++++++ tests/backend/test_mapper.py | 42 ++++++++-- 7 files changed, 392 insertions(+), 65 deletions(-) diff --git a/paibox/backend/graphs.py b/paibox/backend/graphs.py index 335b95cf..c3e2751d 100644 --- a/paibox/backend/graphs.py +++ b/paibox/backend/graphs.py @@ -557,6 +557,80 @@ def _degree_check( ) +def find_cycles(directed_edges: Mapping[_NT, Iterable[_NT]]) -> list[list[_NT]]: + cycles: list[list[_NT]] = [] + visited: set[_NT] = set() + stack: list[_NT] = [] + stack_set: set[_NT] = set() # 方便快速检查路径中的节点 + + # 深度优先搜索的辅助函数 + def dfs(node: _NT): + if node in stack_set: # 检测到环 + cycle_start_index = stack.index(node) + cycles.append(stack[cycle_start_index:]) + return + if node in visited: + return + + visited.add(node) + stack.append(node) + stack_set.add(node) + + for neighbor in directed_edges.get(node, []): + dfs(neighbor) + + stack.pop() + stack_set.remove(node) + + # 遍历每个节点,查找所有可能的环 + for node in directed_edges: + if node not in visited: + dfs(node) + + return cycles + + +def merge_overlap(groups: Iterable[Iterable[_NT]]) -> list[list[_NT]]: + # 并查集数据结构 + parent: dict[_NT, _NT] = dict() + + # 查找集合的根节点 + def find(x): + if parent[x] != x: + parent[x] = find(parent[x]) + return parent[x] + + # 合并两个集合 + def union(x, y): + rootX = find(x) + rootY = find(y) + if rootX != rootY: + parent[rootY] = rootX + + # 初始化并查集 + for group in groups: + for element in group: + if element not in parent: + parent[element] = element + + # 合并所有相互重叠的环 + for group in groups: + first_element = group[0] + for element in group[1:]: + union(first_element, element) + + # 根据并查集结果,将所有节点归类到同一个集合中 + merged_groups: dict[_NT, list[_NT]] = dict() + for element in parent: + root = find(element) + if root not in merged_groups: + merged_groups[root] = [] + merged_groups[root].append(element) + + # 将结果转换为列表列表形式 + return list(merged_groups.values()) + + def toposort(directed_edges: Mapping[_NT, Iterable[_NT]]) -> list[_NT]: """ Topological sort algorithm by Kahn [1]_. diff --git a/paibox/backend/mapper.py b/paibox/backend/mapper.py index 045e33d5..6012d7fc 100644 --- a/paibox/backend/mapper.py +++ b/paibox/backend/mapper.py @@ -22,10 +22,24 @@ OutputDestConf, ) from .context import _BACKEND_CONTEXT, set_cflag -from .graphs import PAIGraph, get_node_degrees, get_succ_cb_by_node, toposort +from .graphs import ( + PAIGraph, + find_cycles, + get_node_degrees, + get_succ_cb_by_node, + merge_overlap, + toposort, +) from .placement import CoreBlock, aligned_coords, max_lcn_of_cb from .routing import RoutingGroup, RoutingManager -from .types import NeuSegment, NodeDegree, NodeType, SourceNodeType, is_iw8 +from .types import ( + MergedSuccGroup, + NeuSegment, + NodeDegree, + NodeType, + SourceNodeType, + is_iw8, +) __all__ = ["Mapper"] @@ -202,10 +216,19 @@ def untwist_branch_nodes(self) -> None: def build_core_blocks(self) -> None: """Build core blocks based on partitioned edges.""" - merged_sgrps = self.graph.graph_partition() + merged_sgrps: list[MergedSuccGroup] = self.graph.graph_partition() + merged_sgrps: list[MergedSuccGroup] = cycle_merge(merged_sgrps) for msgrp in merged_sgrps: - self.routing_groups.append(RoutingGroup.build(msgrp)) + self.routing_groups.append(RoutingGroup.build(msgrp, True)) + + routing_groups: list[RoutingGroup] = list() + for rg in self.routing_groups: + routing_groups.extend(rg.optimize_group()) + self.routing_groups = routing_groups + + for rg in self.routing_groups: + rg.dump() for rg in self.routing_groups: self.core_blocks += rg.core_blocks @@ -214,7 +237,7 @@ def build_core_blocks(self) -> None: succ_cbs: list[CoreBlock] = [] # cur_cb == cb is possible for cb in self.core_blocks: - if any(d for d in cur_cb.dest if d in cb.source): + if any(d for d in cur_cb.dest if d in cb.ordered_axons): succ_cbs.append(cb) self.succ_core_blocks[cur_cb] = succ_cbs @@ -274,8 +297,8 @@ def lcn_ex_adjustment(self) -> None: def cb_axon_grouping(self) -> None: """The axons are grouped after the LCN has been modified & locked.""" - for rg in self.routing_groups: - rg.group_axons() + for core_block in self.core_blocks: + core_block.group_axons() def graph_optimization(self) -> None: optimized = self.graph.graph_optimization(self.core_blocks, self.routing_groups) @@ -416,7 +439,7 @@ def _inpproj_config_export(self) -> InputNodeConf: # LCN of `input_cbs` are the same. input_cb = input_cbs[0] axon_coords = aligned_coords( - slice(0, input_cb.n_axon_of(input_cb.source.index(inode)), 1), + slice(0, input_cb.n_axon_of(input_cb.ordered_axons.index(inode)), 1), input_cb.axon_segments[inode], 1, input_cb.n_timeslot, @@ -646,7 +669,7 @@ def find_axon(self, neuron: Neuron, *, verbose: int = 0) -> None: for cb in self.core_blocks: # Find neuron in one or more core blocks. - if neuron in cb.source: + if neuron in cb.ordered_axons: print(f"axons {neuron.name} placed in {cb.name}, LCN_{1 << cb.lcn_ex}X") axon_segment = cb.axon_segments[neuron] print( @@ -663,11 +686,35 @@ def _find_dest_cb_by_nseg( self, neu_seg: NeuSegment, cb: CoreBlock ) -> list[CoreBlock]: succ_cbs = self.succ_core_blocks[cb] - dest_cb_of_nseg = [cb for cb in succ_cbs if neu_seg.target in cb.source] + dest_cb_of_nseg = [cb for cb in succ_cbs if neu_seg.target in cb.ordered_axons] return dest_cb_of_nseg +def cycle_merge(merged_sgrps: list[MergedSuccGroup]): + succ_merged_sgrps: dict[MergedSuccGroup, list[MergedSuccGroup]] = dict() + for msgrp in merged_sgrps: + succ_merged_sgrps[msgrp] = [] + nodes = set(msgrp.nodes) + for _msgrp in merged_sgrps: + if msgrp == _msgrp: + continue + if not nodes.isdisjoint(_msgrp.input_nodes): + succ_merged_sgrps[msgrp].append(_msgrp) + + cycles: list[list[MergedSuccGroup]] = find_cycles(succ_merged_sgrps) + merged_cycles: list[list[MergedSuccGroup]] = merge_overlap(cycles) + + processed_merged_cycles: list[MergedSuccGroup] = list() + remaining_merged_sgrps: set[MergedSuccGroup] = set(merged_sgrps) + for merged_cycle in merged_cycles: + processed_merged_cycles.append(MergedSuccGroup.merge(merged_cycle)) + for msgrp in merged_cycle: + remaining_merged_sgrps.remove(msgrp) + processed_merged_cycles.extend(remaining_merged_sgrps) + return processed_merged_cycles + + def group_by(dict_: dict, keyfunc=lambda item: item): """Groups the given list or dictionary by the value returned by ``keyfunc``.""" d = defaultdict(list) diff --git a/paibox/backend/placement.py b/paibox/backend/placement.py index 364a397d..e77e0035 100644 --- a/paibox/backend/placement.py +++ b/paibox/backend/placement.py @@ -93,7 +93,7 @@ def __init__( self._parents = parents self.rt_mode = mode self.seed = seed - self._lcn_ex = self._n_axon2lcn_ex() + self._lcn_ex = LCN_EX.LCN_1X self.target_lcn = LCN_EX.LCN_1X self._lcn_locked = False @@ -102,7 +102,7 @@ def __init__( self.core_placements = dict() self.axon_segments = dict() self.neuron_segs_of_cb = [] - self.ordered_axons: list[SourceNodeType] = [] + self._ordered_axons: list[SourceNodeType] = [] """Axons in private + multicast order.""" def group_neurons( @@ -172,7 +172,7 @@ def obj(self) -> tuple[FullConnectedSyn, ...]: @property def shape(self) -> tuple[int, int]: - return (len(self.source), len(self.dest)) + return (len(self.ordered_axons), len(self.dest)) @property def source(self) -> list[SourceNodeType]: @@ -190,7 +190,7 @@ def dest(self) -> list[DestNodeType]: def n_axon_of(self, index: int) -> int: """Get the #N of axons of `index`-th source neuron.""" - return self.axons[index].num_out + return self.ordered_axons[index].num_out """Boundary limitations""" @@ -275,7 +275,7 @@ def pool_max(self) -> MaxPoolingEnable: @property def n_axon(self) -> int: - return sum(s.num_out for s in self.axons) + return sum(s.num_out for s in self.ordered_axons) @property def n_fanout(self) -> int: @@ -307,17 +307,21 @@ def n_neuron_of_plm(self) -> list[int]: for neuron_segs in self.neuron_segs_of_cb ] - def group_axons(self, multicast_axons: list[SourceNodeType] = []) -> None: + @property + def ordered_axons(self) -> list[SourceNodeType]: + return self._ordered_axons + + @ordered_axons.setter + def ordered_axons(self, axons: list[SourceNodeType]): + self._ordered_axons = axons + self._lcn_ex = self._n_axon2lcn_ex() + + def group_axons(self) -> None: """Group the axons, including the private & the multicast parts. NOTE: Take the union of the private axons & the multicast axons, but sort the multicast axons first, then the \ axons that are in the private part and not in the multicast part. """ - if not self._lcn_locked: - raise GraphBuildError("group axons after 'lcn_ex' is locked.") - - axons = multicast_axons + [ax for ax in self.axons if ax not in multicast_axons] - self.ordered_axons = axons self.axon_segments = get_axon_segments( self.ordered_axons, self.n_timeslot, self.n_fanin_base ) @@ -435,6 +439,13 @@ def export_core_plm_config(cls, cb: "CoreBlock") -> CoreConfInChip: return cb_config + def dump(self, i: int = 0) -> None: + tabs = "\t" * i + print(f"{tabs}{self.name} with {self.n_core_required} cores:") + print(f"{tabs}\tLCN: {self.lcn_ex}") + for edge in self._parents: + print(f"{tabs}\t{edge.name}: {edge.source.name} -> {edge.target.name}") + class CorePlacement(CoreAbstract): parent: CoreBlock diff --git a/paibox/backend/routing.py b/paibox/backend/routing.py index 4863b112..9154d185 100644 --- a/paibox/backend/routing.py +++ b/paibox/backend/routing.py @@ -51,12 +51,15 @@ class RoutingGroup: """Class counter for debugging.""" def __init__( - self, unordered_cb: list[CoreBlock], ordered_rgrp: list["RoutingGroup"] + self, + unordered_elems: list[Union[CoreBlock, "RoutingGroup"]], + ordered_elems: list["RoutingGroup"], + is_root: bool = False, ) -> None: - self.unordered_cb: list[CoreBlock] = unordered_cb - self.ordered_rgrp: list["RoutingGroup"] = ordered_rgrp + self.unordered_elems: list[Union[CoreBlock, "RoutingGroup"]] = unordered_elems + self.ordered_elems: list["RoutingGroup"] = ordered_elems self.routing_elems: list[Union[CoreBlock, "RoutingGroup"]] = ( - unordered_cb + ordered_rgrp + unordered_elems + ordered_elems ) self.offset: list[int] = [] # TODO Change a name self.n_core_required: int = 0 @@ -70,6 +73,11 @@ def __init__( self.axons: list[SourceNodeType] = list(axons) # unordered + dest: set[DestNodeType] = set() + for elem in self.routing_elems: + dest.update(elem.dest) + self.dest: list[DestNodeType] = list(dest) + self.assigned_coords: list[Coord] = [] """Assigned core coordinates in the routing group""" self.wasted_coords: list[Coord] = [] @@ -77,17 +85,56 @@ def __init__( self.wasted_core_plm: dict[Coord, EmptyCorePlacement] = {} """Wasted core placements""" + # can not use set here, order matters + self.global_axons: list[SourceNodeType] = [] + """multicast axons inheritted from the parent routing group""" + self.private_axons: list[SourceNodeType] = [] + """multicast axons only effective in the current routing group""" + """Status options""" self.is_assigned = False """Whether the coordinates of chip & cores are assigned.""" + self.is_root = is_root # For debugging self._id = RoutingGroup._debug_id RoutingGroup._debug_id += 1 + if is_root: + self.set_axons() + + def set_axons(self, multicast_axons: list[SourceNodeType] = []) -> None: + """Set the multicast axons for the routing group.""" + self.global_axons = multicast_axons + ax_shared_times: list[int] = [0] * len(self.axons) + + used_axons: set[SourceNodeType] = set() + for elem in self.routing_elems: + # all axon of coreblocks should be multicast to the whole routing group + # because this routing group is the only coord that can access the coreblocks + if isinstance(elem, CoreBlock): + for axon in elem.axons: + if axon not in self.global_axons and axon not in self.private_axons: + self.private_axons.append(axon) + else: + for axon in elem.axons: + if axon not in self.global_axons and axon not in self.private_axons: + if axon in used_axons: + self.private_axons.append(axon) + else: + used_axons.add(axon) + + for elem in self.routing_elems: + if isinstance(elem, RoutingGroup): + elem.set_axons(self.global_axons + self.private_axons) + else: + # coreblocks in the routing group shuold reserve space for + # all axons that multicast to the routing group + elem.ordered_axons = self.global_axons + self.private_axons + def set_core_required(self) -> None: """Calculate the number of cores required for the routing group iteratively.""" - for rgrp in self.ordered_rgrp: + for rgrp in self.ordered_elems: rgrp.set_core_required() # Record the used cores of the members, but not the actual amount. @@ -95,14 +142,14 @@ def set_core_required(self) -> None: # Unordered core blocks sorted in descending order, avoiding assigning waste. unordered_cb = sorted( - self.unordered_cb, key=lambda x: x.n_core_required, reverse=True + self.unordered_elems, key=lambda x: x.n_core_required, reverse=True ) for cb in unordered_cb: self.offset.append(self.n_core_required) n_core_used += cb.n_core_required # Ordered routing groups should be assgined first. - ordered_rgrp = self.ordered_rgrp + ordered_rgrp = self.ordered_elems for rgrp in ordered_rgrp: n_core_assigned = _nearest_multiple_above(n_core_used, rgrp.n_core_required) self.offset.append(n_core_assigned) @@ -154,28 +201,57 @@ def assign_coord( return self.assigned_coords, self.wasted_coords - def group_axons(self, multicast_axons: list[SourceNodeType] = []) -> None: - """Group the axons, using list to keep the order of axons.""" - if not all(cb._lcn_locked for cb in self.core_blocks): - raise GraphBuildError( - "get axon segments of core block after 'lcn_ex' is locked." + def optimize_group(self) -> list["RoutingGroup"]: + optimized_unordered: list[Union[CoreBlock, "RoutingGroup"]] = list() + optimized_ordered: list["RoutingGroup"] = list() + for elem in self.unordered_elems: + if isinstance(elem, RoutingGroup): + optimized_unordered += elem.optimize_group() + else: + optimized_unordered.append(elem) + for elem in self.ordered_elems: + optimized_ordered += elem.optimize_group() + + # If one sub routing group in elems does not use + # the private multicast axons, then make it independent. + + # coreblocks in the routing group always use the private multicast axons + # otherwise, this coreblock should not in the routing group + unordered_groups: list["RoutingGroup"] = list() + remaining_unordered: list[Union[CoreBlock, "RoutingGroup"]] = list() + for elem in optimized_unordered: + if isinstance(elem, CoreBlock): + remaining_unordered.append(elem) + elif not set(self.private_axons).isdisjoint(elem.axons): + remaining_unordered.append(elem) + else: + unordered_groups.append(elem) + + ordered_groups: list["RoutingGroup"] = list() + remaining_ordered: list["RoutingGroup"] = list() + inputs: set[DestNodeType] = set() + for elem in reversed(optimized_ordered): + if not set(self.private_axons).isdisjoint(elem.axons): + inputs.update(elem.axons) + remaining_ordered.insert(0, elem) + elif not inputs.isdisjoint(elem.dest): + inputs.update(elem.dest) + remaining_ordered.insert(0, elem) + else: + elem.global_axons = self.global_axons + elem.is_root = self.is_root + ordered_groups.insert(0, elem) + + optimized_groups: list["RoutingGroup"] = list() + if len(remaining_unordered) > 0: + optimized_groups.append( + RoutingGroup(remaining_unordered, remaining_ordered, self.is_root) ) - private_multicast_axons = multicast_axons.copy() - ax_shared_times: list[int] = [0] * len(self.axons) - - # Axons shared within a routing group also need to be multicast. - for elem in self.routing_elems: - for ax in elem.axons: - idx = self.axons.index(ax) - ax_shared_times[idx] += 1 + # can not change the order here + optimized_groups = unordered_groups + optimized_groups + ordered_groups - for ax, times in zip(self.axons, ax_shared_times): - if times > 1 and ax not in private_multicast_axons: - private_multicast_axons.append(ax) - - for elem in self.routing_elems: - elem.group_axons(private_multicast_axons) + return optimized_groups @property def core_blocks(self) -> list[CoreBlock]: @@ -191,17 +267,26 @@ def core_blocks(self) -> list[CoreBlock]: return cbs @classmethod - def build(cls, merged_sgrp: MergedSuccGroup) -> "RoutingGroup": + def build( + cls, merged_sgrp: MergedSuccGroup, is_root: bool = False + ) -> "RoutingGroup": msgrp = MergedSuccGroup() remaining = MergedSuccGroup() - + sub_nodes = set() + remaining_nodes = set() for group in merged_sgrp.groups: if group.input in merged_sgrp.nodes: + sub_nodes.update(group.nodes) + remaining_nodes = merged_sgrp.nodes - sub_nodes + + for group in merged_sgrp.groups: + if not sub_nodes.isdisjoint(group.nodes): msgrp.add_group(group) - else: + if not remaining_nodes.isdisjoint(group.nodes): remaining.add_group(group) - remaining.nodes -= msgrp.nodes + remaining.nodes &= remaining_nodes + msgrp.nodes &= sub_nodes unordered_cb = CoreBlock.build_core_blocks(remaining) if len(msgrp.nodes) > 0: @@ -210,7 +295,7 @@ def build(cls, merged_sgrp: MergedSuccGroup) -> "RoutingGroup": else: ordered_rgrp = [] - return cls(unordered_cb, ordered_rgrp) + return cls(unordered_cb, ordered_rgrp, is_root) def core_block_alloc(self) -> None: assert self.is_assigned, "coordinates are not assigned." @@ -245,15 +330,15 @@ def chip_coord(self) -> ChipCoord: def dump(self, i: int = 0) -> None: tabs = "\t" * i print(f"{tabs}RoutingGroup: {self} with {self.n_core_required} cores:") + print( + f"{tabs}multicast axons: {[axon.name for axon in self.global_axons + self.private_axons]}" + ) for elem in self.routing_elems: if isinstance(elem, RoutingGroup): elem.dump(i + 1) else: - print(f"{tabs}\t{elem.name} with {elem.n_core_required} cores:") - for edge in elem._parents: - print( - f"{tabs}\t\t{edge.name}: {edge.source.name} -> {edge.target.name}" - ) + elem.dump(i + 1) + print() def __contains__(self, cb: CoreBlock) -> bool: return cb in self.core_blocks diff --git a/paibox/backend/types.py b/paibox/backend/types.py index dacf9dcd..778e3853 100644 --- a/paibox/backend/types.py +++ b/paibox/backend/types.py @@ -129,6 +129,7 @@ class MergedSuccGroup: def __init__(self, *init_sgrp: SuccGroup) -> None: self.nodes: set[NodeType] = set() self.groups: list[SuccGroup] = list() + self.input_nodes: list[NodeType] = list() if init_sgrp: for sgrp in init_sgrp: @@ -137,6 +138,7 @@ def __init__(self, *init_sgrp: SuccGroup) -> None: def add_group(self, group: SuccGroup) -> None: self.groups.append(group) self.nodes.update(group.nodes) + self.input_nodes.append(group.input) @property def outputs(self) -> dict[NodeType, list[EdgeType]]: @@ -148,6 +150,22 @@ def outputs(self) -> dict[NodeType, list[EdgeType]]: return onodes + @property + def num_in(self) -> int: + return sum(input_node.num_out for input_node in self.input_nodes) + + @classmethod + def merge(cls, merged_sgrps: list["MergedSuccGroup"]) -> "MergedSuccGroup": + merged = cls() + for merged_sgrp in merged_sgrps: + merged.nodes.update(merged_sgrp.nodes) + merged.groups.extend(merged_sgrp.groups) + merged.input_nodes.extend(merged_sgrp.input_nodes) + return merged + + def __hash__(self) -> int: + return hash(tuple(self.nodes)) + def dump(self) -> None: print("MergedSuccGroup:") for group in self.groups: diff --git a/tests/backend/conftest.py b/tests/backend/conftest.py index ad7e94a2..fc7b18c7 100644 --- a/tests/backend/conftest.py +++ b/tests/backend/conftest.py @@ -127,6 +127,62 @@ def __init__(self, large_scale: bool = False): ) +class NetForTest5(pb.Network): + def __init__(self): + super().__init__() + self.n1 = pb.InputProj(input=None, shape_out=(400,), name="n1") + self.n2 = pb.TonicSpiking(400, 3, name="n2") + self.n3 = pb.TonicSpiking(400, 3, name="n3") + self.n4 = pb.TonicSpiking(400, 3, name="n4") + self.n5 = pb.TonicSpiking(800, 3, name="n5") + self.n6 = pb.TonicSpiking(400, 4, name="n6") + self.s0 = pb.FullConn( + self.n1, self.n2, conn_type=pb.SynConnType.All2All, name="s0" + ) + self.s1 = pb.FullConn( + self.n2, self.n3, conn_type=pb.SynConnType.All2All, name="s1" + ) + self.s2 = pb.FullConn( + self.n3, self.n4, conn_type=pb.SynConnType.All2All, name="s2" + ) + self.s3 = pb.FullConn( + self.n4, self.n5, conn_type=pb.SynConnType.All2All, name="s3" + ) + self.s4 = pb.FullConn( + self.n5, self.n6, conn_type=pb.SynConnType.All2All, name="s4" + ) + self.s5 = pb.FullConn( + self.n1, self.n6, conn_type=pb.SynConnType.All2All, name="s5" + ) + self.s6 = pb.FullConn( + self.n2, self.n5, conn_type=pb.SynConnType.All2All, name="s6" + ) + + +class NetForTest6(pb.Network): + def __init__(self): + super().__init__() + self.n1 = pb.InputProj(input=None, shape_out=(400,), name="n1") + self.n2 = pb.InputProj(input=None, shape_out=(400,), name="n2") + self.n3 = pb.TonicSpiking(400, 3, name="n3") + self.n4 = pb.TonicSpiking(400, 3, name="n4") + self.s0 = pb.FullConn( + self.n1, self.n3, conn_type=pb.SynConnType.All2All, name="s0" + ) + self.s1 = pb.FullConn( + self.n1, self.n4, conn_type=pb.SynConnType.All2All, name="s1" + ) + self.s2 = pb.FullConn( + self.n2, self.n3, conn_type=pb.SynConnType.All2All, name="s2" + ) + self.s3 = pb.FullConn( + self.n2, self.n4, conn_type=pb.SynConnType.All2All, name="s3" + ) + self.s4 = pb.FullConn( + self.n3, self.n4, conn_type=pb.SynConnType.All2All, name="s4" + ) + + class Network_with_multi_inodes1(pb.Network): """Test the following situations with multiple input nodes: 1. Two input nodes with their own core blocks. @@ -682,6 +738,16 @@ def build_example_net4(): return NetForTest4() +@pytest.fixture(scope="class") +def build_example_net5(): + return NetForTest5() + + +@pytest.fixture(scope="class") +def build_example_net6(): + return NetForTest6() + + @pytest.fixture(scope="class") def build_example_net4_large_scale(): return NetForTest4(large_scale=True) diff --git a/tests/backend/test_mapper.py b/tests/backend/test_mapper.py index 9bd0022b..98db5144 100644 --- a/tests/backend/test_mapper.py +++ b/tests/backend/test_mapper.py @@ -375,15 +375,18 @@ def test_grouping_optim_core(self, monkeypatch, build_example_net4): mapper.build(net) mapper.compile(grouping_optim_target="core") - assert mapper.core_blocks[0].n_core_required == ceil( - net.n1.num_out / HwConfig.N_DENDRITE_MAX_SNN - ) - - assert mapper.core_blocks[1].n_core_required == 1 + 1 + for cb in mapper.core_blocks: + if net.n1 in cb.dest: + assert cb.n_core_required == ceil( + net.n1.num_out / HwConfig.N_DENDRITE_MAX_SNN + ) + elif net.n2 in cb.dest: + assert cb.n_core_required == 1 + 1 - assert mapper.core_blocks[2].n_core_required == ceil( - net.n4.num_out / HwConfig.N_DENDRITE_MAX_SNN - ) + elif net.n4 in cb.dest: + assert cb.n_core_required == ceil( + net.n4.num_out / HwConfig.N_DENDRITE_MAX_SNN + ) def test_grouping_optim_both(self, monkeypatch, build_example_net4): net = build_example_net4 @@ -448,6 +451,29 @@ def __init__(self): multicast_optim=[net.n0], ) + def test_ordered_axons(self, build_example_net5): + net = build_example_net5 + mapper = pb.Mapper() + mapper.build(net) + mapper.compile() + nodes_with_empty_axons = [net.n3, net.n4, net.n5] + for cb in mapper.core_blocks: + if cb.dest[0] in nodes_with_empty_axons: + assert len(cb.ordered_axons) > len(cb.source) + else: + assert len(cb.ordered_axons) == len(cb.source) + + def test_partition(self, build_example_net6): + net = build_example_net6 + mapper = pb.Mapper() + mapper.build(net) + mapper.compile() + for cb in mapper.core_blocks: + if net.n3 in cb.dest: + assert len(cb.ordered_axons) == 2 + if net.n4 in cb.dest: + assert len(cb.ordered_axons) == 3 + def test_core_estimate_only(self, build_example_net4): net = build_example_net4 From d9515d8fd65077029fac760f4fcb948e19b95513 Mon Sep 17 00:00:00 2001 From: yang1556 <92725391+yang1556@users.noreply.github.com> Date: Fri, 29 Nov 2024 23:50:09 +0800 Subject: [PATCH 146/187] support rinbuffer --- paibox/components/_modules.py | 5 +++-- paibox/components/functional.py | 15 +++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/paibox/components/_modules.py b/paibox/components/_modules.py index ea22fbe3..41da1bca 100644 --- a/paibox/components/_modules.py +++ b/paibox/components/_modules.py @@ -212,13 +212,14 @@ def _input_buffer_len_check( math.ceil(in_channels * in_h * kw / HwConfig.N_FANIN_PER_DENDRITE_ANN) ) ) - - if not kw * valid_interval > HwConfig.N_TIMESLOT_MAX / (2**E): + deep = min(in_h - kw, kw - 1) * valid_interval + 1 + if not HwConfig.N_TIMESLOT_MAX / (2**E) > deep: raise ResourceError( f"the input size of {self.name} is too large. Please adjust the input size or the number of channels." ) + class _LinearBase(FunctionalModule): def __init__( self, diff --git a/paibox/components/functional.py b/paibox/components/functional.py index 423af93c..732dd2f0 100644 --- a/paibox/components/functional.py +++ b/paibox/components/functional.py @@ -910,8 +910,13 @@ def build( ) -> BuiltComponentType: assert len(self.source[0].shape_out) == 2 self.ostream_attr = incoming_stream_attr + twe = 1 + self.ostream_attr.t_last_vld + ich, ih = self.source[0].shape_out + + if build_options.get("check_before_compile"): + self._input_buffer_len_check(ich, ih, ih, incoming_stream_attr.interval) n_delays = NodeList() s_delays = NodeList() s_weight = NodeList() @@ -932,7 +937,7 @@ def build( shape=(ich, ih), delay=incoming_stream_attr.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=self.tick_wait_end, + tick_wait_end=twe - incoming_stream_attr.interval * i, keep_shape=self.keep_shape, name=f"n{i}_{self.name}", ) @@ -1049,7 +1054,6 @@ def build( ow, ) twe = 1 + self.ostream_attr.t_last_vld - if build_options.get("check_before_compile"): self._input_buffer_len_check(cin, ih, kw, incoming_stream_attr.interval) @@ -1069,13 +1073,12 @@ def build( keep_shape=self.keep_shape, name=f"nd_{self.name}", ) - for i in range(kw): neuron = ANNBypassNeuron( (cin, ih), delay=incoming_stream_attr.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe, + tick_wait_end=twe - incoming_stream_attr.interval * i, name=f"n{i}_delay_{self.name}", ) n_delays.append(neuron) @@ -1235,7 +1238,7 @@ def build( (cin, ih), delay=incoming_stream_attr.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe, + tick_wait_end=twe - incoming_stream_attr.interval * i, keep_shape=self.keep_shape, name=f"n{i}_{self.name}", ) @@ -1367,7 +1370,7 @@ def build( (cin, ih), delay=incoming_stream_attr.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe, + tick_wait_end=twe - incoming_stream_attr.interval * i, keep_shape=self.keep_shape, name=f"n{i}_{self.name}", ) From 1a8372e112c3807822a2daf20932ff65360b3e84 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 00:51:05 +0000 Subject: [PATCH 147/187] :rotating_light: auto fix by pre-commit hooks --- paibox/components/_modules.py | 1 - paibox/components/functional.py | 1 - 2 files changed, 2 deletions(-) diff --git a/paibox/components/_modules.py b/paibox/components/_modules.py index 41da1bca..990b5a77 100644 --- a/paibox/components/_modules.py +++ b/paibox/components/_modules.py @@ -219,7 +219,6 @@ def _input_buffer_len_check( ) - class _LinearBase(FunctionalModule): def __init__( self, diff --git a/paibox/components/functional.py b/paibox/components/functional.py index 732dd2f0..37aa6fc9 100644 --- a/paibox/components/functional.py +++ b/paibox/components/functional.py @@ -912,7 +912,6 @@ def build( self.ostream_attr = incoming_stream_attr twe = 1 + self.ostream_attr.t_last_vld - ich, ih = self.source[0].shape_out if build_options.get("check_before_compile"): From 3298c5512f21d3a59ef5546ec24e7bef63f55974 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 2 Dec 2024 10:19:04 +0800 Subject: [PATCH 148/187] =?UTF-8?q?=E2=9C=85=20update=20the=20testcase=20`?= =?UTF-8?q?test=5Fgroup=5Fedges=5Fwith=5Fconstrs`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/backend/test_graphs.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/backend/test_graphs.py b/tests/backend/test_graphs.py index 6d3de2f0..9491ee0b 100644 --- a/tests/backend/test_graphs.py +++ b/tests/backend/test_graphs.py @@ -467,14 +467,18 @@ def test_group_edges_with_constrs( # In this case, N2 & N3 should be together. pos_n2 = pos_n3 = 0 for i, cb in enumerate(mapper.core_blocks): - _g_with_name = [e.name for e in cb._parents] + _g_with_name = [e.name for e in cb.obj] if "s2" in _g_with_name: pos_n2 = i + break + + for i, cb in enumerate(mapper.core_blocks): + _g_with_name = [e.name for e in cb.obj] if "s3" in _g_with_name: pos_n3 = i + break assert pos_n2 == pos_n3 - assert pos_n2 != 0 # In this case, N2 & N3 should be split. monkeypatch.setattr(net.n2, "_tws", 2) @@ -486,11 +490,16 @@ def test_group_edges_with_constrs( pos_n2 = pos_n3 = 0 for i, part in enumerate(mapper.core_blocks): - _g_with_name = [e.name for e in part._parents] + _g_with_name = [e.name for e in part.obj] if "s2" in _g_with_name: pos_n2 = i + break + + for i, part in enumerate(mapper.core_blocks): + _g_with_name = [e.name for e in part.obj] if "s3" in _g_with_name: pos_n3 = i + break assert pos_n2 != pos_n3 From 7608c4ded549468631834e6a0e3b1b02f491f0c2 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Thu, 28 Nov 2024 20:45:41 +0800 Subject: [PATCH 149/187] =?UTF-8?q?=F0=9F=90=9B=20bugfix:=20fix=20the=20in?= =?UTF-8?q?put=20buffer=20limit=20check=20for=20semi-folded=20ops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/components/_modules.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/paibox/components/_modules.py b/paibox/components/_modules.py index 990b5a77..30ca7397 100644 --- a/paibox/components/_modules.py +++ b/paibox/components/_modules.py @@ -201,21 +201,21 @@ def build( raise NotImplementedError def _input_buffer_len_check( - self, in_channels: int, in_h: int, kw: int, valid_interval: int + self, ich: int, ih: int, kw: int, interval: int ) -> None: """Check the limit of the semi-folded operators on the input buffer length of the core during the build phase. - NOTE: If the condition is not met, an expection will be raised in the subsequent compilation phase. + NOTE: The right side of the inequality will only be smaller in the backend. If the condition is not met, an \ + expection will be raised in the subsequent compilation phase. """ E = math.ceil( - math.log2( - math.ceil(in_channels * in_h * kw / HwConfig.N_FANIN_PER_DENDRITE_ANN) - ) + math.log2(math.ceil(ich * ih * kw / HwConfig.N_FANIN_PER_DENDRITE_ANN)) ) - deep = min(in_h - kw, kw - 1) * valid_interval + 1 - if not HwConfig.N_TIMESLOT_MAX / (2**E) > deep: + + if min(ih - kw, kw - 1) * interval + 1 >= (HwConfig.N_TIMESLOT_MAX >> E): + _adjust_text = "input size, kernel size or stride along the data flow." raise ResourceError( - f"the input size of {self.name} is too large. Please adjust the input size or the number of channels." + f"the data arrangement of {self.name}'s input buffer may be wrong. Please adjust the {_adjust_text}." ) From e0c18d7d28bfa5e2daa724c4790b897105ed8fc2 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 2 Dec 2024 14:16:04 +0800 Subject: [PATCH 150/187] =?UTF-8?q?=E2=9C=A8=20feat(base):=20use=20`DataSt?= =?UTF-8?q?reamFormat`=20to=20descriibe=20the=20format=20of=20dataflow.=20?= =?UTF-8?q?Update=20the=20update=20logic=20of=20dataflow=20format=20betwee?= =?UTF-8?q?n=20semi-folded=20ops.=20Labeling=20the=20dataflow=20format=20o?= =?UTF-8?q?n=20neurons.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/base.py | 51 ++++++++++++ paibox/components/_modules.py | 36 ++------ paibox/components/functional.py | 123 +++++++++++++++++----------- paibox/components/neuron/base.py | 46 ++++++++++- paibox/network.py | 14 ++-- tests/components/test_functional.py | 13 +-- 6 files changed, 189 insertions(+), 94 deletions(-) diff --git a/paibox/base.py b/paibox/base.py index 31e25387..89ffdbd0 100644 --- a/paibox/base.py +++ b/paibox/base.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass import sys from typing import Any, ClassVar, Literal, Optional @@ -256,6 +257,45 @@ def state(self) -> NodeDict: return self._memories +INFINITE_DATAFLOW = 0 # the dataflow is infinite. + + +@dataclass +class DataFlowFormat: + """Describe in detail the format of valid data in the dataflow.""" + + t_1st_vld: int + """The time of the first valid data, relative to `t_1st_vld` of the external input.""" + interval: int = 1 + """The interval of valid data in the flow.""" + n_vld: int = INFINITE_DATAFLOW + """The number of valid data. 0 for infinite dataflow.""" + + def __post_init__(self) -> None: + if self.n_vld < INFINITE_DATAFLOW: + raise ValueError( + f"'n_vld' should be greater than or equal to {INFINITE_DATAFLOW}, " + f"but got {self.n_vld}." + ) + + def t_at_idx(self, idx: int) -> int: + """The time of the valid data at the given index.""" + if self.n_vld > INFINITE_DATAFLOW: + assert 0 <= idx <= self.n_vld - 1 + + return self.t_1st_vld + idx * self.interval + + def t_at_n(self, n: int) -> int: + """The time of the n-th valid data.""" + return self.t_at_idx(n - 1) + + @property + def t_last_vld(self) -> int: + """The time of the last valid data.""" + assert self.n_vld > INFINITE_DATAFLOW + return self.t_at_n(self.n_vld) + + class NeuDyn(DynamicSys, ReceiveInputProj, TimeRelatedNode): _delay: int @@ -266,6 +306,9 @@ class NeuDyn(DynamicSys, ReceiveInputProj, TimeRelatedNode): _uf: int """unrolling_factor""" + oflow_format: DataFlowFormat + """The format of output data stream""" + def __init__(self, name: Optional[str] = None) -> None: super().__init__(name) self.master_nodes = NodeDict() @@ -291,6 +334,14 @@ def tick_wait_end(self) -> int: def unrolling_factor(self) -> int: return self._uf + @property + def end_tick(self) -> int: + """End time of work.""" + if self.tick_wait_end == 0: + return 9999 # Never end + + return self.tick_wait_start + self.tick_wait_end - 1 + @unrolling_factor.setter def unrolling_factor(self, factor: int) -> None: self._uf = arg_check_pos(factor, "'unrolling_factor'") diff --git a/paibox/components/_modules.py b/paibox/components/_modules.py index 30ca7397..a2a0953d 100644 --- a/paibox/components/_modules.py +++ b/paibox/components/_modules.py @@ -1,12 +1,10 @@ -import math import typing -from dataclasses import dataclass from typing import Literal, Optional, Union import numpy as np from paicorelib import TM, HwConfig -from paibox.base import NeuDyn, NodeList +from paibox.base import DataFlowFormat, NeuDyn, NodeList from paibox.exceptions import ResourceError, ShapeError from paibox.types import ( LEAK_V_DTYPE, @@ -58,7 +56,7 @@ "_SpikingPool2dWithV", "_SemiFoldedModule", "_LinearBase", - "SemiFoldedStreamAttr", + "SemiFoldedDataFlowFormat", ] @@ -161,41 +159,21 @@ class _DelayChainANN(_DelayChainBase): pass -@dataclass(frozen=True) -class SemiFoldedStreamAttr: - """Details of transmission of valid data in semi-folded form data stream.""" - - t_1st_vld: int - """The time of the first valid data, relative to `t_1st_vld` of the external input.""" - interval: int - """The interval of the output data stream.""" - n_data: int = 0 - """The number of valid output data.""" - - def t_at(self, n: int) -> int: - """The time of the n-th valid data.""" - if self.n_data > 0: - assert 1 <= n <= self.n_data - - return self.t_1st_vld + (n - 1) * self.interval - - @property - def t_last_vld(self) -> int: - """The time of the last valid data.""" - assert self.n_data > 0 - return self.t_at(self.n_data) +class SemiFoldedDataFlowFormat(DataFlowFormat): + pass @set_rt_mode_ann() class _SemiFoldedModule(FunctionalModule): """Functional modules with interfaces in semi-folded form. Use `build()` of class `HasSemiFoldedIntf`.""" - ostream_attr: SemiFoldedStreamAttr + inherent_delay = 1 + oflow_format: SemiFoldedDataFlowFormat def build( self, network: "DynSysGroup", - incoming_stream_attr: SemiFoldedStreamAttr, + incoming_flow_format: SemiFoldedDataFlowFormat, **build_options, ) -> BuiltComponentType: raise NotImplementedError diff --git a/paibox/components/functional.py b/paibox/components/functional.py index 37aa6fc9..ae0370e6 100644 --- a/paibox/components/functional.py +++ b/paibox/components/functional.py @@ -905,22 +905,26 @@ class LinearSemiFolded(_LinearBase, _SemiFoldedModule): def build( self, network: "DynSysGroup", - incoming_stream_attr: SemiFoldedStreamAttr, + incoming_flow_format: SemiFoldedDataFlowFormat, **build_options, ) -> BuiltComponentType: assert len(self.source[0].shape_out) == 2 - self.ostream_attr = incoming_stream_attr - twe = 1 + self.ostream_attr.t_last_vld + # For semi-folded linear, the valid output is at only one timestep. + self.oflow_format = SemiFoldedDataFlowFormat( + incoming_flow_format.t_last_vld, 1, 1 + ) + twe = 1 + self.oflow_format.t_last_vld ich, ih = self.source[0].shape_out if build_options.get("check_before_compile"): - self._input_buffer_len_check(ich, ih, ih, incoming_stream_attr.interval) + self._input_buffer_len_check(ich, ih, ih, incoming_flow_format.interval) + n_delays = NodeList() s_delays = NodeList() s_weight = NodeList() - n_fc = ANNNeuron( + n_linear = ANNNeuron( self.shape_out, self.bias, self.bit_trunc, @@ -930,13 +934,16 @@ def build( keep_shape=self.keep_shape, name=f"nd_{self.name}", ) + n_linear.set_oflow_format( + interval=self.oflow_format.interval, n_vld=self.oflow_format.n_vld + ) for i in range(ih): neuron = ANNBypassNeuron( shape=(ich, ih), - delay=incoming_stream_attr.interval * i + 1, + delay=incoming_flow_format.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe - incoming_stream_attr.interval * i, + tick_wait_end=twe - incoming_flow_format.interval * i, keep_shape=self.keep_shape, name=f"n{i}_{self.name}", ) @@ -954,15 +961,15 @@ def build( w = self.weights[ih - i - 1 :: ih, :] syn2 = FullConnSyn( neuron, - n_fc, + n_linear, weights=w, conn_type=ConnType.All2All, name=f"s{i}_{self.name}", ) s_weight.append(syn2) - generated = [n_fc, *n_delays, *s_delays, *s_weight] - self._rebuild_out_intf(network, n_fc, *generated, **build_options) + generated = [n_linear, *n_delays, *s_delays, *s_weight] + self._rebuild_out_intf(network, n_linear, *generated, **build_options) return generated @@ -1008,9 +1015,11 @@ def __init__( # XXX Do not consider the case when the shape of source neurons needs to be changed, for now. # neuron_s.shape_change((in_ch, in_h)) - cout, cin, kh, _ = kernel.shape + cout, cin, kh, kw = kernel.shape out_h = (in_h - kh + 2 * self.padding[0]) // self.stride[0] + 1 + assert self.padding[0] < kh and self.padding[1] < kw + if in_ch != cin: raise ShapeError(f"the channels mismatch: {in_ch} != {cin}.") @@ -1034,7 +1043,7 @@ def __init__( def build( self, network: "DynSysGroup", - incoming_stream_attr: SemiFoldedStreamAttr, + incoming_flow_format: SemiFoldedDataFlowFormat, **build_options, ) -> BuiltComponentType: assert len(self.source[0].shape_out) == 2 @@ -1047,14 +1056,15 @@ def build( _, cin, _, kw = self.kernel.shape _, ow = self.shape_out - self.ostream_attr = SemiFoldedStreamAttr( - incoming_stream_attr.t_at(kw - self.padding[0]), - incoming_stream_attr.interval * self.stride[1], + self.oflow_format = SemiFoldedDataFlowFormat( + incoming_flow_format.t_at_n(kw - self.padding[0]), + incoming_flow_format.interval * self.stride[1], ow, ) - twe = 1 + self.ostream_attr.t_last_vld + twe = 1 + self.oflow_format.t_last_vld + if build_options.get("check_before_compile"): - self._input_buffer_len_check(cin, ih, kw, incoming_stream_attr.interval) + self._input_buffer_len_check(cin, ih, kw, incoming_flow_format.interval) n_delays = NodeList() n_neg_padding = NodeList() @@ -1072,12 +1082,16 @@ def build( keep_shape=self.keep_shape, name=f"nd_{self.name}", ) + n_conv2d.set_oflow_format( + interval=self.oflow_format.interval, n_vld=self.oflow_format.n_vld + ) + for i in range(kw): neuron = ANNBypassNeuron( (cin, ih), - delay=incoming_stream_attr.interval * i + 1, + delay=incoming_flow_format.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe - incoming_stream_attr.interval * i, + tick_wait_end=twe - incoming_flow_format.interval * i, name=f"n{i}_delay_{self.name}", ) n_delays.append(neuron) @@ -1105,13 +1119,13 @@ def build( # Add additional negative padding layer to eliminate the incorrect output # NOTE: `t_1st_vld` = 0 & `padding[0]` > 0 means the previous layer is # an input node. No need to add negative padding layer for this case. - if incoming_stream_attr.t_1st_vld > 0: + if incoming_flow_format.t_1st_vld > 0: for p in range(self.padding[0]): neuron = ANNBypassNeuron( (cin, ih), - delay=1 + incoming_stream_attr.interval * (kw - 1 - p), + delay=1 + incoming_flow_format.interval * (kw - 1 - p), tick_wait_start=self.tick_wait_start, - tick_wait_end=incoming_stream_attr.t_1st_vld, + tick_wait_end=incoming_flow_format.t_1st_vld, keep_shape=self.keep_shape, name=f"n{p}_pad_{self.name}", ) @@ -1196,7 +1210,7 @@ def __init__( def build( self, network: "DynSysGroup", - incoming_stream_attr: SemiFoldedStreamAttr, + incoming_flow_format: SemiFoldedDataFlowFormat, **build_options, ) -> BuiltComponentType: assert len(self.source[0].shape_out) == 2 @@ -1209,20 +1223,20 @@ def build( kh, kw = self.kernel_size _, ow = self.shape_out - self.ostream_attr = SemiFoldedStreamAttr( - incoming_stream_attr.t_at(kw), - incoming_stream_attr.interval * self.stride[1], + self.oflow_format = SemiFoldedDataFlowFormat( + incoming_flow_format.t_at_n(kw), + incoming_flow_format.interval * self.stride[1], ow, ) - twe = 1 + self.ostream_attr.t_last_vld + twe = 1 + self.oflow_format.t_last_vld if build_options.get("check_before_compile"): - self._input_buffer_len_check(cin, ih, kw, incoming_stream_attr.interval) + self._input_buffer_len_check(cin, ih, kw, incoming_flow_format.interval) n_delays = NodeList() s_delays = NodeList() - pool2d = ANNNeuron( + n_pool2d = ANNNeuron( self.shape_out, delay=self.delay_relative, tick_wait_start=self.tick_wait_start + 1, @@ -1231,13 +1245,16 @@ def build( keep_shape=self.keep_shape, name=f"nd_{self.name}", ) + n_pool2d.set_oflow_format( + interval=self.oflow_format.interval, n_vld=self.oflow_format.n_vld + ) for i in range(kw): neuron = ANNBypassNeuron( (cin, ih), - delay=incoming_stream_attr.interval * i + 1, + delay=incoming_flow_format.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe - incoming_stream_attr.interval * i, + tick_wait_end=twe - incoming_flow_format.interval * i, keep_shape=self.keep_shape, name=f"n{i}_{self.name}", ) @@ -1253,7 +1270,7 @@ def build( s_delays.append(syn1) syn2 = MaxPoolSyn( neuron, - pool2d, + n_pool2d, weights=_poo2d_semifolded_mapping_mask( cin, ih, ow, kh, self.stride, (0, 0) ), @@ -1261,8 +1278,8 @@ def build( ) s_delays.append(syn2) - generated = [pool2d, *n_delays, *s_delays] - self._rebuild_out_intf(network, pool2d, *generated, **build_options) + generated = [n_pool2d, *n_delays, *s_delays] + self._rebuild_out_intf(network, n_pool2d, *generated, **build_options) return generated @@ -1302,6 +1319,8 @@ def __init__( assert len(neuron_s.shape_out) == 2 in_ch, in_h = neuron_s.shape_out out_h = (in_h - self.kernel_size[0] + 2 * self.padding[0]) // self.stride[0] + 1 + kh, kw = self.kernel_size + assert self.padding[0] < kh and self.padding[1] < kw super().__init__( neuron_s, @@ -1314,7 +1333,7 @@ def __init__( def build( self, network: "DynSysGroup", - incoming_stream_attr: SemiFoldedStreamAttr, + incoming_flow_format: SemiFoldedDataFlowFormat, **build_options, ) -> BuiltComponentType: assert len(self.source[0].shape_out) == 2 @@ -1327,15 +1346,15 @@ def build( kh, kw = self.kernel_size _, ow = self.shape_out - self.ostream_attr = SemiFoldedStreamAttr( - incoming_stream_attr.t_at(kw - self.padding[0]), - incoming_stream_attr.interval * self.stride[1], + self.oflow_format = SemiFoldedDataFlowFormat( + incoming_flow_format.t_at_n(kw - self.padding[0]), + incoming_flow_format.interval * self.stride[1], ow, ) - twe = 1 + self.ostream_attr.t_last_vld + twe = 1 + self.oflow_format.t_last_vld if build_options.get("check_before_compile"): - self._input_buffer_len_check(cin, ih, kw, incoming_stream_attr.interval) + self._input_buffer_len_check(cin, ih, kw, incoming_flow_format.interval) # NOTE: Division is achieved with the help of output truncation. # TODO Since division with a divisor that is an integer power of 2 can only be implemented by @@ -1355,7 +1374,7 @@ def build( s_delays = NodeList() s_neg_padding = NodeList() - pool2d = ANNNeuron( + n_pool2d = ANNNeuron( self.shape_out, delay=self.delay_relative, bit_trunc=bit_trunc, @@ -1364,12 +1383,16 @@ def build( keep_shape=self.keep_shape, name=f"nd_{self.name}", ) + n_pool2d.set_oflow_format( + interval=self.oflow_format.interval, n_vld=self.oflow_format.n_vld + ) + for i in range(kw): neuron = ANNBypassNeuron( (cin, ih), - delay=incoming_stream_attr.interval * i + 1, + delay=incoming_flow_format.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe - incoming_stream_attr.interval * i, + tick_wait_end=twe - incoming_flow_format.interval * i, keep_shape=self.keep_shape, name=f"n{i}_{self.name}", ) @@ -1385,7 +1408,7 @@ def build( s_delays.append(syn1) syn2 = FullConnSyn( neuron, - pool2d, + n_pool2d, weights=_poo2d_semifolded_mapping_mask( cin, ih, ow, kh, self.stride, self.padding ), @@ -1395,13 +1418,13 @@ def build( s_delays.append(syn2) # Add additional negative padding layer to eliminate the incorrect output - if incoming_stream_attr.t_1st_vld > 0: + if incoming_flow_format.t_1st_vld > 0: for p in range(self.padding[0]): neuron = ANNBypassNeuron( (cin, ih), - delay=1 + incoming_stream_attr.interval * (kw - 1 - p), + delay=1 + incoming_flow_format.interval * (kw - 1 - p), tick_wait_start=self.tick_wait_start, - tick_wait_end=incoming_stream_attr.t_1st_vld, + tick_wait_end=incoming_flow_format.t_1st_vld, keep_shape=self.keep_shape, name=f"n{p}_pad_{self.name}", ) @@ -1418,7 +1441,7 @@ def build( syn2 = FullConnSyn( neuron, - pool2d, + n_pool2d, weights=-_poo2d_semifolded_mapping_mask( cin, ih, ow, kh, self.stride, self.padding ), @@ -1427,8 +1450,8 @@ def build( ) s_neg_padding.append(syn2) - generated = [pool2d, *n_delays, *n_neg_padding, *s_delays, *s_neg_padding] - self._rebuild_out_intf(network, pool2d, *generated, **build_options) + generated = [n_pool2d, *n_delays, *n_neg_padding, *s_delays, *s_neg_padding] + self._rebuild_out_intf(network, n_pool2d, *generated, **build_options) return generated diff --git a/paibox/components/neuron/base.py b/paibox/components/neuron/base.py index a35fa8b5..7e226a41 100644 --- a/paibox/components/neuron/base.py +++ b/paibox/components/neuron/base.py @@ -20,7 +20,7 @@ get_core_mode, ) -from paibox.base import NeuDyn +from paibox.base import DataFlowFormat, NeuDyn, INFINITE_DATAFLOW from paibox.exceptions import NotSupportedError, PAIBoxWarning, ShapeError from paibox.types import ( NEUOUT_U8_DTYPE, @@ -487,11 +487,13 @@ def __init__( ), ) - """Auxiliary internal stateful attributes for debugging""" + """Non-stateful attributes.""" self._delay = arg_check_pos(delay, "'delay'") self._tws = arg_check_non_neg(tick_wait_start, "'tick_wait_start'") self._twe = arg_check_non_neg(tick_wait_end, "'tick_wait_end'") self._uf = arg_check_pos(unrolling_factor, "'unrolling_factor'") + # Default dataflow is infinite and continuous, starting at `tws`. + self.oflow_format = DataFlowFormat(self.tick_wait_start) def __len__(self) -> int: return self._n_neuron @@ -529,6 +531,46 @@ def update( def reset_state(self, *args, **kwargs) -> None: self.reset_memory() # Call reset of `StatusMemory`. + def set_oflow_format( + self, + t_1st_vld: Optional[int] = None, + interval: Optional[int] = None, + n_vld: Optional[int] = None + ) -> None: + assert hasattr(self, "oflow_format") + _t_1st_vld = ( + t_1st_vld if isinstance(t_1st_vld, int) else self.oflow_format.t_1st_vld + ) + + _interval = ( + arg_check_pos(interval, "interval") + if isinstance(interval, int) + else self.oflow_format.interval + ) + + _n_vld = ( + arg_check_non_neg(n_vld, "n_vld") + if isinstance(n_vld, int) + else self.oflow_format.n_vld + ) + + if _t_1st_vld < self.tick_wait_start: + raise ValueError( + f"the output time of the first valid data should be greater than or equal to " + f"{self.tick_wait_start}, but got {_t_1st_vld}." + ) + + if _n_vld > INFINITE_DATAFLOW: + if (t_last_vld := _t_1st_vld + (_n_vld - 1) * _interval) > self.end_tick: + raise ValueError( + f"valid data is output after the end time. The neuron stops working at " + f"{self.end_tick}, but still needs to output at {t_last_vld}." + ) + + self.oflow_format.t_1st_vld = _t_1st_vld + self.oflow_format.interval = _interval + self.oflow_format.n_vld = _n_vld + def __copy__(self) -> "Neuron": """Same as `__deepcopy__`.""" return self.__deepcopy__() diff --git a/paibox/network.py b/paibox/network.py index 4f0a4c12..6db83afd 100644 --- a/paibox/network.py +++ b/paibox/network.py @@ -7,7 +7,7 @@ from .base import DynamicSys, SynSys from .collector import Collector from .components import NeuModule, Neuron, Projection -from .components._modules import SemiFoldedStreamAttr, _SemiFoldedModule +from .components._modules import SemiFoldedDataFlowFormat, _SemiFoldedModule from .components.modules import BuiltComponentType from .exceptions import NotSupportedError from .mixin import Container @@ -102,18 +102,18 @@ def build_modules( generated = dict() - # For external input stream info: - # 1. The start time is 1 - # 2. The interval is 1 - # 3. The #N of data is -1 since it dosen't effect the subsequent output stream. + # For external input dataflow: + # 1. The start time is 0. + # 2. The interval is 1. + # 3. The #N of data is `INFINITE_DATA_STREAM` since it dosen't effect the subsequent output dataflow. # TODO Reserve an interface for setting the properties of external input from `FRONTEND_ENV`? - last_vld_output_attr = SemiFoldedStreamAttr(0, 1) + last_vld_output_attr = SemiFoldedDataFlowFormat(t_1st_vld=0) for m in modules: # TODO for the case of the ResBlock, the `pred_dg_semi_ops` will be used. if isinstance(m, _SemiFoldedModule): generated[m] = m.build(self, last_vld_output_attr, **build_options) - last_vld_output_attr = m.ostream_attr + last_vld_output_attr = m.oflow_format else: generated[m] = m.build(self, **build_options) diff --git a/tests/components/test_functional.py b/tests/components/test_functional.py index c2c63a82..56c80471 100644 --- a/tests/components/test_functional.py +++ b/tests/components/test_functional.py @@ -4,6 +4,7 @@ import paibox as pb from paibox.base import DynamicSys from paibox.components import NeuModule +from paibox.components._modules import _SemiFoldedModule from paibox.components.neuron.base import MetaNeuron from paibox.components.synapses.conv_utils import _conv2d_faster, _pair, _single from paibox.network import DynSysGroup @@ -976,12 +977,12 @@ def test_Conv2dSemiFolded_FC_ChainNet( probe_linear = pb.Probe(generated[linear][0], "output") sim1.add_probe(probe_linear) - semi_folded_modules = [*conv2d_list, linear] + semi_folded_modules: list[_SemiFoldedModule] = [*conv2d_list, linear] # The interval & the time o the first valid data of the external input data stream semi_vld_out_intv0 = 1 t_1st_vld_data0 = 0 # The interval & the time of the first valid data of the current layers - semi_vld_out_intv = [m.ostream_attr.interval for m in semi_folded_modules] + semi_vld_out_intv = [m.oflow_format.interval for m in semi_folded_modules] t_1st_vld_data = [0] * n_conv for i in range(n_conv): if i == 0: @@ -1049,7 +1050,7 @@ def test_Conv2dSemiFolded_FC_ChainNet( assert np.array_equal( expected_fc_t, sim1.data[probe_linear][ - linear.tick_wait_start + linear.ostream_attr.t_last_vld + linear.tick_wait_start + linear.oflow_format.t_last_vld ], ) @@ -1171,12 +1172,12 @@ def test_Pool2dSemiFolded_FC_ChainNet( probe_linear = pb.Probe(generated[linear][0], "output") sim1.add_probe(probe_linear) - semi_folded_modules = [*pool2d_list, linear] + semi_folded_modules: list[_SemiFoldedModule] = [*pool2d_list, linear] # The interval & the time o the first valid data of the external input data stream semi_vld_out_intv0 = 1 t_1st_vld_data0 = 0 # The interval & the time of the first valid data of the current layers - semi_vld_out_intv = [m.ostream_attr.interval for m in semi_folded_modules] + semi_vld_out_intv = [m.oflow_format.interval for m in semi_folded_modules] t_1st_vld_data = [0] * n_pool for i in range(n_pool): if i == 0: @@ -1231,7 +1232,7 @@ def test_Pool2dSemiFolded_FC_ChainNet( assert np.array_equal( expected_fc_t, sim1.data[probe_linear][ - linear.tick_wait_start + linear.ostream_attr.t_last_vld + linear.tick_wait_start + linear.oflow_format.t_last_vld ], ) From 3122dc6b3878aff7644ed0538d9a515525670da0 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 2 Dec 2024 14:19:58 +0800 Subject: [PATCH 151/187] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20support=20py3.13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/codecov.yml | 2 +- pyproject.toml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 05f9ce7b..35d36c57 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -22,7 +22,7 @@ jobs: pytest: strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} diff --git a/pyproject.toml b/pyproject.toml index 71124dab..08223a17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ homepage = "https://github.com/PAICookers/PAIBox" documentation = "https://github.com/PAICookers/PAIBox#readme" keywords = ["PAICORE 2.0", "PAIBox", "SNN", "Toolchain"] classifiers = [ + "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Operating System :: OS Independent", @@ -24,9 +25,9 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Artificial Intelligence", - "Topic :: Software Development :: Build Tools", - "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Compilers", ] packages = [{ include = "paibox" }] From 109b74dbe5241774706a0e97424e10ea911892fd Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 2 Dec 2024 14:22:29 +0800 Subject: [PATCH 152/187] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20removed=20an=20?= =?UTF-8?q?always=20failed=20pre-commit=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 490b6d15..b5b4b101 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,6 @@ repos: - id: check-symlinks - id: check-merge-conflict - id: mixed-line-ending - - id: name-tests-test args: [--pytest-test-first] - id: requirements-txt-fixer - id: pretty-format-json From e0f8342a5be7597a6aa826e68d61bb5cd9591b6b Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 2 Dec 2024 15:42:47 +0800 Subject: [PATCH 153/187] =?UTF-8?q?=E2=9C=A8=20feat(base):=20update=20arg?= =?UTF-8?q?=20check=20for=20`DataFlowFormat`=20&=20function=20`set=5Foflow?= =?UTF-8?q?=5Fformat`=20in=20neuron.=20Add=20test=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/base.py | 54 ++++++++++++++++++++----- paibox/components/neuron/base.py | 69 +++++++++++++++++--------------- tests/test_base.py | 19 ++++++++- 3 files changed, 97 insertions(+), 45 deletions(-) diff --git a/paibox/base.py b/paibox/base.py index 89ffdbd0..c47260ff 100644 --- a/paibox/base.py +++ b/paibox/base.py @@ -257,26 +257,23 @@ def state(self) -> NodeDict: return self._memories -INFINITE_DATAFLOW = 0 # the dataflow is infinite. +INFINITE_DATAFLOW = 0 @dataclass class DataFlowFormat: """Describe in detail the format of valid data in the dataflow.""" - t_1st_vld: int - """The time of the first valid data, relative to `t_1st_vld` of the external input.""" + t_1st_vld: int = 0 + """Global time or a relative time of the first valid data in the dataflow, determined by `is_local_time`.""" interval: int = 1 - """The interval of valid data in the flow.""" + """The interval of valid data in the dataflow.""" n_vld: int = INFINITE_DATAFLOW - """The number of valid data. 0 for infinite dataflow.""" + """The number of valid data. <0 for infinite dataflow.""" - def __post_init__(self) -> None: - if self.n_vld < INFINITE_DATAFLOW: - raise ValueError( - f"'n_vld' should be greater than or equal to {INFINITE_DATAFLOW}, " - f"but got {self.n_vld}." - ) + is_local_time: bool = True + """Whether the `t_1st_vld` is relative to the local time(tws+T) of the neuron, or \ + relative to the global time of the external input.""" def t_at_idx(self, idx: int) -> int: """The time of the valid data at the given index.""" @@ -295,6 +292,41 @@ def t_last_vld(self) -> int: assert self.n_vld > INFINITE_DATAFLOW return self.t_at_n(self.n_vld) + def get_global_t_1st_vld(self, tws: int) -> int: + """Get the global time of the first valid data.""" + return tws + self.t_1st_vld if self.is_local_time else self.t_1st_vld + + def _check_after_assign(self, tws: int, end_tick: int) -> None: + _t_1st_vld_out_of_range_text = ( + "the {0} output time of the first valid data should be in the working " + + "time from {1} to {2}, but got {3}." + ) + + # The global time of the first valid data is in [tws, end_tick]. + gb_t_1st_vld = self.get_global_t_1st_vld(tws) + if gb_t_1st_vld < tws or gb_t_1st_vld > end_tick: + if self.is_local_time: + raise ValueError( + _t_1st_vld_out_of_range_text.format( + "local", "+0", f"+{end_tick - tws + 1}", self.t_1st_vld + ) + ) + else: + raise ValueError( + _t_1st_vld_out_of_range_text.format( + "global", tws, end_tick, self.t_1st_vld + ) + ) + + if self.n_vld > INFINITE_DATAFLOW: + if ( + t_last_vld := gb_t_1st_vld + (self.n_vld - 1) * self.interval + ) > end_tick: + raise ValueError( + f"valid data is output after the end time. The neuron stops working at " + f"{end_tick}, but still needs to output at {t_last_vld}." + ) + class NeuDyn(DynamicSys, ReceiveInputProj, TimeRelatedNode): diff --git a/paibox/components/neuron/base.py b/paibox/components/neuron/base.py index 7e226a41..14def12f 100644 --- a/paibox/components/neuron/base.py +++ b/paibox/components/neuron/base.py @@ -20,7 +20,7 @@ get_core_mode, ) -from paibox.base import DataFlowFormat, NeuDyn, INFINITE_DATAFLOW +from paibox.base import DataFlowFormat, NeuDyn from paibox.exceptions import NotSupportedError, PAIBoxWarning, ShapeError from paibox.types import ( NEUOUT_U8_DTYPE, @@ -492,8 +492,8 @@ def __init__( self._tws = arg_check_non_neg(tick_wait_start, "'tick_wait_start'") self._twe = arg_check_non_neg(tick_wait_end, "'tick_wait_end'") self._uf = arg_check_pos(unrolling_factor, "'unrolling_factor'") - # Default dataflow is infinite and continuous, starting at `tws`. - self.oflow_format = DataFlowFormat(self.tick_wait_start) + # Default dataflow is infinite and continuous, starting at tws+0. + self.oflow_format = DataFlowFormat(0, is_local_time=True) def __len__(self) -> int: return self._n_neuron @@ -535,41 +535,44 @@ def set_oflow_format( self, t_1st_vld: Optional[int] = None, interval: Optional[int] = None, - n_vld: Optional[int] = None + n_vld: Optional[int] = None, + *, + format_type: type[DataFlowFormat] = DataFlowFormat, ) -> None: - assert hasattr(self, "oflow_format") - _t_1st_vld = ( - t_1st_vld if isinstance(t_1st_vld, int) else self.oflow_format.t_1st_vld - ) - - _interval = ( - arg_check_pos(interval, "interval") - if isinstance(interval, int) - else self.oflow_format.interval - ) - - _n_vld = ( - arg_check_non_neg(n_vld, "n_vld") - if isinstance(n_vld, int) - else self.oflow_format.n_vld - ) - - if _t_1st_vld < self.tick_wait_start: - raise ValueError( - f"the output time of the first valid data should be greater than or equal to " - f"{self.tick_wait_start}, but got {_t_1st_vld}." + """Set the attributes of output dataflow format by given arguments.""" + if hasattr(self, "oflow_format"): + _t_1st_vld = ( + t_1st_vld if isinstance(t_1st_vld, int) else self.oflow_format.t_1st_vld ) - - if _n_vld > INFINITE_DATAFLOW: - if (t_last_vld := _t_1st_vld + (_n_vld - 1) * _interval) > self.end_tick: + _interval = ( + arg_check_pos(interval, "interval") + if isinstance(interval, int) + else self.oflow_format.interval + ) + _n_vld = ( + arg_check_non_neg(n_vld, "n_vld") + if isinstance(n_vld, int) + else self.oflow_format.n_vld + ) + self._assign_flow_format(_t_1st_vld, _interval, _n_vld) + else: + if not ( + isinstance(interval, int) + and isinstance(n_vld, int) + and isinstance(t_1st_vld, int) + ): raise ValueError( - f"valid data is output after the end time. The neuron stops working at " - f"{self.end_tick}, but still needs to output at {t_last_vld}." + "if 'oflow_format' is not set, 't_1st_vld', 'interval' & 'n_vld' must be set." ) - self.oflow_format.t_1st_vld = _t_1st_vld - self.oflow_format.interval = _interval - self.oflow_format.n_vld = _n_vld + self.oflow_format = format_type(t_1st_vld, interval, n_vld) + self.oflow_format._check_after_assign(self.tick_wait_start, self.end_tick) + + def _assign_flow_format(self, t_1st_vld: int, intv: int, n_vld: int) -> None: + self.oflow_format.t_1st_vld = t_1st_vld + self.oflow_format.interval = intv + self.oflow_format.n_vld = n_vld + self.oflow_format._check_after_assign(self.tick_wait_start, self.end_tick) def __copy__(self) -> "Neuron": """Same as `__deepcopy__`.""" diff --git a/tests/test_base.py b/tests/test_base.py index 5adb2c7f..cd55346d 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,7 +1,7 @@ import pytest import paibox as pb -from paibox.base import PAIBoxObject +from paibox.base import PAIBoxObject, DataFlowFormat from paibox.exceptions import RegisterError @@ -41,3 +41,20 @@ def test_paiboxobject_nodes(): nodes4 = obj1.nodes(method="absolute", level=-1, include_self=True) assert nodes4["obj111"] == obj1 + + +class TestDataFlowFormat: + def test_dff_infinite_dataflow(self): + with pytest.raises((AssertionError, ValueError)): + dff = DataFlowFormat(1, 0, -1) + _ = dff.t_last_vld + + def test_dff_valid(self): + # 1. t1 >= tws, t_last > endtick + dff1 = DataFlowFormat(10, 3, 10, is_local_time=False) + with pytest.raises(ValueError): + dff1._check_after_assign(8, 36) + + # 2. t1 >= tws, t_last <= endtick + dff2 = DataFlowFormat(10, 3, 10, is_local_time=True) + dff2._check_after_assign(2, 39) From 870e254554abbd72c8fd16d5f90f997cac128053 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 2 Dec 2024 15:48:51 +0800 Subject: [PATCH 154/187] =?UTF-8?q?=E2=9C=A8=20feat(graphs):=20corrected?= =?UTF-8?q?=20the=20calculation=20method=20of=20attribute=20`inherent=5Fti?= =?UTF-8?q?mestep`.=20Strictly=20annotate=20the=20data=20flow=20format=20f?= =?UTF-8?q?or=20the=20computational=20neuron=20of=20the=20semi-folded=20op?= =?UTF-8?q?s.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/graphs.py | 20 +++++++++++++------- paibox/components/functional.py | 16 ++++++++++++---- tests/components/test_functional.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/paibox/backend/graphs.py b/paibox/backend/graphs.py index c3e2751d..0f1f68ad 100644 --- a/paibox/backend/graphs.py +++ b/paibox/backend/graphs.py @@ -127,7 +127,7 @@ def _pre_build(self, **build_options) -> None: # checks. These additional checks may be removed as more network structures will be supported. # Currently, `LinearSemiFolded` is at the end of the network, since it will change the form of - # the input data stream, and its effective output is at the same time. + # the input dataflow, and its effective output is at the same time. semi_linears = modules.subset(LinearSemiFolded) if not all( len(succ_dg_semi_ops[linear]) == 0 for linear in semi_linears @@ -172,9 +172,14 @@ def _update_graph(self, **build_options) -> None: self.inodes = self._raw_nodes.subset(InputProj) # By default, nodes with out-degree = 0 are considered as output nodes. - self.onodes = self._raw_nodes.key_on_condition( - lambda node: self.degree_of_nodes[node].out_degree == 0 - ) # type: ignore + # TODO A node with out-degree can also be an output node. However, no network for now has this topology. + self.onodes = Collector( + { + k: cast(DestNodeType, v) + for k, v in self._raw_nodes.items() + if self.degree_of_nodes[k].out_degree == 0 + } + ).not_subset(InputProj) for name, node in self._raw_nodes.items(): self.nodes[name] = NodeAttr( @@ -525,9 +530,10 @@ def _find_rg_by_cb( @property def inherent_timestep(self) -> int: self.build_check() - _, distance = get_longest_path(self.succ_dg, self.ordered_nodes) - - return distance + return max( + n.oflow_format.get_global_t_1st_vld(n.tick_wait_start) + for n in self.onodes.values() + ) @property def graph_name_repr(self) -> str: diff --git a/paibox/components/functional.py b/paibox/components/functional.py index ae0370e6..6755cc5e 100644 --- a/paibox/components/functional.py +++ b/paibox/components/functional.py @@ -935,7 +935,9 @@ def build( name=f"nd_{self.name}", ) n_linear.set_oflow_format( - interval=self.oflow_format.interval, n_vld=self.oflow_format.n_vld + self.oflow_format.t_1st_vld, + self.oflow_format.interval, + self.oflow_format.n_vld, ) for i in range(ih): @@ -1083,7 +1085,9 @@ def build( name=f"nd_{self.name}", ) n_conv2d.set_oflow_format( - interval=self.oflow_format.interval, n_vld=self.oflow_format.n_vld + self.oflow_format.t_1st_vld, + self.oflow_format.interval, + self.oflow_format.n_vld, ) for i in range(kw): @@ -1246,7 +1250,9 @@ def build( name=f"nd_{self.name}", ) n_pool2d.set_oflow_format( - interval=self.oflow_format.interval, n_vld=self.oflow_format.n_vld + self.oflow_format.t_1st_vld, + self.oflow_format.interval, + self.oflow_format.n_vld, ) for i in range(kw): @@ -1384,7 +1390,9 @@ def build( name=f"nd_{self.name}", ) n_pool2d.set_oflow_format( - interval=self.oflow_format.interval, n_vld=self.oflow_format.n_vld + self.oflow_format.t_1st_vld, + self.oflow_format.interval, + self.oflow_format.n_vld, ) for i in range(kw): diff --git a/tests/components/test_functional.py b/tests/components/test_functional.py index 56c80471..4aae8961 100644 --- a/tests/components/test_functional.py +++ b/tests/components/test_functional.py @@ -1043,6 +1043,16 @@ def test_Conv2dSemiFolded_FC_ChainNet( ], ) + assert conv2d_list[i_conv].tick_wait_start + t_1st_vld_data[ + i_conv + ] + i * semi_vld_out_intv[i_conv] - 1 == conv2d_list[ + i_conv + ].tick_wait_start + conv2d_list[ + i_conv + ].oflow_format.t_at_idx( + i + ) + # x is the reference result of the last convolution. expected_fc_t = _ann_bit_trunc(x.ravel() @ fc_weight.astype(VOLTAGE_DTYPE)) @@ -1053,6 +1063,10 @@ def test_Conv2dSemiFolded_FC_ChainNet( linear.tick_wait_start + linear.oflow_format.t_last_vld ], ) + assert ( + linear.oflow_format.get_global_t_1st_vld(linear.tick_wait_start) + == linear.tick_wait_start + linear.oflow_format.t_last_vld + ) @pytest.mark.parametrize( "ishape_chw, n_pool, kshape_hw, stride, padding, out_features, pool_type", @@ -1225,6 +1239,16 @@ def test_Pool2dSemiFolded_FC_ChainNet( ], ) + assert pool2d_list[i_pool].tick_wait_start + t_1st_vld_data[ + i_pool + ] + i * semi_vld_out_intv[i_pool] - 1 == pool2d_list[ + i_pool + ].tick_wait_start + pool2d_list[ + i_pool + ].oflow_format.t_at_idx( + i + ) + # x is the reference result of the last pooling. expected_fc_t = _ann_bit_trunc(x.ravel() @ fc_weight.astype(VOLTAGE_DTYPE)) @@ -1236,6 +1260,11 @@ def test_Pool2dSemiFolded_FC_ChainNet( ], ) + assert ( + linear.oflow_format.get_global_t_1st_vld(linear.tick_wait_start) + == linear.tick_wait_start + linear.oflow_format.t_last_vld + ) + @pytest.mark.parametrize( "shape, weight", [ From c81ffd18886efb41cf7048270c26074350b55c2a Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 2 Dec 2024 15:50:20 +0800 Subject: [PATCH 155/187] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20typing(neuron):?= =?UTF-8?q?=20add=20typed=20dict=20`ExtraNeuAttrKwds`=20to=20check=20extra?= =?UTF-8?q?=20keywords=20passing=20to=20neurons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/components/modules.py | 4 ++-- paibox/components/neuron/base.py | 10 +++++----- paibox/components/neuron/neurons.py | 25 +++++++++++++++---------- paibox/components/neuron/utils.py | 24 ++++++++++++++++++++++-- 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/paibox/components/modules.py b/paibox/components/modules.py index 41e06fdc..2c13edef 100644 --- a/paibox/components/modules.py +++ b/paibox/components/modules.py @@ -14,7 +14,7 @@ from paibox.types import NEUOUT_U8_DTYPE, NeuOutType, VoltageType from paibox.utils import check_elem_unique, shape2num -from .neuron.utils import _input_width_format, _RTModeKwds, _spike_width_format +from .neuron.utils import _input_width_format, RTModeKwds, _spike_width_format from .projection import InputProj if sys.version_info >= (3, 10): @@ -91,7 +91,7 @@ class NeuModule(NeuDyn, BuildingModule): """#N of outputs.""" inherent_delay: int = 0 """Internal delay of the module, relative to the external.""" - rt_mode_kwds: _RTModeKwds + rt_mode_kwds: RTModeKwds mode: CoreMode def __init__( diff --git a/paibox/components/neuron/base.py b/paibox/components/neuron/base.py index 14def12f..bef9976b 100644 --- a/paibox/components/neuron/base.py +++ b/paibox/components/neuron/base.py @@ -21,7 +21,7 @@ ) from paibox.base import DataFlowFormat, NeuDyn -from paibox.exceptions import NotSupportedError, PAIBoxWarning, ShapeError +from paibox.exceptions import ConfigInvalidError, PAIBoxWarning, ShapeError from paibox.types import ( NEUOUT_U8_DTYPE, VOLTAGE_DTYPE, @@ -44,7 +44,7 @@ _input_width_format, _leak_v_check, _mask, - _RTModeKwds, + RTModeKwds, _spike_width_format, vjt_overflow, ) @@ -57,7 +57,7 @@ class MetaNeuron: """Meta neuron""" - rt_mode_kwds: _RTModeKwds + rt_mode_kwds: RTModeKwds mode: CoreMode def __init__( @@ -96,8 +96,8 @@ def __init__( # check whether the mode is valid self.mode = get_core_mode(input_width, spike_width, snn_en) - if pool_max == True and self.mode != CoreMode.MODE_ANN: - raise NotSupportedError( + if pool_max and self.mode != CoreMode.MODE_ANN: + raise ConfigInvalidError( f"max pooling is only supported in {CoreMode.MODE_ANN.name}, " f"but got {self.mode.name}." ) diff --git a/paibox/components/neuron/neurons.py b/paibox/components/neuron/neurons.py index 85e3df47..3f7f0279 100644 --- a/paibox/components/neuron/neurons.py +++ b/paibox/components/neuron/neurons.py @@ -8,10 +8,15 @@ from paibox.types import LEAK_V_DTYPE, DataType, Shape from .base import Neuron -from .utils import LEAK_V_MAX +from .utils import LEAK_V_MAX, ExtraNeuAttrKwds + +if sys.version_info >= (3, 12): + from typing import Unpack +else: + from typing_extensions import Unpack if sys.version_info >= (3, 13): - from typing import deprecated + from warnings import deprecated else: from typing_extensions import deprecated @@ -37,7 +42,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs, + **kwargs: Unpack[ExtraNeuAttrKwds], ) -> None: """IF neuron. @@ -93,7 +98,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs, + **kwargs: Unpack[ExtraNeuAttrKwds], ) -> None: """LIF neuron. @@ -152,7 +157,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs, + **kwargs: Unpack[ExtraNeuAttrKwds], ) -> None: """Tonic spiking neuron. @@ -178,7 +183,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs, + **kwargs: Unpack[ExtraNeuAttrKwds], ) -> None: """Phasic spiking neuron. Once the neuron receives `N` spikes and fires, it will reset to \ the negative floor and never fires again. `N` is `fire_step`. @@ -213,7 +218,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs, + **kwargs: Unpack[ExtraNeuAttrKwds], ) -> None: """A neuron that always outputs 1 as long as it starts working. @@ -245,7 +250,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs, + **kwargs: Unpack[ExtraNeuAttrKwds], ) -> None: """Bypass neuron. Output is equal to input. @@ -279,7 +284,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs, + **kwargs: Unpack[ExtraNeuAttrKwds], ) -> None: """General neuron used in ANN mode. Positive threshold = 1, negative threshold = 0.""" kwargs["bit_truncation"] = bit_trunc @@ -299,7 +304,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs, + **kwargs: Unpack[ExtraNeuAttrKwds], ) -> None: super().__init__( shape, bias=0, bit_trunc=8, keep_shape=keep_shape, name=name, **kwargs diff --git a/paibox/components/neuron/utils.py b/paibox/components/neuron/utils.py index 1d9ea2ff..dd58006f 100644 --- a/paibox/components/neuron/utils.py +++ b/paibox/components/neuron/utils.py @@ -2,7 +2,12 @@ from typing import Literal, TypedDict, Union import numpy as np -from paicorelib import InputWidthFormat, SNNModeEnable, SpikeWidthFormat +from paicorelib import ( + InputWidthFormat, + SNNModeEnable, + SpikeWidthFormat, + MaxPoolingEnable, +) from paicorelib.framelib.utils import _mask from paicorelib.ram_model import ( BIT_TRUNCATE_MAX, @@ -107,9 +112,24 @@ def _get_neu_out_dtype( return NEUOUT_U8_DTYPE -class _RTModeKwds(TypedDict): +class RTModeKwds(TypedDict): """A typed keywords for runtime mode. Only for checking if necessary.""" input_width: InputWidthFormat spike_width: SpikeWidthFormat snn_en: SNNModeEnable + + +class ExtraNeuAttrKwds(TypedDict, total=False): + """A typed keywords for extra neuron attributes.""" + + bit_truncation: int # For ANNNeuron + delay: int + tick_wait_start: int + tick_wait_end: int + input_width: Union[L[1, 8], InputWidthFormat] + spike_width: Union[L[1, 8], SpikeWidthFormat] + snn_en: Union[bool, SNNModeEnable] + pool_max: Union[bool, MaxPoolingEnable] + unrolling_factor: int + overflow_strict: bool From 2437357f6014170d8d1d871775266ec8cc2a6b79 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 2 Dec 2024 16:05:46 +0800 Subject: [PATCH 156/187] =?UTF-8?q?=F0=9F=90=9B=20bugfix(compile):=20if=20?= =?UTF-8?q?compiling=20with=20`core=5Festimate=5Fonly`=20turned=20on,=20pr?= =?UTF-8?q?event=20exporting=20the=20compiled=20results?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/mapper.py | 19 ++++++++++++++++--- paibox/exceptions.py | 6 ++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/paibox/backend/mapper.py b/paibox/backend/mapper.py index 6012d7fc..b91e2814 100644 --- a/paibox/backend/mapper.py +++ b/paibox/backend/mapper.py @@ -8,7 +8,7 @@ from paibox.base import SynSys from paibox.components import Neuron -from paibox.exceptions import ConfigInvalidError, ResourceError +from paibox.exceptions import CompileError, ConfigInvalidError, ResourceError from paibox.network import DynSysGroup from .conf_exporting import * @@ -71,6 +71,9 @@ def __init__(self) -> None: chip_list=_BACKEND_CONTEXT["target_chip_addr"] ) + self._core_estimate_only = False + """Wether this compilation is for core estimation only. If so, no core will be assigned.""" + self.clear() def clear(self) -> None: @@ -90,6 +93,8 @@ def clear(self) -> None: self.n_core_required = 0 self.n_core_occupied = 0 + self._core_estimate_only = False + # Set default cflags _BACKEND_CONTEXT.cflags.clear() set_cflag(enable_wp_opt=True) @@ -169,6 +174,8 @@ def compile( set_cflag(multicast_optim=True) set_cflag(multicast_optim_nodes=_mul_optim_nodes) + self._core_estimate_only = core_estimate_only + """Preperation. 1. Check whether the PAIGraph has built. 2. Set global compilation flags. @@ -192,9 +199,9 @@ def compile( self.cb_axon_grouping() """Core coordinate assignment.""" - self.coord_assign(core_estimate_only) + self.coord_assign(self._core_estimate_only) - if core_estimate_only: + if self._core_estimate_only: return GraphInfo( name=self.graph.graph_name_repr, input={}, @@ -619,6 +626,12 @@ def export( Return: total configurations in dictionary format. """ + if self._core_estimate_only: + raise CompileError( + "the current compilation is only for core estimation. " + "Please disable 'core_estimate_only' and compile again before exporting." + ) + if format not in ("bin", "npy", "txt"): raise ValueError(f"format {format} is not supported.") diff --git a/paibox/exceptions.py b/paibox/exceptions.py index 55514a43..bcc4d32a 100644 --- a/paibox/exceptions.py +++ b/paibox/exceptions.py @@ -74,6 +74,12 @@ class FunctionalError(PAIBoxError, RuntimeError): pass +class CompileError(PAIBoxError, RuntimeError): + """Exception for compilation.""" + + pass + + class RoutingError(PAIBoxError): """Exception for routing tree.""" From 1e0451fce217a4f322d4a4ba3972ca099fa1aabb Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 2 Dec 2024 16:16:14 +0800 Subject: [PATCH 157/187] =?UTF-8?q?=F0=9F=9A=B8=20typing:=20update=20typin?= =?UTF-8?q?g=20&=20error=20handling.=20Removed=20useless=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/graphs.py | 66 +++++++++++++++++++------------------ paibox/backend/mapper.py | 27 +++++++-------- paibox/backend/placement.py | 8 +++-- paibox/backend/routing.py | 9 ++--- paibox/backend/types.py | 13 +++----- paibox/exceptions.py | 8 ++++- paibox/utils.py | 16 ++++----- 7 files changed, 74 insertions(+), 73 deletions(-) diff --git a/paibox/backend/graphs.py b/paibox/backend/graphs.py index 0f1f68ad..69a117e7 100644 --- a/paibox/backend/graphs.py +++ b/paibox/backend/graphs.py @@ -2,14 +2,18 @@ from collections import defaultdict from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass, field -from typing import Any, TypeVar, Union +from typing import Any, TypeVar, Union, cast from paicorelib import HwConfig from paibox.collector import Collector from paibox.components import FullConnectedSyn, InputProj, NeuModule, Neuron from paibox.components.functional import LinearSemiFolded -from paibox.exceptions import GraphBuildError, GraphConnectionError, NotSupportedError +from paibox.exceptions import ( + GraphBuildError, + GraphConnectionError, + GraphNotSupportedError, +) from paibox.network import DynSysGroup from paibox.utils import check_elem_unique @@ -132,7 +136,7 @@ def _pre_build(self, **build_options) -> None: if not all( len(succ_dg_semi_ops[linear]) == 0 for linear in semi_linears ): - raise NotSupportedError( + raise GraphNotSupportedError( "currently, the semi-folded linear can only be used as output of the network." ) @@ -183,9 +187,7 @@ def _update_graph(self, **build_options) -> None: for name, node in self._raw_nodes.items(): self.nodes[name] = NodeAttr( - node=node, - position=self._node_pos(name), - degree=self.degree_of_nodes[name], + node, self._node_pos(name), self.degree_of_nodes[name] ) self.ordered_nodes = toposort(self.succ_dg) @@ -220,7 +222,7 @@ def topo_support_check(self) -> None: onode.num_out > HwConfig.N_FANIN_PER_DENDRITE_MAX for onode in self.onodes.values() ): - raise NotSupportedError( + raise GraphNotSupportedError( f"only output nodes with no more than {HwConfig.N_FANIN_PER_DENDRITE_MAX} " f"neurons are supported." ) @@ -541,7 +543,7 @@ def graph_name_repr(self) -> str: return _prefix + "_and_".join(network.name for network in self._raw_networks) -_NT = TypeVar("_NT", CoreBlock, NodeName, RoutingGroup) +_NT = TypeVar("_NT", CoreBlock, NodeName, RoutingGroup, MergedSuccGroup) _T = TypeVar("_T") @@ -557,7 +559,7 @@ def _degree_check( if isinstance(succ_node, CoreBlock) else str(succ_node) ) - raise NotSupportedError( + raise GraphNotSupportedError( f"If out-degree of a node is greater than 1, the in-degree of its sucessors must be 1. " f"However, in-degree of {_node_repr} is {degree_of_nodes[succ_node].in_degree}." ) @@ -570,7 +572,7 @@ def find_cycles(directed_edges: Mapping[_NT, Iterable[_NT]]) -> list[list[_NT]]: stack_set: set[_NT] = set() # 方便快速检查路径中的节点 # 深度优先搜索的辅助函数 - def dfs(node: _NT): + def dfs(node: _NT) -> None: if node in stack_set: # 检测到环 cycle_start_index = stack.index(node) cycles.append(stack[cycle_start_index:]) @@ -596,45 +598,45 @@ def dfs(node: _NT): return cycles -def merge_overlap(groups: Iterable[Iterable[_NT]]) -> list[list[_NT]]: +def merge_overlap(groups: Iterable[Sequence[_NT]]) -> list[list[_NT]]: # 并查集数据结构 parent: dict[_NT, _NT] = dict() # 查找集合的根节点 - def find(x): + def find(x: _NT) -> _NT: if parent[x] != x: parent[x] = find(parent[x]) + return parent[x] # 合并两个集合 - def union(x, y): - rootX = find(x) - rootY = find(y) - if rootX != rootY: - parent[rootY] = rootX + def union(x, y) -> None: + rootx = find(x) + rooty = find(y) + if rootx != rooty: + parent[rooty] = rootx # 初始化并查集 for group in groups: - for element in group: - if element not in parent: - parent[element] = element + for elem in group: + if elem not in parent: + parent[elem] = elem # 合并所有相互重叠的环 for group in groups: - first_element = group[0] - for element in group[1:]: - union(first_element, element) + first_elem = group[0] + for elem in group[1:]: + union(first_elem, elem) # 根据并查集结果,将所有节点归类到同一个集合中 - merged_groups: dict[_NT, list[_NT]] = dict() - for element in parent: - root = find(element) - if root not in merged_groups: - merged_groups[root] = [] - merged_groups[root].append(element) + mgrps: dict[_NT, list[_NT]] = dict() + for elem in parent: + root = find(elem) + if root not in mgrps: + mgrps[root] = [] + mgrps[root].append(elem) - # 将结果转换为列表列表形式 - return list(merged_groups.values()) + return list(mgrps.values()) def toposort(directed_edges: Mapping[_NT, Iterable[_NT]]) -> list[_NT]: @@ -691,7 +693,7 @@ def toposort(directed_edges: Mapping[_NT, Iterable[_NT]]) -> list[_NT]: vertices.add(m) if any(incoming_edges.get(v, None) for v in directed_edges): - raise NotSupportedError("the graph with cycles is not supported.") + raise GraphNotSupportedError("the graph with cycles is not supported.") return ordered diff --git a/paibox/backend/mapper.py b/paibox/backend/mapper.py index b91e2814..e0bfb715 100644 --- a/paibox/backend/mapper.py +++ b/paibox/backend/mapper.py @@ -45,10 +45,11 @@ class Mapper: - graph = PAIGraph() + graph: PAIGraph graph_info: GraphInfo def __init__(self) -> None: + self.graph = PAIGraph() self.core_blocks: list[CoreBlock] = [] """List for core blocks in the network.""" self.succ_core_blocks: dict[CoreBlock, list[CoreBlock]] = defaultdict(list) @@ -381,7 +382,7 @@ def config_export(self) -> GraphInfo: ]: raise ConfigInvalidError( f"the output chip address {ochip_coord} should not overlap with the " - f"chip addresses, but got {_BACKEND_CONTEXT._target_chip_addr_repr()}." + f"target chip addresses, but got {_BACKEND_CONTEXT._target_chip_addr_repr()}." ) input_nodes_info = self._inpproj_config_export() @@ -704,27 +705,27 @@ def _find_dest_cb_by_nseg( return dest_cb_of_nseg -def cycle_merge(merged_sgrps: list[MergedSuccGroup]): - succ_merged_sgrps: dict[MergedSuccGroup, list[MergedSuccGroup]] = dict() +def cycle_merge(merged_sgrps: list[MergedSuccGroup]) -> list[MergedSuccGroup]: + succ_merged_sgrps: dict[MergedSuccGroup, list[MergedSuccGroup]] = defaultdict(list) + for msgrp in merged_sgrps: - succ_merged_sgrps[msgrp] = [] - nodes = set(msgrp.nodes) for _msgrp in merged_sgrps: if msgrp == _msgrp: continue - if not nodes.isdisjoint(_msgrp.input_nodes): + if not msgrp.nodes.isdisjoint(_msgrp.input_nodes): succ_merged_sgrps[msgrp].append(_msgrp) cycles: list[list[MergedSuccGroup]] = find_cycles(succ_merged_sgrps) merged_cycles: list[list[MergedSuccGroup]] = merge_overlap(cycles) processed_merged_cycles: list[MergedSuccGroup] = list() - remaining_merged_sgrps: set[MergedSuccGroup] = set(merged_sgrps) - for merged_cycle in merged_cycles: - processed_merged_cycles.append(MergedSuccGroup.merge(merged_cycle)) - for msgrp in merged_cycle: - remaining_merged_sgrps.remove(msgrp) - processed_merged_cycles.extend(remaining_merged_sgrps) + remaining_msgrps: set[MergedSuccGroup] = set(merged_sgrps) + for mc in merged_cycles: + processed_merged_cycles.append(MergedSuccGroup.merge(mc)) + for msgrp in mc: + remaining_msgrps.remove(msgrp) + + processed_merged_cycles.extend(remaining_msgrps) return processed_merged_cycles diff --git a/paibox/backend/placement.py b/paibox/backend/placement.py index e77e0035..3fc370b9 100644 --- a/paibox/backend/placement.py +++ b/paibox/backend/placement.py @@ -1,6 +1,6 @@ import math import warnings -from typing import ClassVar, Literal, Optional, overload +from typing import ClassVar, Literal, Optional, cast, overload import numpy as np from paicorelib import LCN_EX, ChipCoord, Coord, CoreMode, HwConfig, MaxPoolingEnable @@ -177,7 +177,9 @@ def shape(self) -> tuple[int, int]: @property def source(self) -> list[SourceNodeType]: """Ordered unique source nodes.""" - return list(set([parent.source for parent in self.obj])) + return cast( + list[SourceNodeType], list(set([parent.source for parent in self.obj])) + ) @property def axons(self) -> list[SourceNodeType]: @@ -186,7 +188,7 @@ def axons(self) -> list[SourceNodeType]: @property def dest(self) -> list[DestNodeType]: """Ordered unique destination nodes.""" - return list(set([parent.dest for parent in self.obj])) + return cast(list[DestNodeType], list(set([parent.dest for parent in self.obj]))) def n_axon_of(self, index: int) -> int: """Get the #N of axons of `index`-th source neuron.""" diff --git a/paibox/backend/routing.py b/paibox/backend/routing.py index 9154d185..2e854d6d 100644 --- a/paibox/backend/routing.py +++ b/paibox/backend/routing.py @@ -11,19 +11,14 @@ from paicorelib import RoutingLevel as Level from paicorelib.routing_defs import MAX_ROUTING_PATH_LENGTH -from paibox.exceptions import ( - GraphBuildError, - PAIBoxDeprecationWarning, - ResourceError, - RoutingError, -) +from paibox.exceptions import PAIBoxDeprecationWarning, ResourceError, RoutingError from .conf_types import CorePlmConfInChip from .placement import CoreBlock, EmptyCorePlacement from .types import * if sys.version_info >= (3, 13): - from typing import deprecated + from warnings import deprecated else: from typing_extensions import deprecated diff --git a/paibox/backend/types.py b/paibox/backend/types.py index 778e3853..b8f34f3b 100644 --- a/paibox/backend/types.py +++ b/paibox/backend/types.py @@ -30,7 +30,6 @@ "NodeDegree", "NodeAttr", "EdgeAttr", - "PartitionedEdges", "NeuSlice", "NeuSegment", "NeuSegOfCorePlm", @@ -88,23 +87,19 @@ def copy(self) -> "NodeDegree": return self.__deepcopy__() -class NodeAttr(NamedTuple): +@dataclass +class NodeAttr: node: NodeType position: NodePosition degree: NodeDegree -class EdgeAttr(NamedTuple): +@dataclass +class EdgeAttr: # TODO FIXME distance? edge: EdgeType distance: int -class PartitionedEdges(NamedTuple): - edges: set[EdgeType] - rg_id: int - rt_mode: CoreMode = CoreMode.MODE_SNN # XXX Temp solution - - NeuSlice: TypeAlias = slice diff --git a/paibox/exceptions.py b/paibox/exceptions.py index bcc4d32a..e5204ad0 100644 --- a/paibox/exceptions.py +++ b/paibox/exceptions.py @@ -57,7 +57,13 @@ class GraphConnectionError(GraphBuildError): class NotSupportedError(PAIBoxError, NotImplementedError): - """Exception for a certain function not supported.""" + """Exception for unsupported functions.""" + + pass + + +class GraphNotSupportedError(GraphBuildError, NotSupportedError): + """Eception for unsupported structures of graph.""" pass diff --git a/paibox/utils.py b/paibox/utils.py index 6c43864b..906933cf 100644 --- a/paibox/utils.py +++ b/paibox/utils.py @@ -155,33 +155,33 @@ def reverse_16bit(x: int) -> int: return ((x >> 8) | (x << 8)) & 0xFFFF +def _get_desc(desc: Optional[str] = None) -> str: + return "value" if desc is None else desc + + def arg_check_pos(arg: int, desc: Optional[str] = None) -> int: - _desc = "value" if desc is None else f"{desc}" if arg < 1: - raise ValueError(f"{_desc} must be positive, but got {arg}.") + raise ValueError(f"{_get_desc(desc)} must be positive, but got {arg}.") return arg def arg_check_non_pos(arg: int, desc: Optional[str] = None) -> int: - _desc = "value" if desc is None else f"{desc}" if arg > 0: - raise ValueError(f"{_desc} must be non-positive, but got {arg}.") + raise ValueError(f"{_get_desc(desc)} must be non-positive, but got {arg}.") return arg def arg_check_neg(arg: int, desc: Optional[str] = None) -> int: - _desc = "value" if desc is None else f"{desc}" if arg > -1: - raise ValueError(f"{_desc} must be negative, but got {arg}.") + raise ValueError(f"{_get_desc(desc)} must be negative, but got {arg}.") return arg def arg_check_non_neg(arg: int, desc: Optional[str] = None) -> int: - _desc = "value" if desc is None else f"{desc}" if arg < 0: - raise ValueError(f"{_desc} must be non-negative, but got {arg}.") + raise ValueError(f"{_get_desc(desc)} must be non-negative, but got {arg}.") return arg From 72507079ee700921534682ff0a684b321a1bf15d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 08:19:13 +0000 Subject: [PATCH 158/187] :rotating_light: auto fix by pre-commit hooks --- paibox/base.py | 2 +- paibox/components/modules.py | 2 +- paibox/components/neuron/base.py | 2 +- paibox/components/neuron/utils.py | 2 +- tests/test_base.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/paibox/base.py b/paibox/base.py index c47260ff..fbfe3959 100644 --- a/paibox/base.py +++ b/paibox/base.py @@ -1,5 +1,5 @@ -from dataclasses import dataclass import sys +from dataclasses import dataclass from typing import Any, ClassVar, Literal, Optional import numpy as np diff --git a/paibox/components/modules.py b/paibox/components/modules.py index 2c13edef..703c1a00 100644 --- a/paibox/components/modules.py +++ b/paibox/components/modules.py @@ -14,7 +14,7 @@ from paibox.types import NEUOUT_U8_DTYPE, NeuOutType, VoltageType from paibox.utils import check_elem_unique, shape2num -from .neuron.utils import _input_width_format, RTModeKwds, _spike_width_format +from .neuron.utils import RTModeKwds, _input_width_format, _spike_width_format from .projection import InputProj if sys.version_info >= (3, 10): diff --git a/paibox/components/neuron/base.py b/paibox/components/neuron/base.py index bef9976b..635ac3f6 100644 --- a/paibox/components/neuron/base.py +++ b/paibox/components/neuron/base.py @@ -41,10 +41,10 @@ from .utils import ( BIT_TRUNCATE_MAX, NEG_THRES_MIN, + RTModeKwds, _input_width_format, _leak_v_check, _mask, - RTModeKwds, _spike_width_format, vjt_overflow, ) diff --git a/paibox/components/neuron/utils.py b/paibox/components/neuron/utils.py index dd58006f..349ea050 100644 --- a/paibox/components/neuron/utils.py +++ b/paibox/components/neuron/utils.py @@ -4,9 +4,9 @@ import numpy as np from paicorelib import ( InputWidthFormat, + MaxPoolingEnable, SNNModeEnable, SpikeWidthFormat, - MaxPoolingEnable, ) from paicorelib.framelib.utils import _mask from paicorelib.ram_model import ( diff --git a/tests/test_base.py b/tests/test_base.py index cd55346d..efe9a2ef 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,7 +1,7 @@ import pytest import paibox as pb -from paibox.base import PAIBoxObject, DataFlowFormat +from paibox.base import DataFlowFormat, PAIBoxObject from paibox.exceptions import RegisterError From efc09c2e66ea72c41d9fb5c4c18fa1ffc444dbcc Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 2 Dec 2024 14:16:04 +0800 Subject: [PATCH 159/187] =?UTF-8?q?=E2=9C=A8=20feat(base):=20use=20`DataSt?= =?UTF-8?q?reamFormat`=20to=20descriibe=20the=20format=20of=20dataflow.=20?= =?UTF-8?q?Update=20the=20update=20logic=20of=20dataflow=20format=20betwee?= =?UTF-8?q?n=20semi-folded=20ops.=20Labeling=20the=20dataflow=20format=20o?= =?UTF-8?q?n=20neurons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/base.py | 51 ++++++++++++ paibox/components/_modules.py | 36 ++------ paibox/components/functional.py | 122 ++++++++++++++++------------ paibox/components/neuron/base.py | 46 ++++++++++- paibox/network.py | 14 ++-- tests/components/test_functional.py | 13 +-- 6 files changed, 187 insertions(+), 95 deletions(-) diff --git a/paibox/base.py b/paibox/base.py index 31e25387..89ffdbd0 100644 --- a/paibox/base.py +++ b/paibox/base.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass import sys from typing import Any, ClassVar, Literal, Optional @@ -256,6 +257,45 @@ def state(self) -> NodeDict: return self._memories +INFINITE_DATAFLOW = 0 # the dataflow is infinite. + + +@dataclass +class DataFlowFormat: + """Describe in detail the format of valid data in the dataflow.""" + + t_1st_vld: int + """The time of the first valid data, relative to `t_1st_vld` of the external input.""" + interval: int = 1 + """The interval of valid data in the flow.""" + n_vld: int = INFINITE_DATAFLOW + """The number of valid data. 0 for infinite dataflow.""" + + def __post_init__(self) -> None: + if self.n_vld < INFINITE_DATAFLOW: + raise ValueError( + f"'n_vld' should be greater than or equal to {INFINITE_DATAFLOW}, " + f"but got {self.n_vld}." + ) + + def t_at_idx(self, idx: int) -> int: + """The time of the valid data at the given index.""" + if self.n_vld > INFINITE_DATAFLOW: + assert 0 <= idx <= self.n_vld - 1 + + return self.t_1st_vld + idx * self.interval + + def t_at_n(self, n: int) -> int: + """The time of the n-th valid data.""" + return self.t_at_idx(n - 1) + + @property + def t_last_vld(self) -> int: + """The time of the last valid data.""" + assert self.n_vld > INFINITE_DATAFLOW + return self.t_at_n(self.n_vld) + + class NeuDyn(DynamicSys, ReceiveInputProj, TimeRelatedNode): _delay: int @@ -266,6 +306,9 @@ class NeuDyn(DynamicSys, ReceiveInputProj, TimeRelatedNode): _uf: int """unrolling_factor""" + oflow_format: DataFlowFormat + """The format of output data stream""" + def __init__(self, name: Optional[str] = None) -> None: super().__init__(name) self.master_nodes = NodeDict() @@ -291,6 +334,14 @@ def tick_wait_end(self) -> int: def unrolling_factor(self) -> int: return self._uf + @property + def end_tick(self) -> int: + """End time of work.""" + if self.tick_wait_end == 0: + return 9999 # Never end + + return self.tick_wait_start + self.tick_wait_end - 1 + @unrolling_factor.setter def unrolling_factor(self, factor: int) -> None: self._uf = arg_check_pos(factor, "'unrolling_factor'") diff --git a/paibox/components/_modules.py b/paibox/components/_modules.py index 30ca7397..a2a0953d 100644 --- a/paibox/components/_modules.py +++ b/paibox/components/_modules.py @@ -1,12 +1,10 @@ -import math import typing -from dataclasses import dataclass from typing import Literal, Optional, Union import numpy as np from paicorelib import TM, HwConfig -from paibox.base import NeuDyn, NodeList +from paibox.base import DataFlowFormat, NeuDyn, NodeList from paibox.exceptions import ResourceError, ShapeError from paibox.types import ( LEAK_V_DTYPE, @@ -58,7 +56,7 @@ "_SpikingPool2dWithV", "_SemiFoldedModule", "_LinearBase", - "SemiFoldedStreamAttr", + "SemiFoldedDataFlowFormat", ] @@ -161,41 +159,21 @@ class _DelayChainANN(_DelayChainBase): pass -@dataclass(frozen=True) -class SemiFoldedStreamAttr: - """Details of transmission of valid data in semi-folded form data stream.""" - - t_1st_vld: int - """The time of the first valid data, relative to `t_1st_vld` of the external input.""" - interval: int - """The interval of the output data stream.""" - n_data: int = 0 - """The number of valid output data.""" - - def t_at(self, n: int) -> int: - """The time of the n-th valid data.""" - if self.n_data > 0: - assert 1 <= n <= self.n_data - - return self.t_1st_vld + (n - 1) * self.interval - - @property - def t_last_vld(self) -> int: - """The time of the last valid data.""" - assert self.n_data > 0 - return self.t_at(self.n_data) +class SemiFoldedDataFlowFormat(DataFlowFormat): + pass @set_rt_mode_ann() class _SemiFoldedModule(FunctionalModule): """Functional modules with interfaces in semi-folded form. Use `build()` of class `HasSemiFoldedIntf`.""" - ostream_attr: SemiFoldedStreamAttr + inherent_delay = 1 + oflow_format: SemiFoldedDataFlowFormat def build( self, network: "DynSysGroup", - incoming_stream_attr: SemiFoldedStreamAttr, + incoming_flow_format: SemiFoldedDataFlowFormat, **build_options, ) -> BuiltComponentType: raise NotImplementedError diff --git a/paibox/components/functional.py b/paibox/components/functional.py index 37aa6fc9..e3701a7f 100644 --- a/paibox/components/functional.py +++ b/paibox/components/functional.py @@ -905,22 +905,23 @@ class LinearSemiFolded(_LinearBase, _SemiFoldedModule): def build( self, network: "DynSysGroup", - incoming_stream_attr: SemiFoldedStreamAttr, + incoming_flow_format: SemiFoldedDataFlowFormat, **build_options, ) -> BuiltComponentType: assert len(self.source[0].shape_out) == 2 - self.ostream_attr = incoming_stream_attr - twe = 1 + self.ostream_attr.t_last_vld + # For semi-folded linear, the valid output is at only one timestep. + self.oflow_format = SemiFoldedDataFlowFormat( + incoming_flow_format.t_last_vld, 1, 1 + ) + twe = 1 + self.oflow_format.t_last_vld ich, ih = self.source[0].shape_out - if build_options.get("check_before_compile"): - self._input_buffer_len_check(ich, ih, ih, incoming_stream_attr.interval) n_delays = NodeList() s_delays = NodeList() s_weight = NodeList() - n_fc = ANNNeuron( + n_linear = ANNNeuron( self.shape_out, self.bias, self.bit_trunc, @@ -930,13 +931,16 @@ def build( keep_shape=self.keep_shape, name=f"nd_{self.name}", ) + n_linear.set_oflow_format( + interval=self.oflow_format.interval, n_vld=self.oflow_format.n_vld + ) for i in range(ih): neuron = ANNBypassNeuron( shape=(ich, ih), - delay=incoming_stream_attr.interval * i + 1, + delay=incoming_flow_format.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe - incoming_stream_attr.interval * i, + tick_wait_end=twe - incoming_flow_format.interval * i, keep_shape=self.keep_shape, name=f"n{i}_{self.name}", ) @@ -954,15 +958,15 @@ def build( w = self.weights[ih - i - 1 :: ih, :] syn2 = FullConnSyn( neuron, - n_fc, + n_linear, weights=w, conn_type=ConnType.All2All, name=f"s{i}_{self.name}", ) s_weight.append(syn2) - generated = [n_fc, *n_delays, *s_delays, *s_weight] - self._rebuild_out_intf(network, n_fc, *generated, **build_options) + generated = [n_linear, *n_delays, *s_delays, *s_weight] + self._rebuild_out_intf(network, n_linear, *generated, **build_options) return generated @@ -1008,9 +1012,11 @@ def __init__( # XXX Do not consider the case when the shape of source neurons needs to be changed, for now. # neuron_s.shape_change((in_ch, in_h)) - cout, cin, kh, _ = kernel.shape + cout, cin, kh, kw = kernel.shape out_h = (in_h - kh + 2 * self.padding[0]) // self.stride[0] + 1 + assert self.padding[0] < kh and self.padding[1] < kw + if in_ch != cin: raise ShapeError(f"the channels mismatch: {in_ch} != {cin}.") @@ -1034,7 +1040,7 @@ def __init__( def build( self, network: "DynSysGroup", - incoming_stream_attr: SemiFoldedStreamAttr, + incoming_flow_format: SemiFoldedDataFlowFormat, **build_options, ) -> BuiltComponentType: assert len(self.source[0].shape_out) == 2 @@ -1047,14 +1053,15 @@ def build( _, cin, _, kw = self.kernel.shape _, ow = self.shape_out - self.ostream_attr = SemiFoldedStreamAttr( - incoming_stream_attr.t_at(kw - self.padding[0]), - incoming_stream_attr.interval * self.stride[1], + self.oflow_format = SemiFoldedDataFlowFormat( + incoming_flow_format.t_at_n(kw - self.padding[0]), + incoming_flow_format.interval * self.stride[1], ow, ) - twe = 1 + self.ostream_attr.t_last_vld + twe = 1 + self.oflow_format.t_last_vld + if build_options.get("check_before_compile"): - self._input_buffer_len_check(cin, ih, kw, incoming_stream_attr.interval) + self._input_buffer_len_check(cin, ih, kw, incoming_flow_format.interval) n_delays = NodeList() n_neg_padding = NodeList() @@ -1072,12 +1079,16 @@ def build( keep_shape=self.keep_shape, name=f"nd_{self.name}", ) + n_conv2d.set_oflow_format( + interval=self.oflow_format.interval, n_vld=self.oflow_format.n_vld + ) + for i in range(kw): neuron = ANNBypassNeuron( (cin, ih), - delay=incoming_stream_attr.interval * i + 1, + delay=incoming_flow_format.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe - incoming_stream_attr.interval * i, + tick_wait_end=twe - incoming_flow_format.interval * i, name=f"n{i}_delay_{self.name}", ) n_delays.append(neuron) @@ -1105,13 +1116,13 @@ def build( # Add additional negative padding layer to eliminate the incorrect output # NOTE: `t_1st_vld` = 0 & `padding[0]` > 0 means the previous layer is # an input node. No need to add negative padding layer for this case. - if incoming_stream_attr.t_1st_vld > 0: + if incoming_flow_format.t_1st_vld > 0: for p in range(self.padding[0]): neuron = ANNBypassNeuron( (cin, ih), - delay=1 + incoming_stream_attr.interval * (kw - 1 - p), + delay=1 + incoming_flow_format.interval * (kw - 1 - p), tick_wait_start=self.tick_wait_start, - tick_wait_end=incoming_stream_attr.t_1st_vld, + tick_wait_end=incoming_flow_format.t_1st_vld, keep_shape=self.keep_shape, name=f"n{p}_pad_{self.name}", ) @@ -1196,7 +1207,7 @@ def __init__( def build( self, network: "DynSysGroup", - incoming_stream_attr: SemiFoldedStreamAttr, + incoming_flow_format: SemiFoldedDataFlowFormat, **build_options, ) -> BuiltComponentType: assert len(self.source[0].shape_out) == 2 @@ -1209,20 +1220,20 @@ def build( kh, kw = self.kernel_size _, ow = self.shape_out - self.ostream_attr = SemiFoldedStreamAttr( - incoming_stream_attr.t_at(kw), - incoming_stream_attr.interval * self.stride[1], + self.oflow_format = SemiFoldedDataFlowFormat( + incoming_flow_format.t_at_n(kw), + incoming_flow_format.interval * self.stride[1], ow, ) - twe = 1 + self.ostream_attr.t_last_vld + twe = 1 + self.oflow_format.t_last_vld if build_options.get("check_before_compile"): - self._input_buffer_len_check(cin, ih, kw, incoming_stream_attr.interval) + self._input_buffer_len_check(cin, ih, kw, incoming_flow_format.interval) n_delays = NodeList() s_delays = NodeList() - pool2d = ANNNeuron( + n_pool2d = ANNNeuron( self.shape_out, delay=self.delay_relative, tick_wait_start=self.tick_wait_start + 1, @@ -1231,13 +1242,16 @@ def build( keep_shape=self.keep_shape, name=f"nd_{self.name}", ) + n_pool2d.set_oflow_format( + interval=self.oflow_format.interval, n_vld=self.oflow_format.n_vld + ) for i in range(kw): neuron = ANNBypassNeuron( (cin, ih), - delay=incoming_stream_attr.interval * i + 1, + delay=incoming_flow_format.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe - incoming_stream_attr.interval * i, + tick_wait_end=twe - incoming_flow_format.interval * i, keep_shape=self.keep_shape, name=f"n{i}_{self.name}", ) @@ -1253,7 +1267,7 @@ def build( s_delays.append(syn1) syn2 = MaxPoolSyn( neuron, - pool2d, + n_pool2d, weights=_poo2d_semifolded_mapping_mask( cin, ih, ow, kh, self.stride, (0, 0) ), @@ -1261,8 +1275,8 @@ def build( ) s_delays.append(syn2) - generated = [pool2d, *n_delays, *s_delays] - self._rebuild_out_intf(network, pool2d, *generated, **build_options) + generated = [n_pool2d, *n_delays, *s_delays] + self._rebuild_out_intf(network, n_pool2d, *generated, **build_options) return generated @@ -1302,6 +1316,8 @@ def __init__( assert len(neuron_s.shape_out) == 2 in_ch, in_h = neuron_s.shape_out out_h = (in_h - self.kernel_size[0] + 2 * self.padding[0]) // self.stride[0] + 1 + kh, kw = self.kernel_size + assert self.padding[0] < kh and self.padding[1] < kw super().__init__( neuron_s, @@ -1314,7 +1330,7 @@ def __init__( def build( self, network: "DynSysGroup", - incoming_stream_attr: SemiFoldedStreamAttr, + incoming_flow_format: SemiFoldedDataFlowFormat, **build_options, ) -> BuiltComponentType: assert len(self.source[0].shape_out) == 2 @@ -1327,15 +1343,15 @@ def build( kh, kw = self.kernel_size _, ow = self.shape_out - self.ostream_attr = SemiFoldedStreamAttr( - incoming_stream_attr.t_at(kw - self.padding[0]), - incoming_stream_attr.interval * self.stride[1], + self.oflow_format = SemiFoldedDataFlowFormat( + incoming_flow_format.t_at_n(kw - self.padding[0]), + incoming_flow_format.interval * self.stride[1], ow, ) - twe = 1 + self.ostream_attr.t_last_vld + twe = 1 + self.oflow_format.t_last_vld if build_options.get("check_before_compile"): - self._input_buffer_len_check(cin, ih, kw, incoming_stream_attr.interval) + self._input_buffer_len_check(cin, ih, kw, incoming_flow_format.interval) # NOTE: Division is achieved with the help of output truncation. # TODO Since division with a divisor that is an integer power of 2 can only be implemented by @@ -1355,7 +1371,7 @@ def build( s_delays = NodeList() s_neg_padding = NodeList() - pool2d = ANNNeuron( + n_pool2d = ANNNeuron( self.shape_out, delay=self.delay_relative, bit_trunc=bit_trunc, @@ -1364,12 +1380,16 @@ def build( keep_shape=self.keep_shape, name=f"nd_{self.name}", ) + n_pool2d.set_oflow_format( + interval=self.oflow_format.interval, n_vld=self.oflow_format.n_vld + ) + for i in range(kw): neuron = ANNBypassNeuron( (cin, ih), - delay=incoming_stream_attr.interval * i + 1, + delay=incoming_flow_format.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe - incoming_stream_attr.interval * i, + tick_wait_end=twe - incoming_flow_format.interval * i, keep_shape=self.keep_shape, name=f"n{i}_{self.name}", ) @@ -1385,7 +1405,7 @@ def build( s_delays.append(syn1) syn2 = FullConnSyn( neuron, - pool2d, + n_pool2d, weights=_poo2d_semifolded_mapping_mask( cin, ih, ow, kh, self.stride, self.padding ), @@ -1395,13 +1415,13 @@ def build( s_delays.append(syn2) # Add additional negative padding layer to eliminate the incorrect output - if incoming_stream_attr.t_1st_vld > 0: + if incoming_flow_format.t_1st_vld > 0: for p in range(self.padding[0]): neuron = ANNBypassNeuron( (cin, ih), - delay=1 + incoming_stream_attr.interval * (kw - 1 - p), + delay=1 + incoming_flow_format.interval * (kw - 1 - p), tick_wait_start=self.tick_wait_start, - tick_wait_end=incoming_stream_attr.t_1st_vld, + tick_wait_end=incoming_flow_format.t_1st_vld, keep_shape=self.keep_shape, name=f"n{p}_pad_{self.name}", ) @@ -1418,7 +1438,7 @@ def build( syn2 = FullConnSyn( neuron, - pool2d, + n_pool2d, weights=-_poo2d_semifolded_mapping_mask( cin, ih, ow, kh, self.stride, self.padding ), @@ -1427,8 +1447,8 @@ def build( ) s_neg_padding.append(syn2) - generated = [pool2d, *n_delays, *n_neg_padding, *s_delays, *s_neg_padding] - self._rebuild_out_intf(network, pool2d, *generated, **build_options) + generated = [n_pool2d, *n_delays, *n_neg_padding, *s_delays, *s_neg_padding] + self._rebuild_out_intf(network, n_pool2d, *generated, **build_options) return generated diff --git a/paibox/components/neuron/base.py b/paibox/components/neuron/base.py index a35fa8b5..7e226a41 100644 --- a/paibox/components/neuron/base.py +++ b/paibox/components/neuron/base.py @@ -20,7 +20,7 @@ get_core_mode, ) -from paibox.base import NeuDyn +from paibox.base import DataFlowFormat, NeuDyn, INFINITE_DATAFLOW from paibox.exceptions import NotSupportedError, PAIBoxWarning, ShapeError from paibox.types import ( NEUOUT_U8_DTYPE, @@ -487,11 +487,13 @@ def __init__( ), ) - """Auxiliary internal stateful attributes for debugging""" + """Non-stateful attributes.""" self._delay = arg_check_pos(delay, "'delay'") self._tws = arg_check_non_neg(tick_wait_start, "'tick_wait_start'") self._twe = arg_check_non_neg(tick_wait_end, "'tick_wait_end'") self._uf = arg_check_pos(unrolling_factor, "'unrolling_factor'") + # Default dataflow is infinite and continuous, starting at `tws`. + self.oflow_format = DataFlowFormat(self.tick_wait_start) def __len__(self) -> int: return self._n_neuron @@ -529,6 +531,46 @@ def update( def reset_state(self, *args, **kwargs) -> None: self.reset_memory() # Call reset of `StatusMemory`. + def set_oflow_format( + self, + t_1st_vld: Optional[int] = None, + interval: Optional[int] = None, + n_vld: Optional[int] = None + ) -> None: + assert hasattr(self, "oflow_format") + _t_1st_vld = ( + t_1st_vld if isinstance(t_1st_vld, int) else self.oflow_format.t_1st_vld + ) + + _interval = ( + arg_check_pos(interval, "interval") + if isinstance(interval, int) + else self.oflow_format.interval + ) + + _n_vld = ( + arg_check_non_neg(n_vld, "n_vld") + if isinstance(n_vld, int) + else self.oflow_format.n_vld + ) + + if _t_1st_vld < self.tick_wait_start: + raise ValueError( + f"the output time of the first valid data should be greater than or equal to " + f"{self.tick_wait_start}, but got {_t_1st_vld}." + ) + + if _n_vld > INFINITE_DATAFLOW: + if (t_last_vld := _t_1st_vld + (_n_vld - 1) * _interval) > self.end_tick: + raise ValueError( + f"valid data is output after the end time. The neuron stops working at " + f"{self.end_tick}, but still needs to output at {t_last_vld}." + ) + + self.oflow_format.t_1st_vld = _t_1st_vld + self.oflow_format.interval = _interval + self.oflow_format.n_vld = _n_vld + def __copy__(self) -> "Neuron": """Same as `__deepcopy__`.""" return self.__deepcopy__() diff --git a/paibox/network.py b/paibox/network.py index 4f0a4c12..6db83afd 100644 --- a/paibox/network.py +++ b/paibox/network.py @@ -7,7 +7,7 @@ from .base import DynamicSys, SynSys from .collector import Collector from .components import NeuModule, Neuron, Projection -from .components._modules import SemiFoldedStreamAttr, _SemiFoldedModule +from .components._modules import SemiFoldedDataFlowFormat, _SemiFoldedModule from .components.modules import BuiltComponentType from .exceptions import NotSupportedError from .mixin import Container @@ -102,18 +102,18 @@ def build_modules( generated = dict() - # For external input stream info: - # 1. The start time is 1 - # 2. The interval is 1 - # 3. The #N of data is -1 since it dosen't effect the subsequent output stream. + # For external input dataflow: + # 1. The start time is 0. + # 2. The interval is 1. + # 3. The #N of data is `INFINITE_DATA_STREAM` since it dosen't effect the subsequent output dataflow. # TODO Reserve an interface for setting the properties of external input from `FRONTEND_ENV`? - last_vld_output_attr = SemiFoldedStreamAttr(0, 1) + last_vld_output_attr = SemiFoldedDataFlowFormat(t_1st_vld=0) for m in modules: # TODO for the case of the ResBlock, the `pred_dg_semi_ops` will be used. if isinstance(m, _SemiFoldedModule): generated[m] = m.build(self, last_vld_output_attr, **build_options) - last_vld_output_attr = m.ostream_attr + last_vld_output_attr = m.oflow_format else: generated[m] = m.build(self, **build_options) diff --git a/tests/components/test_functional.py b/tests/components/test_functional.py index c2c63a82..56c80471 100644 --- a/tests/components/test_functional.py +++ b/tests/components/test_functional.py @@ -4,6 +4,7 @@ import paibox as pb from paibox.base import DynamicSys from paibox.components import NeuModule +from paibox.components._modules import _SemiFoldedModule from paibox.components.neuron.base import MetaNeuron from paibox.components.synapses.conv_utils import _conv2d_faster, _pair, _single from paibox.network import DynSysGroup @@ -976,12 +977,12 @@ def test_Conv2dSemiFolded_FC_ChainNet( probe_linear = pb.Probe(generated[linear][0], "output") sim1.add_probe(probe_linear) - semi_folded_modules = [*conv2d_list, linear] + semi_folded_modules: list[_SemiFoldedModule] = [*conv2d_list, linear] # The interval & the time o the first valid data of the external input data stream semi_vld_out_intv0 = 1 t_1st_vld_data0 = 0 # The interval & the time of the first valid data of the current layers - semi_vld_out_intv = [m.ostream_attr.interval for m in semi_folded_modules] + semi_vld_out_intv = [m.oflow_format.interval for m in semi_folded_modules] t_1st_vld_data = [0] * n_conv for i in range(n_conv): if i == 0: @@ -1049,7 +1050,7 @@ def test_Conv2dSemiFolded_FC_ChainNet( assert np.array_equal( expected_fc_t, sim1.data[probe_linear][ - linear.tick_wait_start + linear.ostream_attr.t_last_vld + linear.tick_wait_start + linear.oflow_format.t_last_vld ], ) @@ -1171,12 +1172,12 @@ def test_Pool2dSemiFolded_FC_ChainNet( probe_linear = pb.Probe(generated[linear][0], "output") sim1.add_probe(probe_linear) - semi_folded_modules = [*pool2d_list, linear] + semi_folded_modules: list[_SemiFoldedModule] = [*pool2d_list, linear] # The interval & the time o the first valid data of the external input data stream semi_vld_out_intv0 = 1 t_1st_vld_data0 = 0 # The interval & the time of the first valid data of the current layers - semi_vld_out_intv = [m.ostream_attr.interval for m in semi_folded_modules] + semi_vld_out_intv = [m.oflow_format.interval for m in semi_folded_modules] t_1st_vld_data = [0] * n_pool for i in range(n_pool): if i == 0: @@ -1231,7 +1232,7 @@ def test_Pool2dSemiFolded_FC_ChainNet( assert np.array_equal( expected_fc_t, sim1.data[probe_linear][ - linear.tick_wait_start + linear.ostream_attr.t_last_vld + linear.tick_wait_start + linear.oflow_format.t_last_vld ], ) From 865c8be6bf3d43a1323b5051eff02dabc365e454 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 2 Dec 2024 14:19:58 +0800 Subject: [PATCH 160/187] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20support=20py3.13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/codecov.yml | 2 +- pyproject.toml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 05f9ce7b..35d36c57 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -22,7 +22,7 @@ jobs: pytest: strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} diff --git a/pyproject.toml b/pyproject.toml index 71124dab..08223a17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ homepage = "https://github.com/PAICookers/PAIBox" documentation = "https://github.com/PAICookers/PAIBox#readme" keywords = ["PAICORE 2.0", "PAIBox", "SNN", "Toolchain"] classifiers = [ + "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Operating System :: OS Independent", @@ -24,9 +25,9 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Artificial Intelligence", - "Topic :: Software Development :: Build Tools", - "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Compilers", ] packages = [{ include = "paibox" }] From 5c1ef57826ff6ac41de9545b7146e7b7f9c60cd2 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 2 Dec 2024 14:22:29 +0800 Subject: [PATCH 161/187] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20removed=20an=20?= =?UTF-8?q?always=20failed=20pre-commit=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 490b6d15..b5b4b101 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,6 @@ repos: - id: check-symlinks - id: check-merge-conflict - id: mixed-line-ending - - id: name-tests-test args: [--pytest-test-first] - id: requirements-txt-fixer - id: pretty-format-json From 468efa687780f23c0f0dfec6f710163d6a7fc7fe Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 2 Dec 2024 15:42:47 +0800 Subject: [PATCH 162/187] =?UTF-8?q?=E2=9C=A8=20feat(base):=20update=20arg?= =?UTF-8?q?=20check=20for=20`DataFlowFormat`=20&=20function=20`set=5Foflow?= =?UTF-8?q?=5Fformat`=20in=20neuron.=20Add=20test=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/base.py | 54 ++++++++++++++++++++----- paibox/components/neuron/base.py | 69 +++++++++++++++++--------------- tests/test_base.py | 19 ++++++++- 3 files changed, 97 insertions(+), 45 deletions(-) diff --git a/paibox/base.py b/paibox/base.py index 89ffdbd0..c47260ff 100644 --- a/paibox/base.py +++ b/paibox/base.py @@ -257,26 +257,23 @@ def state(self) -> NodeDict: return self._memories -INFINITE_DATAFLOW = 0 # the dataflow is infinite. +INFINITE_DATAFLOW = 0 @dataclass class DataFlowFormat: """Describe in detail the format of valid data in the dataflow.""" - t_1st_vld: int - """The time of the first valid data, relative to `t_1st_vld` of the external input.""" + t_1st_vld: int = 0 + """Global time or a relative time of the first valid data in the dataflow, determined by `is_local_time`.""" interval: int = 1 - """The interval of valid data in the flow.""" + """The interval of valid data in the dataflow.""" n_vld: int = INFINITE_DATAFLOW - """The number of valid data. 0 for infinite dataflow.""" + """The number of valid data. <0 for infinite dataflow.""" - def __post_init__(self) -> None: - if self.n_vld < INFINITE_DATAFLOW: - raise ValueError( - f"'n_vld' should be greater than or equal to {INFINITE_DATAFLOW}, " - f"but got {self.n_vld}." - ) + is_local_time: bool = True + """Whether the `t_1st_vld` is relative to the local time(tws+T) of the neuron, or \ + relative to the global time of the external input.""" def t_at_idx(self, idx: int) -> int: """The time of the valid data at the given index.""" @@ -295,6 +292,41 @@ def t_last_vld(self) -> int: assert self.n_vld > INFINITE_DATAFLOW return self.t_at_n(self.n_vld) + def get_global_t_1st_vld(self, tws: int) -> int: + """Get the global time of the first valid data.""" + return tws + self.t_1st_vld if self.is_local_time else self.t_1st_vld + + def _check_after_assign(self, tws: int, end_tick: int) -> None: + _t_1st_vld_out_of_range_text = ( + "the {0} output time of the first valid data should be in the working " + + "time from {1} to {2}, but got {3}." + ) + + # The global time of the first valid data is in [tws, end_tick]. + gb_t_1st_vld = self.get_global_t_1st_vld(tws) + if gb_t_1st_vld < tws or gb_t_1st_vld > end_tick: + if self.is_local_time: + raise ValueError( + _t_1st_vld_out_of_range_text.format( + "local", "+0", f"+{end_tick - tws + 1}", self.t_1st_vld + ) + ) + else: + raise ValueError( + _t_1st_vld_out_of_range_text.format( + "global", tws, end_tick, self.t_1st_vld + ) + ) + + if self.n_vld > INFINITE_DATAFLOW: + if ( + t_last_vld := gb_t_1st_vld + (self.n_vld - 1) * self.interval + ) > end_tick: + raise ValueError( + f"valid data is output after the end time. The neuron stops working at " + f"{end_tick}, but still needs to output at {t_last_vld}." + ) + class NeuDyn(DynamicSys, ReceiveInputProj, TimeRelatedNode): diff --git a/paibox/components/neuron/base.py b/paibox/components/neuron/base.py index 7e226a41..14def12f 100644 --- a/paibox/components/neuron/base.py +++ b/paibox/components/neuron/base.py @@ -20,7 +20,7 @@ get_core_mode, ) -from paibox.base import DataFlowFormat, NeuDyn, INFINITE_DATAFLOW +from paibox.base import DataFlowFormat, NeuDyn from paibox.exceptions import NotSupportedError, PAIBoxWarning, ShapeError from paibox.types import ( NEUOUT_U8_DTYPE, @@ -492,8 +492,8 @@ def __init__( self._tws = arg_check_non_neg(tick_wait_start, "'tick_wait_start'") self._twe = arg_check_non_neg(tick_wait_end, "'tick_wait_end'") self._uf = arg_check_pos(unrolling_factor, "'unrolling_factor'") - # Default dataflow is infinite and continuous, starting at `tws`. - self.oflow_format = DataFlowFormat(self.tick_wait_start) + # Default dataflow is infinite and continuous, starting at tws+0. + self.oflow_format = DataFlowFormat(0, is_local_time=True) def __len__(self) -> int: return self._n_neuron @@ -535,41 +535,44 @@ def set_oflow_format( self, t_1st_vld: Optional[int] = None, interval: Optional[int] = None, - n_vld: Optional[int] = None + n_vld: Optional[int] = None, + *, + format_type: type[DataFlowFormat] = DataFlowFormat, ) -> None: - assert hasattr(self, "oflow_format") - _t_1st_vld = ( - t_1st_vld if isinstance(t_1st_vld, int) else self.oflow_format.t_1st_vld - ) - - _interval = ( - arg_check_pos(interval, "interval") - if isinstance(interval, int) - else self.oflow_format.interval - ) - - _n_vld = ( - arg_check_non_neg(n_vld, "n_vld") - if isinstance(n_vld, int) - else self.oflow_format.n_vld - ) - - if _t_1st_vld < self.tick_wait_start: - raise ValueError( - f"the output time of the first valid data should be greater than or equal to " - f"{self.tick_wait_start}, but got {_t_1st_vld}." + """Set the attributes of output dataflow format by given arguments.""" + if hasattr(self, "oflow_format"): + _t_1st_vld = ( + t_1st_vld if isinstance(t_1st_vld, int) else self.oflow_format.t_1st_vld ) - - if _n_vld > INFINITE_DATAFLOW: - if (t_last_vld := _t_1st_vld + (_n_vld - 1) * _interval) > self.end_tick: + _interval = ( + arg_check_pos(interval, "interval") + if isinstance(interval, int) + else self.oflow_format.interval + ) + _n_vld = ( + arg_check_non_neg(n_vld, "n_vld") + if isinstance(n_vld, int) + else self.oflow_format.n_vld + ) + self._assign_flow_format(_t_1st_vld, _interval, _n_vld) + else: + if not ( + isinstance(interval, int) + and isinstance(n_vld, int) + and isinstance(t_1st_vld, int) + ): raise ValueError( - f"valid data is output after the end time. The neuron stops working at " - f"{self.end_tick}, but still needs to output at {t_last_vld}." + "if 'oflow_format' is not set, 't_1st_vld', 'interval' & 'n_vld' must be set." ) - self.oflow_format.t_1st_vld = _t_1st_vld - self.oflow_format.interval = _interval - self.oflow_format.n_vld = _n_vld + self.oflow_format = format_type(t_1st_vld, interval, n_vld) + self.oflow_format._check_after_assign(self.tick_wait_start, self.end_tick) + + def _assign_flow_format(self, t_1st_vld: int, intv: int, n_vld: int) -> None: + self.oflow_format.t_1st_vld = t_1st_vld + self.oflow_format.interval = intv + self.oflow_format.n_vld = n_vld + self.oflow_format._check_after_assign(self.tick_wait_start, self.end_tick) def __copy__(self) -> "Neuron": """Same as `__deepcopy__`.""" diff --git a/tests/test_base.py b/tests/test_base.py index 5adb2c7f..cd55346d 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,7 +1,7 @@ import pytest import paibox as pb -from paibox.base import PAIBoxObject +from paibox.base import PAIBoxObject, DataFlowFormat from paibox.exceptions import RegisterError @@ -41,3 +41,20 @@ def test_paiboxobject_nodes(): nodes4 = obj1.nodes(method="absolute", level=-1, include_self=True) assert nodes4["obj111"] == obj1 + + +class TestDataFlowFormat: + def test_dff_infinite_dataflow(self): + with pytest.raises((AssertionError, ValueError)): + dff = DataFlowFormat(1, 0, -1) + _ = dff.t_last_vld + + def test_dff_valid(self): + # 1. t1 >= tws, t_last > endtick + dff1 = DataFlowFormat(10, 3, 10, is_local_time=False) + with pytest.raises(ValueError): + dff1._check_after_assign(8, 36) + + # 2. t1 >= tws, t_last <= endtick + dff2 = DataFlowFormat(10, 3, 10, is_local_time=True) + dff2._check_after_assign(2, 39) From 9eb94048849bba423a554bd7df93341e54cfe946 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 2 Dec 2024 15:48:51 +0800 Subject: [PATCH 163/187] =?UTF-8?q?=E2=9C=A8=20feat(graphs):=20corrected?= =?UTF-8?q?=20the=20calculation=20method=20of=20attribute=20`inherent=5Fti?= =?UTF-8?q?mestep`.=20Strictly=20annotate=20the=20data=20flow=20format=20f?= =?UTF-8?q?or=20the=20computational=20neuron=20of=20the=20semi-folded=20op?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/graphs.py | 20 +++++++++++++------- paibox/components/functional.py | 16 ++++++++++++---- tests/components/test_functional.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/paibox/backend/graphs.py b/paibox/backend/graphs.py index c3e2751d..0f1f68ad 100644 --- a/paibox/backend/graphs.py +++ b/paibox/backend/graphs.py @@ -127,7 +127,7 @@ def _pre_build(self, **build_options) -> None: # checks. These additional checks may be removed as more network structures will be supported. # Currently, `LinearSemiFolded` is at the end of the network, since it will change the form of - # the input data stream, and its effective output is at the same time. + # the input dataflow, and its effective output is at the same time. semi_linears = modules.subset(LinearSemiFolded) if not all( len(succ_dg_semi_ops[linear]) == 0 for linear in semi_linears @@ -172,9 +172,14 @@ def _update_graph(self, **build_options) -> None: self.inodes = self._raw_nodes.subset(InputProj) # By default, nodes with out-degree = 0 are considered as output nodes. - self.onodes = self._raw_nodes.key_on_condition( - lambda node: self.degree_of_nodes[node].out_degree == 0 - ) # type: ignore + # TODO A node with out-degree can also be an output node. However, no network for now has this topology. + self.onodes = Collector( + { + k: cast(DestNodeType, v) + for k, v in self._raw_nodes.items() + if self.degree_of_nodes[k].out_degree == 0 + } + ).not_subset(InputProj) for name, node in self._raw_nodes.items(): self.nodes[name] = NodeAttr( @@ -525,9 +530,10 @@ def _find_rg_by_cb( @property def inherent_timestep(self) -> int: self.build_check() - _, distance = get_longest_path(self.succ_dg, self.ordered_nodes) - - return distance + return max( + n.oflow_format.get_global_t_1st_vld(n.tick_wait_start) + for n in self.onodes.values() + ) @property def graph_name_repr(self) -> str: diff --git a/paibox/components/functional.py b/paibox/components/functional.py index e3701a7f..be7897c9 100644 --- a/paibox/components/functional.py +++ b/paibox/components/functional.py @@ -932,7 +932,9 @@ def build( name=f"nd_{self.name}", ) n_linear.set_oflow_format( - interval=self.oflow_format.interval, n_vld=self.oflow_format.n_vld + self.oflow_format.t_1st_vld, + self.oflow_format.interval, + self.oflow_format.n_vld, ) for i in range(ih): @@ -1080,7 +1082,9 @@ def build( name=f"nd_{self.name}", ) n_conv2d.set_oflow_format( - interval=self.oflow_format.interval, n_vld=self.oflow_format.n_vld + self.oflow_format.t_1st_vld, + self.oflow_format.interval, + self.oflow_format.n_vld, ) for i in range(kw): @@ -1243,7 +1247,9 @@ def build( name=f"nd_{self.name}", ) n_pool2d.set_oflow_format( - interval=self.oflow_format.interval, n_vld=self.oflow_format.n_vld + self.oflow_format.t_1st_vld, + self.oflow_format.interval, + self.oflow_format.n_vld, ) for i in range(kw): @@ -1381,7 +1387,9 @@ def build( name=f"nd_{self.name}", ) n_pool2d.set_oflow_format( - interval=self.oflow_format.interval, n_vld=self.oflow_format.n_vld + self.oflow_format.t_1st_vld, + self.oflow_format.interval, + self.oflow_format.n_vld, ) for i in range(kw): diff --git a/tests/components/test_functional.py b/tests/components/test_functional.py index 56c80471..4aae8961 100644 --- a/tests/components/test_functional.py +++ b/tests/components/test_functional.py @@ -1043,6 +1043,16 @@ def test_Conv2dSemiFolded_FC_ChainNet( ], ) + assert conv2d_list[i_conv].tick_wait_start + t_1st_vld_data[ + i_conv + ] + i * semi_vld_out_intv[i_conv] - 1 == conv2d_list[ + i_conv + ].tick_wait_start + conv2d_list[ + i_conv + ].oflow_format.t_at_idx( + i + ) + # x is the reference result of the last convolution. expected_fc_t = _ann_bit_trunc(x.ravel() @ fc_weight.astype(VOLTAGE_DTYPE)) @@ -1053,6 +1063,10 @@ def test_Conv2dSemiFolded_FC_ChainNet( linear.tick_wait_start + linear.oflow_format.t_last_vld ], ) + assert ( + linear.oflow_format.get_global_t_1st_vld(linear.tick_wait_start) + == linear.tick_wait_start + linear.oflow_format.t_last_vld + ) @pytest.mark.parametrize( "ishape_chw, n_pool, kshape_hw, stride, padding, out_features, pool_type", @@ -1225,6 +1239,16 @@ def test_Pool2dSemiFolded_FC_ChainNet( ], ) + assert pool2d_list[i_pool].tick_wait_start + t_1st_vld_data[ + i_pool + ] + i * semi_vld_out_intv[i_pool] - 1 == pool2d_list[ + i_pool + ].tick_wait_start + pool2d_list[ + i_pool + ].oflow_format.t_at_idx( + i + ) + # x is the reference result of the last pooling. expected_fc_t = _ann_bit_trunc(x.ravel() @ fc_weight.astype(VOLTAGE_DTYPE)) @@ -1236,6 +1260,11 @@ def test_Pool2dSemiFolded_FC_ChainNet( ], ) + assert ( + linear.oflow_format.get_global_t_1st_vld(linear.tick_wait_start) + == linear.tick_wait_start + linear.oflow_format.t_last_vld + ) + @pytest.mark.parametrize( "shape, weight", [ From f7733804e4386a02bb98cf7ce8f68ae8f5777b2b Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 2 Dec 2024 15:50:20 +0800 Subject: [PATCH 164/187] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20typing(neuron):?= =?UTF-8?q?=20add=20typed=20dict=20`ExtraNeuAttrKwds`=20to=20check=20extra?= =?UTF-8?q?=20keywords=20passing=20to=20neurons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/components/modules.py | 4 ++-- paibox/components/neuron/base.py | 10 +++++----- paibox/components/neuron/neurons.py | 25 +++++++++++++++---------- paibox/components/neuron/utils.py | 24 ++++++++++++++++++++++-- 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/paibox/components/modules.py b/paibox/components/modules.py index 41e06fdc..2c13edef 100644 --- a/paibox/components/modules.py +++ b/paibox/components/modules.py @@ -14,7 +14,7 @@ from paibox.types import NEUOUT_U8_DTYPE, NeuOutType, VoltageType from paibox.utils import check_elem_unique, shape2num -from .neuron.utils import _input_width_format, _RTModeKwds, _spike_width_format +from .neuron.utils import _input_width_format, RTModeKwds, _spike_width_format from .projection import InputProj if sys.version_info >= (3, 10): @@ -91,7 +91,7 @@ class NeuModule(NeuDyn, BuildingModule): """#N of outputs.""" inherent_delay: int = 0 """Internal delay of the module, relative to the external.""" - rt_mode_kwds: _RTModeKwds + rt_mode_kwds: RTModeKwds mode: CoreMode def __init__( diff --git a/paibox/components/neuron/base.py b/paibox/components/neuron/base.py index 14def12f..bef9976b 100644 --- a/paibox/components/neuron/base.py +++ b/paibox/components/neuron/base.py @@ -21,7 +21,7 @@ ) from paibox.base import DataFlowFormat, NeuDyn -from paibox.exceptions import NotSupportedError, PAIBoxWarning, ShapeError +from paibox.exceptions import ConfigInvalidError, PAIBoxWarning, ShapeError from paibox.types import ( NEUOUT_U8_DTYPE, VOLTAGE_DTYPE, @@ -44,7 +44,7 @@ _input_width_format, _leak_v_check, _mask, - _RTModeKwds, + RTModeKwds, _spike_width_format, vjt_overflow, ) @@ -57,7 +57,7 @@ class MetaNeuron: """Meta neuron""" - rt_mode_kwds: _RTModeKwds + rt_mode_kwds: RTModeKwds mode: CoreMode def __init__( @@ -96,8 +96,8 @@ def __init__( # check whether the mode is valid self.mode = get_core_mode(input_width, spike_width, snn_en) - if pool_max == True and self.mode != CoreMode.MODE_ANN: - raise NotSupportedError( + if pool_max and self.mode != CoreMode.MODE_ANN: + raise ConfigInvalidError( f"max pooling is only supported in {CoreMode.MODE_ANN.name}, " f"but got {self.mode.name}." ) diff --git a/paibox/components/neuron/neurons.py b/paibox/components/neuron/neurons.py index 85e3df47..3f7f0279 100644 --- a/paibox/components/neuron/neurons.py +++ b/paibox/components/neuron/neurons.py @@ -8,10 +8,15 @@ from paibox.types import LEAK_V_DTYPE, DataType, Shape from .base import Neuron -from .utils import LEAK_V_MAX +from .utils import LEAK_V_MAX, ExtraNeuAttrKwds + +if sys.version_info >= (3, 12): + from typing import Unpack +else: + from typing_extensions import Unpack if sys.version_info >= (3, 13): - from typing import deprecated + from warnings import deprecated else: from typing_extensions import deprecated @@ -37,7 +42,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs, + **kwargs: Unpack[ExtraNeuAttrKwds], ) -> None: """IF neuron. @@ -93,7 +98,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs, + **kwargs: Unpack[ExtraNeuAttrKwds], ) -> None: """LIF neuron. @@ -152,7 +157,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs, + **kwargs: Unpack[ExtraNeuAttrKwds], ) -> None: """Tonic spiking neuron. @@ -178,7 +183,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs, + **kwargs: Unpack[ExtraNeuAttrKwds], ) -> None: """Phasic spiking neuron. Once the neuron receives `N` spikes and fires, it will reset to \ the negative floor and never fires again. `N` is `fire_step`. @@ -213,7 +218,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs, + **kwargs: Unpack[ExtraNeuAttrKwds], ) -> None: """A neuron that always outputs 1 as long as it starts working. @@ -245,7 +250,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs, + **kwargs: Unpack[ExtraNeuAttrKwds], ) -> None: """Bypass neuron. Output is equal to input. @@ -279,7 +284,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs, + **kwargs: Unpack[ExtraNeuAttrKwds], ) -> None: """General neuron used in ANN mode. Positive threshold = 1, negative threshold = 0.""" kwargs["bit_truncation"] = bit_trunc @@ -299,7 +304,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs, + **kwargs: Unpack[ExtraNeuAttrKwds], ) -> None: super().__init__( shape, bias=0, bit_trunc=8, keep_shape=keep_shape, name=name, **kwargs diff --git a/paibox/components/neuron/utils.py b/paibox/components/neuron/utils.py index 1d9ea2ff..dd58006f 100644 --- a/paibox/components/neuron/utils.py +++ b/paibox/components/neuron/utils.py @@ -2,7 +2,12 @@ from typing import Literal, TypedDict, Union import numpy as np -from paicorelib import InputWidthFormat, SNNModeEnable, SpikeWidthFormat +from paicorelib import ( + InputWidthFormat, + SNNModeEnable, + SpikeWidthFormat, + MaxPoolingEnable, +) from paicorelib.framelib.utils import _mask from paicorelib.ram_model import ( BIT_TRUNCATE_MAX, @@ -107,9 +112,24 @@ def _get_neu_out_dtype( return NEUOUT_U8_DTYPE -class _RTModeKwds(TypedDict): +class RTModeKwds(TypedDict): """A typed keywords for runtime mode. Only for checking if necessary.""" input_width: InputWidthFormat spike_width: SpikeWidthFormat snn_en: SNNModeEnable + + +class ExtraNeuAttrKwds(TypedDict, total=False): + """A typed keywords for extra neuron attributes.""" + + bit_truncation: int # For ANNNeuron + delay: int + tick_wait_start: int + tick_wait_end: int + input_width: Union[L[1, 8], InputWidthFormat] + spike_width: Union[L[1, 8], SpikeWidthFormat] + snn_en: Union[bool, SNNModeEnable] + pool_max: Union[bool, MaxPoolingEnable] + unrolling_factor: int + overflow_strict: bool From 3ddc9fb32162c2869f51b0455802a9ea0ce27a91 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 2 Dec 2024 16:05:46 +0800 Subject: [PATCH 165/187] =?UTF-8?q?=F0=9F=90=9B=20bugfix(compile):=20if=20?= =?UTF-8?q?compiling=20with=20`core=5Festimate=5Fonly`=20turned=20on,=20pr?= =?UTF-8?q?event=20exporting=20the=20compiled=20results?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/mapper.py | 19 ++++++++++++++++--- paibox/exceptions.py | 6 ++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/paibox/backend/mapper.py b/paibox/backend/mapper.py index 6012d7fc..b91e2814 100644 --- a/paibox/backend/mapper.py +++ b/paibox/backend/mapper.py @@ -8,7 +8,7 @@ from paibox.base import SynSys from paibox.components import Neuron -from paibox.exceptions import ConfigInvalidError, ResourceError +from paibox.exceptions import CompileError, ConfigInvalidError, ResourceError from paibox.network import DynSysGroup from .conf_exporting import * @@ -71,6 +71,9 @@ def __init__(self) -> None: chip_list=_BACKEND_CONTEXT["target_chip_addr"] ) + self._core_estimate_only = False + """Wether this compilation is for core estimation only. If so, no core will be assigned.""" + self.clear() def clear(self) -> None: @@ -90,6 +93,8 @@ def clear(self) -> None: self.n_core_required = 0 self.n_core_occupied = 0 + self._core_estimate_only = False + # Set default cflags _BACKEND_CONTEXT.cflags.clear() set_cflag(enable_wp_opt=True) @@ -169,6 +174,8 @@ def compile( set_cflag(multicast_optim=True) set_cflag(multicast_optim_nodes=_mul_optim_nodes) + self._core_estimate_only = core_estimate_only + """Preperation. 1. Check whether the PAIGraph has built. 2. Set global compilation flags. @@ -192,9 +199,9 @@ def compile( self.cb_axon_grouping() """Core coordinate assignment.""" - self.coord_assign(core_estimate_only) + self.coord_assign(self._core_estimate_only) - if core_estimate_only: + if self._core_estimate_only: return GraphInfo( name=self.graph.graph_name_repr, input={}, @@ -619,6 +626,12 @@ def export( Return: total configurations in dictionary format. """ + if self._core_estimate_only: + raise CompileError( + "the current compilation is only for core estimation. " + "Please disable 'core_estimate_only' and compile again before exporting." + ) + if format not in ("bin", "npy", "txt"): raise ValueError(f"format {format} is not supported.") diff --git a/paibox/exceptions.py b/paibox/exceptions.py index 55514a43..bcc4d32a 100644 --- a/paibox/exceptions.py +++ b/paibox/exceptions.py @@ -74,6 +74,12 @@ class FunctionalError(PAIBoxError, RuntimeError): pass +class CompileError(PAIBoxError, RuntimeError): + """Exception for compilation.""" + + pass + + class RoutingError(PAIBoxError): """Exception for routing tree.""" From 51e7aa06fa3fccb5bc624581f37eceef36f8b8dc Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 2 Dec 2024 16:16:14 +0800 Subject: [PATCH 166/187] =?UTF-8?q?=F0=9F=9A=B8=20typing:=20update=20typin?= =?UTF-8?q?g=20&=20error=20handling.=20Removed=20useless=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/graphs.py | 66 +++++++++++++++++++------------------ paibox/backend/mapper.py | 27 +++++++-------- paibox/backend/placement.py | 8 +++-- paibox/backend/routing.py | 9 ++--- paibox/backend/types.py | 13 +++----- paibox/exceptions.py | 8 ++++- paibox/utils.py | 16 ++++----- 7 files changed, 74 insertions(+), 73 deletions(-) diff --git a/paibox/backend/graphs.py b/paibox/backend/graphs.py index 0f1f68ad..69a117e7 100644 --- a/paibox/backend/graphs.py +++ b/paibox/backend/graphs.py @@ -2,14 +2,18 @@ from collections import defaultdict from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass, field -from typing import Any, TypeVar, Union +from typing import Any, TypeVar, Union, cast from paicorelib import HwConfig from paibox.collector import Collector from paibox.components import FullConnectedSyn, InputProj, NeuModule, Neuron from paibox.components.functional import LinearSemiFolded -from paibox.exceptions import GraphBuildError, GraphConnectionError, NotSupportedError +from paibox.exceptions import ( + GraphBuildError, + GraphConnectionError, + GraphNotSupportedError, +) from paibox.network import DynSysGroup from paibox.utils import check_elem_unique @@ -132,7 +136,7 @@ def _pre_build(self, **build_options) -> None: if not all( len(succ_dg_semi_ops[linear]) == 0 for linear in semi_linears ): - raise NotSupportedError( + raise GraphNotSupportedError( "currently, the semi-folded linear can only be used as output of the network." ) @@ -183,9 +187,7 @@ def _update_graph(self, **build_options) -> None: for name, node in self._raw_nodes.items(): self.nodes[name] = NodeAttr( - node=node, - position=self._node_pos(name), - degree=self.degree_of_nodes[name], + node, self._node_pos(name), self.degree_of_nodes[name] ) self.ordered_nodes = toposort(self.succ_dg) @@ -220,7 +222,7 @@ def topo_support_check(self) -> None: onode.num_out > HwConfig.N_FANIN_PER_DENDRITE_MAX for onode in self.onodes.values() ): - raise NotSupportedError( + raise GraphNotSupportedError( f"only output nodes with no more than {HwConfig.N_FANIN_PER_DENDRITE_MAX} " f"neurons are supported." ) @@ -541,7 +543,7 @@ def graph_name_repr(self) -> str: return _prefix + "_and_".join(network.name for network in self._raw_networks) -_NT = TypeVar("_NT", CoreBlock, NodeName, RoutingGroup) +_NT = TypeVar("_NT", CoreBlock, NodeName, RoutingGroup, MergedSuccGroup) _T = TypeVar("_T") @@ -557,7 +559,7 @@ def _degree_check( if isinstance(succ_node, CoreBlock) else str(succ_node) ) - raise NotSupportedError( + raise GraphNotSupportedError( f"If out-degree of a node is greater than 1, the in-degree of its sucessors must be 1. " f"However, in-degree of {_node_repr} is {degree_of_nodes[succ_node].in_degree}." ) @@ -570,7 +572,7 @@ def find_cycles(directed_edges: Mapping[_NT, Iterable[_NT]]) -> list[list[_NT]]: stack_set: set[_NT] = set() # 方便快速检查路径中的节点 # 深度优先搜索的辅助函数 - def dfs(node: _NT): + def dfs(node: _NT) -> None: if node in stack_set: # 检测到环 cycle_start_index = stack.index(node) cycles.append(stack[cycle_start_index:]) @@ -596,45 +598,45 @@ def dfs(node: _NT): return cycles -def merge_overlap(groups: Iterable[Iterable[_NT]]) -> list[list[_NT]]: +def merge_overlap(groups: Iterable[Sequence[_NT]]) -> list[list[_NT]]: # 并查集数据结构 parent: dict[_NT, _NT] = dict() # 查找集合的根节点 - def find(x): + def find(x: _NT) -> _NT: if parent[x] != x: parent[x] = find(parent[x]) + return parent[x] # 合并两个集合 - def union(x, y): - rootX = find(x) - rootY = find(y) - if rootX != rootY: - parent[rootY] = rootX + def union(x, y) -> None: + rootx = find(x) + rooty = find(y) + if rootx != rooty: + parent[rooty] = rootx # 初始化并查集 for group in groups: - for element in group: - if element not in parent: - parent[element] = element + for elem in group: + if elem not in parent: + parent[elem] = elem # 合并所有相互重叠的环 for group in groups: - first_element = group[0] - for element in group[1:]: - union(first_element, element) + first_elem = group[0] + for elem in group[1:]: + union(first_elem, elem) # 根据并查集结果,将所有节点归类到同一个集合中 - merged_groups: dict[_NT, list[_NT]] = dict() - for element in parent: - root = find(element) - if root not in merged_groups: - merged_groups[root] = [] - merged_groups[root].append(element) + mgrps: dict[_NT, list[_NT]] = dict() + for elem in parent: + root = find(elem) + if root not in mgrps: + mgrps[root] = [] + mgrps[root].append(elem) - # 将结果转换为列表列表形式 - return list(merged_groups.values()) + return list(mgrps.values()) def toposort(directed_edges: Mapping[_NT, Iterable[_NT]]) -> list[_NT]: @@ -691,7 +693,7 @@ def toposort(directed_edges: Mapping[_NT, Iterable[_NT]]) -> list[_NT]: vertices.add(m) if any(incoming_edges.get(v, None) for v in directed_edges): - raise NotSupportedError("the graph with cycles is not supported.") + raise GraphNotSupportedError("the graph with cycles is not supported.") return ordered diff --git a/paibox/backend/mapper.py b/paibox/backend/mapper.py index b91e2814..e0bfb715 100644 --- a/paibox/backend/mapper.py +++ b/paibox/backend/mapper.py @@ -45,10 +45,11 @@ class Mapper: - graph = PAIGraph() + graph: PAIGraph graph_info: GraphInfo def __init__(self) -> None: + self.graph = PAIGraph() self.core_blocks: list[CoreBlock] = [] """List for core blocks in the network.""" self.succ_core_blocks: dict[CoreBlock, list[CoreBlock]] = defaultdict(list) @@ -381,7 +382,7 @@ def config_export(self) -> GraphInfo: ]: raise ConfigInvalidError( f"the output chip address {ochip_coord} should not overlap with the " - f"chip addresses, but got {_BACKEND_CONTEXT._target_chip_addr_repr()}." + f"target chip addresses, but got {_BACKEND_CONTEXT._target_chip_addr_repr()}." ) input_nodes_info = self._inpproj_config_export() @@ -704,27 +705,27 @@ def _find_dest_cb_by_nseg( return dest_cb_of_nseg -def cycle_merge(merged_sgrps: list[MergedSuccGroup]): - succ_merged_sgrps: dict[MergedSuccGroup, list[MergedSuccGroup]] = dict() +def cycle_merge(merged_sgrps: list[MergedSuccGroup]) -> list[MergedSuccGroup]: + succ_merged_sgrps: dict[MergedSuccGroup, list[MergedSuccGroup]] = defaultdict(list) + for msgrp in merged_sgrps: - succ_merged_sgrps[msgrp] = [] - nodes = set(msgrp.nodes) for _msgrp in merged_sgrps: if msgrp == _msgrp: continue - if not nodes.isdisjoint(_msgrp.input_nodes): + if not msgrp.nodes.isdisjoint(_msgrp.input_nodes): succ_merged_sgrps[msgrp].append(_msgrp) cycles: list[list[MergedSuccGroup]] = find_cycles(succ_merged_sgrps) merged_cycles: list[list[MergedSuccGroup]] = merge_overlap(cycles) processed_merged_cycles: list[MergedSuccGroup] = list() - remaining_merged_sgrps: set[MergedSuccGroup] = set(merged_sgrps) - for merged_cycle in merged_cycles: - processed_merged_cycles.append(MergedSuccGroup.merge(merged_cycle)) - for msgrp in merged_cycle: - remaining_merged_sgrps.remove(msgrp) - processed_merged_cycles.extend(remaining_merged_sgrps) + remaining_msgrps: set[MergedSuccGroup] = set(merged_sgrps) + for mc in merged_cycles: + processed_merged_cycles.append(MergedSuccGroup.merge(mc)) + for msgrp in mc: + remaining_msgrps.remove(msgrp) + + processed_merged_cycles.extend(remaining_msgrps) return processed_merged_cycles diff --git a/paibox/backend/placement.py b/paibox/backend/placement.py index e77e0035..3fc370b9 100644 --- a/paibox/backend/placement.py +++ b/paibox/backend/placement.py @@ -1,6 +1,6 @@ import math import warnings -from typing import ClassVar, Literal, Optional, overload +from typing import ClassVar, Literal, Optional, cast, overload import numpy as np from paicorelib import LCN_EX, ChipCoord, Coord, CoreMode, HwConfig, MaxPoolingEnable @@ -177,7 +177,9 @@ def shape(self) -> tuple[int, int]: @property def source(self) -> list[SourceNodeType]: """Ordered unique source nodes.""" - return list(set([parent.source for parent in self.obj])) + return cast( + list[SourceNodeType], list(set([parent.source for parent in self.obj])) + ) @property def axons(self) -> list[SourceNodeType]: @@ -186,7 +188,7 @@ def axons(self) -> list[SourceNodeType]: @property def dest(self) -> list[DestNodeType]: """Ordered unique destination nodes.""" - return list(set([parent.dest for parent in self.obj])) + return cast(list[DestNodeType], list(set([parent.dest for parent in self.obj]))) def n_axon_of(self, index: int) -> int: """Get the #N of axons of `index`-th source neuron.""" diff --git a/paibox/backend/routing.py b/paibox/backend/routing.py index 9154d185..2e854d6d 100644 --- a/paibox/backend/routing.py +++ b/paibox/backend/routing.py @@ -11,19 +11,14 @@ from paicorelib import RoutingLevel as Level from paicorelib.routing_defs import MAX_ROUTING_PATH_LENGTH -from paibox.exceptions import ( - GraphBuildError, - PAIBoxDeprecationWarning, - ResourceError, - RoutingError, -) +from paibox.exceptions import PAIBoxDeprecationWarning, ResourceError, RoutingError from .conf_types import CorePlmConfInChip from .placement import CoreBlock, EmptyCorePlacement from .types import * if sys.version_info >= (3, 13): - from typing import deprecated + from warnings import deprecated else: from typing_extensions import deprecated diff --git a/paibox/backend/types.py b/paibox/backend/types.py index 778e3853..b8f34f3b 100644 --- a/paibox/backend/types.py +++ b/paibox/backend/types.py @@ -30,7 +30,6 @@ "NodeDegree", "NodeAttr", "EdgeAttr", - "PartitionedEdges", "NeuSlice", "NeuSegment", "NeuSegOfCorePlm", @@ -88,23 +87,19 @@ def copy(self) -> "NodeDegree": return self.__deepcopy__() -class NodeAttr(NamedTuple): +@dataclass +class NodeAttr: node: NodeType position: NodePosition degree: NodeDegree -class EdgeAttr(NamedTuple): +@dataclass +class EdgeAttr: # TODO FIXME distance? edge: EdgeType distance: int -class PartitionedEdges(NamedTuple): - edges: set[EdgeType] - rg_id: int - rt_mode: CoreMode = CoreMode.MODE_SNN # XXX Temp solution - - NeuSlice: TypeAlias = slice diff --git a/paibox/exceptions.py b/paibox/exceptions.py index bcc4d32a..e5204ad0 100644 --- a/paibox/exceptions.py +++ b/paibox/exceptions.py @@ -57,7 +57,13 @@ class GraphConnectionError(GraphBuildError): class NotSupportedError(PAIBoxError, NotImplementedError): - """Exception for a certain function not supported.""" + """Exception for unsupported functions.""" + + pass + + +class GraphNotSupportedError(GraphBuildError, NotSupportedError): + """Eception for unsupported structures of graph.""" pass diff --git a/paibox/utils.py b/paibox/utils.py index 6c43864b..906933cf 100644 --- a/paibox/utils.py +++ b/paibox/utils.py @@ -155,33 +155,33 @@ def reverse_16bit(x: int) -> int: return ((x >> 8) | (x << 8)) & 0xFFFF +def _get_desc(desc: Optional[str] = None) -> str: + return "value" if desc is None else desc + + def arg_check_pos(arg: int, desc: Optional[str] = None) -> int: - _desc = "value" if desc is None else f"{desc}" if arg < 1: - raise ValueError(f"{_desc} must be positive, but got {arg}.") + raise ValueError(f"{_get_desc(desc)} must be positive, but got {arg}.") return arg def arg_check_non_pos(arg: int, desc: Optional[str] = None) -> int: - _desc = "value" if desc is None else f"{desc}" if arg > 0: - raise ValueError(f"{_desc} must be non-positive, but got {arg}.") + raise ValueError(f"{_get_desc(desc)} must be non-positive, but got {arg}.") return arg def arg_check_neg(arg: int, desc: Optional[str] = None) -> int: - _desc = "value" if desc is None else f"{desc}" if arg > -1: - raise ValueError(f"{_desc} must be negative, but got {arg}.") + raise ValueError(f"{_get_desc(desc)} must be negative, but got {arg}.") return arg def arg_check_non_neg(arg: int, desc: Optional[str] = None) -> int: - _desc = "value" if desc is None else f"{desc}" if arg < 0: - raise ValueError(f"{_desc} must be non-negative, but got {arg}.") + raise ValueError(f"{_get_desc(desc)} must be non-negative, but got {arg}.") return arg From c550954a1106e33dba92719070e1862ac6708d00 Mon Sep 17 00:00:00 2001 From: KafCoppelia <69038090+KafCoppelia@users.noreply.github.com> Date: Mon, 2 Dec 2024 19:30:48 +0800 Subject: [PATCH 167/187] Revert "Feat(graphs): label the data flow format in each neuron of the network" --- .github/workflows/codecov.yml | 2 +- .pre-commit-config.yaml | 1 + paibox/backend/graphs.py | 86 +++++++++--------- paibox/backend/mapper.py | 46 ++++------ paibox/backend/placement.py | 8 +- paibox/backend/routing.py | 9 +- paibox/backend/types.py | 13 ++- paibox/base.py | 83 ------------------ paibox/components/_modules.py | 52 +++++++---- paibox/components/functional.py | 131 +++++++++++----------------- paibox/components/modules.py | 4 +- paibox/components/neuron/base.py | 59 ++----------- paibox/components/neuron/neurons.py | 25 +++--- paibox/components/neuron/utils.py | 24 +---- paibox/exceptions.py | 14 +-- paibox/network.py | 14 +-- paibox/utils.py | 16 ++-- pyproject.toml | 5 +- tests/components/test_functional.py | 42 ++------- tests/test_base.py | 19 +--- 20 files changed, 209 insertions(+), 444 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 35d36c57..05f9ce7b 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -22,7 +22,7 @@ jobs: pytest: strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12"] os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5b4b101..490b6d15 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,6 +42,7 @@ repos: - id: check-symlinks - id: check-merge-conflict - id: mixed-line-ending + - id: name-tests-test args: [--pytest-test-first] - id: requirements-txt-fixer - id: pretty-format-json diff --git a/paibox/backend/graphs.py b/paibox/backend/graphs.py index 69a117e7..c3e2751d 100644 --- a/paibox/backend/graphs.py +++ b/paibox/backend/graphs.py @@ -2,18 +2,14 @@ from collections import defaultdict from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass, field -from typing import Any, TypeVar, Union, cast +from typing import Any, TypeVar, Union from paicorelib import HwConfig from paibox.collector import Collector from paibox.components import FullConnectedSyn, InputProj, NeuModule, Neuron from paibox.components.functional import LinearSemiFolded -from paibox.exceptions import ( - GraphBuildError, - GraphConnectionError, - GraphNotSupportedError, -) +from paibox.exceptions import GraphBuildError, GraphConnectionError, NotSupportedError from paibox.network import DynSysGroup from paibox.utils import check_elem_unique @@ -131,12 +127,12 @@ def _pre_build(self, **build_options) -> None: # checks. These additional checks may be removed as more network structures will be supported. # Currently, `LinearSemiFolded` is at the end of the network, since it will change the form of - # the input dataflow, and its effective output is at the same time. + # the input data stream, and its effective output is at the same time. semi_linears = modules.subset(LinearSemiFolded) if not all( len(succ_dg_semi_ops[linear]) == 0 for linear in semi_linears ): - raise GraphNotSupportedError( + raise NotSupportedError( "currently, the semi-folded linear can only be used as output of the network." ) @@ -176,18 +172,15 @@ def _update_graph(self, **build_options) -> None: self.inodes = self._raw_nodes.subset(InputProj) # By default, nodes with out-degree = 0 are considered as output nodes. - # TODO A node with out-degree can also be an output node. However, no network for now has this topology. - self.onodes = Collector( - { - k: cast(DestNodeType, v) - for k, v in self._raw_nodes.items() - if self.degree_of_nodes[k].out_degree == 0 - } - ).not_subset(InputProj) + self.onodes = self._raw_nodes.key_on_condition( + lambda node: self.degree_of_nodes[node].out_degree == 0 + ) # type: ignore for name, node in self._raw_nodes.items(): self.nodes[name] = NodeAttr( - node, self._node_pos(name), self.degree_of_nodes[name] + node=node, + position=self._node_pos(name), + degree=self.degree_of_nodes[name], ) self.ordered_nodes = toposort(self.succ_dg) @@ -222,7 +215,7 @@ def topo_support_check(self) -> None: onode.num_out > HwConfig.N_FANIN_PER_DENDRITE_MAX for onode in self.onodes.values() ): - raise GraphNotSupportedError( + raise NotSupportedError( f"only output nodes with no more than {HwConfig.N_FANIN_PER_DENDRITE_MAX} " f"neurons are supported." ) @@ -532,10 +525,9 @@ def _find_rg_by_cb( @property def inherent_timestep(self) -> int: self.build_check() - return max( - n.oflow_format.get_global_t_1st_vld(n.tick_wait_start) - for n in self.onodes.values() - ) + _, distance = get_longest_path(self.succ_dg, self.ordered_nodes) + + return distance @property def graph_name_repr(self) -> str: @@ -543,7 +535,7 @@ def graph_name_repr(self) -> str: return _prefix + "_and_".join(network.name for network in self._raw_networks) -_NT = TypeVar("_NT", CoreBlock, NodeName, RoutingGroup, MergedSuccGroup) +_NT = TypeVar("_NT", CoreBlock, NodeName, RoutingGroup) _T = TypeVar("_T") @@ -559,7 +551,7 @@ def _degree_check( if isinstance(succ_node, CoreBlock) else str(succ_node) ) - raise GraphNotSupportedError( + raise NotSupportedError( f"If out-degree of a node is greater than 1, the in-degree of its sucessors must be 1. " f"However, in-degree of {_node_repr} is {degree_of_nodes[succ_node].in_degree}." ) @@ -572,7 +564,7 @@ def find_cycles(directed_edges: Mapping[_NT, Iterable[_NT]]) -> list[list[_NT]]: stack_set: set[_NT] = set() # 方便快速检查路径中的节点 # 深度优先搜索的辅助函数 - def dfs(node: _NT) -> None: + def dfs(node: _NT): if node in stack_set: # 检测到环 cycle_start_index = stack.index(node) cycles.append(stack[cycle_start_index:]) @@ -598,45 +590,45 @@ def dfs(node: _NT) -> None: return cycles -def merge_overlap(groups: Iterable[Sequence[_NT]]) -> list[list[_NT]]: +def merge_overlap(groups: Iterable[Iterable[_NT]]) -> list[list[_NT]]: # 并查集数据结构 parent: dict[_NT, _NT] = dict() # 查找集合的根节点 - def find(x: _NT) -> _NT: + def find(x): if parent[x] != x: parent[x] = find(parent[x]) - return parent[x] # 合并两个集合 - def union(x, y) -> None: - rootx = find(x) - rooty = find(y) - if rootx != rooty: - parent[rooty] = rootx + def union(x, y): + rootX = find(x) + rootY = find(y) + if rootX != rootY: + parent[rootY] = rootX # 初始化并查集 for group in groups: - for elem in group: - if elem not in parent: - parent[elem] = elem + for element in group: + if element not in parent: + parent[element] = element # 合并所有相互重叠的环 for group in groups: - first_elem = group[0] - for elem in group[1:]: - union(first_elem, elem) + first_element = group[0] + for element in group[1:]: + union(first_element, element) # 根据并查集结果,将所有节点归类到同一个集合中 - mgrps: dict[_NT, list[_NT]] = dict() - for elem in parent: - root = find(elem) - if root not in mgrps: - mgrps[root] = [] - mgrps[root].append(elem) + merged_groups: dict[_NT, list[_NT]] = dict() + for element in parent: + root = find(element) + if root not in merged_groups: + merged_groups[root] = [] + merged_groups[root].append(element) - return list(mgrps.values()) + # 将结果转换为列表列表形式 + return list(merged_groups.values()) def toposort(directed_edges: Mapping[_NT, Iterable[_NT]]) -> list[_NT]: @@ -693,7 +685,7 @@ def toposort(directed_edges: Mapping[_NT, Iterable[_NT]]) -> list[_NT]: vertices.add(m) if any(incoming_edges.get(v, None) for v in directed_edges): - raise GraphNotSupportedError("the graph with cycles is not supported.") + raise NotSupportedError("the graph with cycles is not supported.") return ordered diff --git a/paibox/backend/mapper.py b/paibox/backend/mapper.py index e0bfb715..6012d7fc 100644 --- a/paibox/backend/mapper.py +++ b/paibox/backend/mapper.py @@ -8,7 +8,7 @@ from paibox.base import SynSys from paibox.components import Neuron -from paibox.exceptions import CompileError, ConfigInvalidError, ResourceError +from paibox.exceptions import ConfigInvalidError, ResourceError from paibox.network import DynSysGroup from .conf_exporting import * @@ -45,11 +45,10 @@ class Mapper: - graph: PAIGraph + graph = PAIGraph() graph_info: GraphInfo def __init__(self) -> None: - self.graph = PAIGraph() self.core_blocks: list[CoreBlock] = [] """List for core blocks in the network.""" self.succ_core_blocks: dict[CoreBlock, list[CoreBlock]] = defaultdict(list) @@ -72,9 +71,6 @@ def __init__(self) -> None: chip_list=_BACKEND_CONTEXT["target_chip_addr"] ) - self._core_estimate_only = False - """Wether this compilation is for core estimation only. If so, no core will be assigned.""" - self.clear() def clear(self) -> None: @@ -94,8 +90,6 @@ def clear(self) -> None: self.n_core_required = 0 self.n_core_occupied = 0 - self._core_estimate_only = False - # Set default cflags _BACKEND_CONTEXT.cflags.clear() set_cflag(enable_wp_opt=True) @@ -175,8 +169,6 @@ def compile( set_cflag(multicast_optim=True) set_cflag(multicast_optim_nodes=_mul_optim_nodes) - self._core_estimate_only = core_estimate_only - """Preperation. 1. Check whether the PAIGraph has built. 2. Set global compilation flags. @@ -200,9 +192,9 @@ def compile( self.cb_axon_grouping() """Core coordinate assignment.""" - self.coord_assign(self._core_estimate_only) + self.coord_assign(core_estimate_only) - if self._core_estimate_only: + if core_estimate_only: return GraphInfo( name=self.graph.graph_name_repr, input={}, @@ -382,7 +374,7 @@ def config_export(self) -> GraphInfo: ]: raise ConfigInvalidError( f"the output chip address {ochip_coord} should not overlap with the " - f"target chip addresses, but got {_BACKEND_CONTEXT._target_chip_addr_repr()}." + f"chip addresses, but got {_BACKEND_CONTEXT._target_chip_addr_repr()}." ) input_nodes_info = self._inpproj_config_export() @@ -627,12 +619,6 @@ def export( Return: total configurations in dictionary format. """ - if self._core_estimate_only: - raise CompileError( - "the current compilation is only for core estimation. " - "Please disable 'core_estimate_only' and compile again before exporting." - ) - if format not in ("bin", "npy", "txt"): raise ValueError(f"format {format} is not supported.") @@ -705,27 +691,27 @@ def _find_dest_cb_by_nseg( return dest_cb_of_nseg -def cycle_merge(merged_sgrps: list[MergedSuccGroup]) -> list[MergedSuccGroup]: - succ_merged_sgrps: dict[MergedSuccGroup, list[MergedSuccGroup]] = defaultdict(list) - +def cycle_merge(merged_sgrps: list[MergedSuccGroup]): + succ_merged_sgrps: dict[MergedSuccGroup, list[MergedSuccGroup]] = dict() for msgrp in merged_sgrps: + succ_merged_sgrps[msgrp] = [] + nodes = set(msgrp.nodes) for _msgrp in merged_sgrps: if msgrp == _msgrp: continue - if not msgrp.nodes.isdisjoint(_msgrp.input_nodes): + if not nodes.isdisjoint(_msgrp.input_nodes): succ_merged_sgrps[msgrp].append(_msgrp) cycles: list[list[MergedSuccGroup]] = find_cycles(succ_merged_sgrps) merged_cycles: list[list[MergedSuccGroup]] = merge_overlap(cycles) processed_merged_cycles: list[MergedSuccGroup] = list() - remaining_msgrps: set[MergedSuccGroup] = set(merged_sgrps) - for mc in merged_cycles: - processed_merged_cycles.append(MergedSuccGroup.merge(mc)) - for msgrp in mc: - remaining_msgrps.remove(msgrp) - - processed_merged_cycles.extend(remaining_msgrps) + remaining_merged_sgrps: set[MergedSuccGroup] = set(merged_sgrps) + for merged_cycle in merged_cycles: + processed_merged_cycles.append(MergedSuccGroup.merge(merged_cycle)) + for msgrp in merged_cycle: + remaining_merged_sgrps.remove(msgrp) + processed_merged_cycles.extend(remaining_merged_sgrps) return processed_merged_cycles diff --git a/paibox/backend/placement.py b/paibox/backend/placement.py index 3fc370b9..e77e0035 100644 --- a/paibox/backend/placement.py +++ b/paibox/backend/placement.py @@ -1,6 +1,6 @@ import math import warnings -from typing import ClassVar, Literal, Optional, cast, overload +from typing import ClassVar, Literal, Optional, overload import numpy as np from paicorelib import LCN_EX, ChipCoord, Coord, CoreMode, HwConfig, MaxPoolingEnable @@ -177,9 +177,7 @@ def shape(self) -> tuple[int, int]: @property def source(self) -> list[SourceNodeType]: """Ordered unique source nodes.""" - return cast( - list[SourceNodeType], list(set([parent.source for parent in self.obj])) - ) + return list(set([parent.source for parent in self.obj])) @property def axons(self) -> list[SourceNodeType]: @@ -188,7 +186,7 @@ def axons(self) -> list[SourceNodeType]: @property def dest(self) -> list[DestNodeType]: """Ordered unique destination nodes.""" - return cast(list[DestNodeType], list(set([parent.dest for parent in self.obj]))) + return list(set([parent.dest for parent in self.obj])) def n_axon_of(self, index: int) -> int: """Get the #N of axons of `index`-th source neuron.""" diff --git a/paibox/backend/routing.py b/paibox/backend/routing.py index 2e854d6d..9154d185 100644 --- a/paibox/backend/routing.py +++ b/paibox/backend/routing.py @@ -11,14 +11,19 @@ from paicorelib import RoutingLevel as Level from paicorelib.routing_defs import MAX_ROUTING_PATH_LENGTH -from paibox.exceptions import PAIBoxDeprecationWarning, ResourceError, RoutingError +from paibox.exceptions import ( + GraphBuildError, + PAIBoxDeprecationWarning, + ResourceError, + RoutingError, +) from .conf_types import CorePlmConfInChip from .placement import CoreBlock, EmptyCorePlacement from .types import * if sys.version_info >= (3, 13): - from warnings import deprecated + from typing import deprecated else: from typing_extensions import deprecated diff --git a/paibox/backend/types.py b/paibox/backend/types.py index b8f34f3b..778e3853 100644 --- a/paibox/backend/types.py +++ b/paibox/backend/types.py @@ -30,6 +30,7 @@ "NodeDegree", "NodeAttr", "EdgeAttr", + "PartitionedEdges", "NeuSlice", "NeuSegment", "NeuSegOfCorePlm", @@ -87,19 +88,23 @@ def copy(self) -> "NodeDegree": return self.__deepcopy__() -@dataclass -class NodeAttr: +class NodeAttr(NamedTuple): node: NodeType position: NodePosition degree: NodeDegree -@dataclass -class EdgeAttr: # TODO FIXME distance? +class EdgeAttr(NamedTuple): edge: EdgeType distance: int +class PartitionedEdges(NamedTuple): + edges: set[EdgeType] + rg_id: int + rt_mode: CoreMode = CoreMode.MODE_SNN # XXX Temp solution + + NeuSlice: TypeAlias = slice diff --git a/paibox/base.py b/paibox/base.py index fbfe3959..31e25387 100644 --- a/paibox/base.py +++ b/paibox/base.py @@ -1,5 +1,4 @@ import sys -from dataclasses import dataclass from typing import Any, ClassVar, Literal, Optional import numpy as np @@ -257,77 +256,6 @@ def state(self) -> NodeDict: return self._memories -INFINITE_DATAFLOW = 0 - - -@dataclass -class DataFlowFormat: - """Describe in detail the format of valid data in the dataflow.""" - - t_1st_vld: int = 0 - """Global time or a relative time of the first valid data in the dataflow, determined by `is_local_time`.""" - interval: int = 1 - """The interval of valid data in the dataflow.""" - n_vld: int = INFINITE_DATAFLOW - """The number of valid data. <0 for infinite dataflow.""" - - is_local_time: bool = True - """Whether the `t_1st_vld` is relative to the local time(tws+T) of the neuron, or \ - relative to the global time of the external input.""" - - def t_at_idx(self, idx: int) -> int: - """The time of the valid data at the given index.""" - if self.n_vld > INFINITE_DATAFLOW: - assert 0 <= idx <= self.n_vld - 1 - - return self.t_1st_vld + idx * self.interval - - def t_at_n(self, n: int) -> int: - """The time of the n-th valid data.""" - return self.t_at_idx(n - 1) - - @property - def t_last_vld(self) -> int: - """The time of the last valid data.""" - assert self.n_vld > INFINITE_DATAFLOW - return self.t_at_n(self.n_vld) - - def get_global_t_1st_vld(self, tws: int) -> int: - """Get the global time of the first valid data.""" - return tws + self.t_1st_vld if self.is_local_time else self.t_1st_vld - - def _check_after_assign(self, tws: int, end_tick: int) -> None: - _t_1st_vld_out_of_range_text = ( - "the {0} output time of the first valid data should be in the working " - + "time from {1} to {2}, but got {3}." - ) - - # The global time of the first valid data is in [tws, end_tick]. - gb_t_1st_vld = self.get_global_t_1st_vld(tws) - if gb_t_1st_vld < tws or gb_t_1st_vld > end_tick: - if self.is_local_time: - raise ValueError( - _t_1st_vld_out_of_range_text.format( - "local", "+0", f"+{end_tick - tws + 1}", self.t_1st_vld - ) - ) - else: - raise ValueError( - _t_1st_vld_out_of_range_text.format( - "global", tws, end_tick, self.t_1st_vld - ) - ) - - if self.n_vld > INFINITE_DATAFLOW: - if ( - t_last_vld := gb_t_1st_vld + (self.n_vld - 1) * self.interval - ) > end_tick: - raise ValueError( - f"valid data is output after the end time. The neuron stops working at " - f"{end_tick}, but still needs to output at {t_last_vld}." - ) - - class NeuDyn(DynamicSys, ReceiveInputProj, TimeRelatedNode): _delay: int @@ -338,9 +266,6 @@ class NeuDyn(DynamicSys, ReceiveInputProj, TimeRelatedNode): _uf: int """unrolling_factor""" - oflow_format: DataFlowFormat - """The format of output data stream""" - def __init__(self, name: Optional[str] = None) -> None: super().__init__(name) self.master_nodes = NodeDict() @@ -366,14 +291,6 @@ def tick_wait_end(self) -> int: def unrolling_factor(self) -> int: return self._uf - @property - def end_tick(self) -> int: - """End time of work.""" - if self.tick_wait_end == 0: - return 9999 # Never end - - return self.tick_wait_start + self.tick_wait_end - 1 - @unrolling_factor.setter def unrolling_factor(self, factor: int) -> None: self._uf = arg_check_pos(factor, "'unrolling_factor'") diff --git a/paibox/components/_modules.py b/paibox/components/_modules.py index a2a0953d..990b5a77 100644 --- a/paibox/components/_modules.py +++ b/paibox/components/_modules.py @@ -1,10 +1,12 @@ +import math import typing +from dataclasses import dataclass from typing import Literal, Optional, Union import numpy as np from paicorelib import TM, HwConfig -from paibox.base import DataFlowFormat, NeuDyn, NodeList +from paibox.base import NeuDyn, NodeList from paibox.exceptions import ResourceError, ShapeError from paibox.types import ( LEAK_V_DTYPE, @@ -56,7 +58,7 @@ "_SpikingPool2dWithV", "_SemiFoldedModule", "_LinearBase", - "SemiFoldedDataFlowFormat", + "SemiFoldedStreamAttr", ] @@ -159,41 +161,61 @@ class _DelayChainANN(_DelayChainBase): pass -class SemiFoldedDataFlowFormat(DataFlowFormat): - pass +@dataclass(frozen=True) +class SemiFoldedStreamAttr: + """Details of transmission of valid data in semi-folded form data stream.""" + + t_1st_vld: int + """The time of the first valid data, relative to `t_1st_vld` of the external input.""" + interval: int + """The interval of the output data stream.""" + n_data: int = 0 + """The number of valid output data.""" + + def t_at(self, n: int) -> int: + """The time of the n-th valid data.""" + if self.n_data > 0: + assert 1 <= n <= self.n_data + + return self.t_1st_vld + (n - 1) * self.interval + + @property + def t_last_vld(self) -> int: + """The time of the last valid data.""" + assert self.n_data > 0 + return self.t_at(self.n_data) @set_rt_mode_ann() class _SemiFoldedModule(FunctionalModule): """Functional modules with interfaces in semi-folded form. Use `build()` of class `HasSemiFoldedIntf`.""" - inherent_delay = 1 - oflow_format: SemiFoldedDataFlowFormat + ostream_attr: SemiFoldedStreamAttr def build( self, network: "DynSysGroup", - incoming_flow_format: SemiFoldedDataFlowFormat, + incoming_stream_attr: SemiFoldedStreamAttr, **build_options, ) -> BuiltComponentType: raise NotImplementedError def _input_buffer_len_check( - self, ich: int, ih: int, kw: int, interval: int + self, in_channels: int, in_h: int, kw: int, valid_interval: int ) -> None: """Check the limit of the semi-folded operators on the input buffer length of the core during the build phase. - NOTE: The right side of the inequality will only be smaller in the backend. If the condition is not met, an \ - expection will be raised in the subsequent compilation phase. + NOTE: If the condition is not met, an expection will be raised in the subsequent compilation phase. """ E = math.ceil( - math.log2(math.ceil(ich * ih * kw / HwConfig.N_FANIN_PER_DENDRITE_ANN)) + math.log2( + math.ceil(in_channels * in_h * kw / HwConfig.N_FANIN_PER_DENDRITE_ANN) + ) ) - - if min(ih - kw, kw - 1) * interval + 1 >= (HwConfig.N_TIMESLOT_MAX >> E): - _adjust_text = "input size, kernel size or stride along the data flow." + deep = min(in_h - kw, kw - 1) * valid_interval + 1 + if not HwConfig.N_TIMESLOT_MAX / (2**E) > deep: raise ResourceError( - f"the data arrangement of {self.name}'s input buffer may be wrong. Please adjust the {_adjust_text}." + f"the input size of {self.name} is too large. Please adjust the input size or the number of channels." ) diff --git a/paibox/components/functional.py b/paibox/components/functional.py index 6755cc5e..37aa6fc9 100644 --- a/paibox/components/functional.py +++ b/paibox/components/functional.py @@ -905,26 +905,22 @@ class LinearSemiFolded(_LinearBase, _SemiFoldedModule): def build( self, network: "DynSysGroup", - incoming_flow_format: SemiFoldedDataFlowFormat, + incoming_stream_attr: SemiFoldedStreamAttr, **build_options, ) -> BuiltComponentType: assert len(self.source[0].shape_out) == 2 - # For semi-folded linear, the valid output is at only one timestep. - self.oflow_format = SemiFoldedDataFlowFormat( - incoming_flow_format.t_last_vld, 1, 1 - ) - twe = 1 + self.oflow_format.t_last_vld + self.ostream_attr = incoming_stream_attr + twe = 1 + self.ostream_attr.t_last_vld ich, ih = self.source[0].shape_out if build_options.get("check_before_compile"): - self._input_buffer_len_check(ich, ih, ih, incoming_flow_format.interval) - + self._input_buffer_len_check(ich, ih, ih, incoming_stream_attr.interval) n_delays = NodeList() s_delays = NodeList() s_weight = NodeList() - n_linear = ANNNeuron( + n_fc = ANNNeuron( self.shape_out, self.bias, self.bit_trunc, @@ -934,18 +930,13 @@ def build( keep_shape=self.keep_shape, name=f"nd_{self.name}", ) - n_linear.set_oflow_format( - self.oflow_format.t_1st_vld, - self.oflow_format.interval, - self.oflow_format.n_vld, - ) for i in range(ih): neuron = ANNBypassNeuron( shape=(ich, ih), - delay=incoming_flow_format.interval * i + 1, + delay=incoming_stream_attr.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe - incoming_flow_format.interval * i, + tick_wait_end=twe - incoming_stream_attr.interval * i, keep_shape=self.keep_shape, name=f"n{i}_{self.name}", ) @@ -963,15 +954,15 @@ def build( w = self.weights[ih - i - 1 :: ih, :] syn2 = FullConnSyn( neuron, - n_linear, + n_fc, weights=w, conn_type=ConnType.All2All, name=f"s{i}_{self.name}", ) s_weight.append(syn2) - generated = [n_linear, *n_delays, *s_delays, *s_weight] - self._rebuild_out_intf(network, n_linear, *generated, **build_options) + generated = [n_fc, *n_delays, *s_delays, *s_weight] + self._rebuild_out_intf(network, n_fc, *generated, **build_options) return generated @@ -1017,11 +1008,9 @@ def __init__( # XXX Do not consider the case when the shape of source neurons needs to be changed, for now. # neuron_s.shape_change((in_ch, in_h)) - cout, cin, kh, kw = kernel.shape + cout, cin, kh, _ = kernel.shape out_h = (in_h - kh + 2 * self.padding[0]) // self.stride[0] + 1 - assert self.padding[0] < kh and self.padding[1] < kw - if in_ch != cin: raise ShapeError(f"the channels mismatch: {in_ch} != {cin}.") @@ -1045,7 +1034,7 @@ def __init__( def build( self, network: "DynSysGroup", - incoming_flow_format: SemiFoldedDataFlowFormat, + incoming_stream_attr: SemiFoldedStreamAttr, **build_options, ) -> BuiltComponentType: assert len(self.source[0].shape_out) == 2 @@ -1058,15 +1047,14 @@ def build( _, cin, _, kw = self.kernel.shape _, ow = self.shape_out - self.oflow_format = SemiFoldedDataFlowFormat( - incoming_flow_format.t_at_n(kw - self.padding[0]), - incoming_flow_format.interval * self.stride[1], + self.ostream_attr = SemiFoldedStreamAttr( + incoming_stream_attr.t_at(kw - self.padding[0]), + incoming_stream_attr.interval * self.stride[1], ow, ) - twe = 1 + self.oflow_format.t_last_vld - + twe = 1 + self.ostream_attr.t_last_vld if build_options.get("check_before_compile"): - self._input_buffer_len_check(cin, ih, kw, incoming_flow_format.interval) + self._input_buffer_len_check(cin, ih, kw, incoming_stream_attr.interval) n_delays = NodeList() n_neg_padding = NodeList() @@ -1084,18 +1072,12 @@ def build( keep_shape=self.keep_shape, name=f"nd_{self.name}", ) - n_conv2d.set_oflow_format( - self.oflow_format.t_1st_vld, - self.oflow_format.interval, - self.oflow_format.n_vld, - ) - for i in range(kw): neuron = ANNBypassNeuron( (cin, ih), - delay=incoming_flow_format.interval * i + 1, + delay=incoming_stream_attr.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe - incoming_flow_format.interval * i, + tick_wait_end=twe - incoming_stream_attr.interval * i, name=f"n{i}_delay_{self.name}", ) n_delays.append(neuron) @@ -1123,13 +1105,13 @@ def build( # Add additional negative padding layer to eliminate the incorrect output # NOTE: `t_1st_vld` = 0 & `padding[0]` > 0 means the previous layer is # an input node. No need to add negative padding layer for this case. - if incoming_flow_format.t_1st_vld > 0: + if incoming_stream_attr.t_1st_vld > 0: for p in range(self.padding[0]): neuron = ANNBypassNeuron( (cin, ih), - delay=1 + incoming_flow_format.interval * (kw - 1 - p), + delay=1 + incoming_stream_attr.interval * (kw - 1 - p), tick_wait_start=self.tick_wait_start, - tick_wait_end=incoming_flow_format.t_1st_vld, + tick_wait_end=incoming_stream_attr.t_1st_vld, keep_shape=self.keep_shape, name=f"n{p}_pad_{self.name}", ) @@ -1214,7 +1196,7 @@ def __init__( def build( self, network: "DynSysGroup", - incoming_flow_format: SemiFoldedDataFlowFormat, + incoming_stream_attr: SemiFoldedStreamAttr, **build_options, ) -> BuiltComponentType: assert len(self.source[0].shape_out) == 2 @@ -1227,20 +1209,20 @@ def build( kh, kw = self.kernel_size _, ow = self.shape_out - self.oflow_format = SemiFoldedDataFlowFormat( - incoming_flow_format.t_at_n(kw), - incoming_flow_format.interval * self.stride[1], + self.ostream_attr = SemiFoldedStreamAttr( + incoming_stream_attr.t_at(kw), + incoming_stream_attr.interval * self.stride[1], ow, ) - twe = 1 + self.oflow_format.t_last_vld + twe = 1 + self.ostream_attr.t_last_vld if build_options.get("check_before_compile"): - self._input_buffer_len_check(cin, ih, kw, incoming_flow_format.interval) + self._input_buffer_len_check(cin, ih, kw, incoming_stream_attr.interval) n_delays = NodeList() s_delays = NodeList() - n_pool2d = ANNNeuron( + pool2d = ANNNeuron( self.shape_out, delay=self.delay_relative, tick_wait_start=self.tick_wait_start + 1, @@ -1249,18 +1231,13 @@ def build( keep_shape=self.keep_shape, name=f"nd_{self.name}", ) - n_pool2d.set_oflow_format( - self.oflow_format.t_1st_vld, - self.oflow_format.interval, - self.oflow_format.n_vld, - ) for i in range(kw): neuron = ANNBypassNeuron( (cin, ih), - delay=incoming_flow_format.interval * i + 1, + delay=incoming_stream_attr.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe - incoming_flow_format.interval * i, + tick_wait_end=twe - incoming_stream_attr.interval * i, keep_shape=self.keep_shape, name=f"n{i}_{self.name}", ) @@ -1276,7 +1253,7 @@ def build( s_delays.append(syn1) syn2 = MaxPoolSyn( neuron, - n_pool2d, + pool2d, weights=_poo2d_semifolded_mapping_mask( cin, ih, ow, kh, self.stride, (0, 0) ), @@ -1284,8 +1261,8 @@ def build( ) s_delays.append(syn2) - generated = [n_pool2d, *n_delays, *s_delays] - self._rebuild_out_intf(network, n_pool2d, *generated, **build_options) + generated = [pool2d, *n_delays, *s_delays] + self._rebuild_out_intf(network, pool2d, *generated, **build_options) return generated @@ -1325,8 +1302,6 @@ def __init__( assert len(neuron_s.shape_out) == 2 in_ch, in_h = neuron_s.shape_out out_h = (in_h - self.kernel_size[0] + 2 * self.padding[0]) // self.stride[0] + 1 - kh, kw = self.kernel_size - assert self.padding[0] < kh and self.padding[1] < kw super().__init__( neuron_s, @@ -1339,7 +1314,7 @@ def __init__( def build( self, network: "DynSysGroup", - incoming_flow_format: SemiFoldedDataFlowFormat, + incoming_stream_attr: SemiFoldedStreamAttr, **build_options, ) -> BuiltComponentType: assert len(self.source[0].shape_out) == 2 @@ -1352,15 +1327,15 @@ def build( kh, kw = self.kernel_size _, ow = self.shape_out - self.oflow_format = SemiFoldedDataFlowFormat( - incoming_flow_format.t_at_n(kw - self.padding[0]), - incoming_flow_format.interval * self.stride[1], + self.ostream_attr = SemiFoldedStreamAttr( + incoming_stream_attr.t_at(kw - self.padding[0]), + incoming_stream_attr.interval * self.stride[1], ow, ) - twe = 1 + self.oflow_format.t_last_vld + twe = 1 + self.ostream_attr.t_last_vld if build_options.get("check_before_compile"): - self._input_buffer_len_check(cin, ih, kw, incoming_flow_format.interval) + self._input_buffer_len_check(cin, ih, kw, incoming_stream_attr.interval) # NOTE: Division is achieved with the help of output truncation. # TODO Since division with a divisor that is an integer power of 2 can only be implemented by @@ -1380,7 +1355,7 @@ def build( s_delays = NodeList() s_neg_padding = NodeList() - n_pool2d = ANNNeuron( + pool2d = ANNNeuron( self.shape_out, delay=self.delay_relative, bit_trunc=bit_trunc, @@ -1389,18 +1364,12 @@ def build( keep_shape=self.keep_shape, name=f"nd_{self.name}", ) - n_pool2d.set_oflow_format( - self.oflow_format.t_1st_vld, - self.oflow_format.interval, - self.oflow_format.n_vld, - ) - for i in range(kw): neuron = ANNBypassNeuron( (cin, ih), - delay=incoming_flow_format.interval * i + 1, + delay=incoming_stream_attr.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe - incoming_flow_format.interval * i, + tick_wait_end=twe - incoming_stream_attr.interval * i, keep_shape=self.keep_shape, name=f"n{i}_{self.name}", ) @@ -1416,7 +1385,7 @@ def build( s_delays.append(syn1) syn2 = FullConnSyn( neuron, - n_pool2d, + pool2d, weights=_poo2d_semifolded_mapping_mask( cin, ih, ow, kh, self.stride, self.padding ), @@ -1426,13 +1395,13 @@ def build( s_delays.append(syn2) # Add additional negative padding layer to eliminate the incorrect output - if incoming_flow_format.t_1st_vld > 0: + if incoming_stream_attr.t_1st_vld > 0: for p in range(self.padding[0]): neuron = ANNBypassNeuron( (cin, ih), - delay=1 + incoming_flow_format.interval * (kw - 1 - p), + delay=1 + incoming_stream_attr.interval * (kw - 1 - p), tick_wait_start=self.tick_wait_start, - tick_wait_end=incoming_flow_format.t_1st_vld, + tick_wait_end=incoming_stream_attr.t_1st_vld, keep_shape=self.keep_shape, name=f"n{p}_pad_{self.name}", ) @@ -1449,7 +1418,7 @@ def build( syn2 = FullConnSyn( neuron, - n_pool2d, + pool2d, weights=-_poo2d_semifolded_mapping_mask( cin, ih, ow, kh, self.stride, self.padding ), @@ -1458,8 +1427,8 @@ def build( ) s_neg_padding.append(syn2) - generated = [n_pool2d, *n_delays, *n_neg_padding, *s_delays, *s_neg_padding] - self._rebuild_out_intf(network, n_pool2d, *generated, **build_options) + generated = [pool2d, *n_delays, *n_neg_padding, *s_delays, *s_neg_padding] + self._rebuild_out_intf(network, pool2d, *generated, **build_options) return generated diff --git a/paibox/components/modules.py b/paibox/components/modules.py index 703c1a00..41e06fdc 100644 --- a/paibox/components/modules.py +++ b/paibox/components/modules.py @@ -14,7 +14,7 @@ from paibox.types import NEUOUT_U8_DTYPE, NeuOutType, VoltageType from paibox.utils import check_elem_unique, shape2num -from .neuron.utils import RTModeKwds, _input_width_format, _spike_width_format +from .neuron.utils import _input_width_format, _RTModeKwds, _spike_width_format from .projection import InputProj if sys.version_info >= (3, 10): @@ -91,7 +91,7 @@ class NeuModule(NeuDyn, BuildingModule): """#N of outputs.""" inherent_delay: int = 0 """Internal delay of the module, relative to the external.""" - rt_mode_kwds: RTModeKwds + rt_mode_kwds: _RTModeKwds mode: CoreMode def __init__( diff --git a/paibox/components/neuron/base.py b/paibox/components/neuron/base.py index 635ac3f6..a35fa8b5 100644 --- a/paibox/components/neuron/base.py +++ b/paibox/components/neuron/base.py @@ -20,8 +20,8 @@ get_core_mode, ) -from paibox.base import DataFlowFormat, NeuDyn -from paibox.exceptions import ConfigInvalidError, PAIBoxWarning, ShapeError +from paibox.base import NeuDyn +from paibox.exceptions import NotSupportedError, PAIBoxWarning, ShapeError from paibox.types import ( NEUOUT_U8_DTYPE, VOLTAGE_DTYPE, @@ -41,10 +41,10 @@ from .utils import ( BIT_TRUNCATE_MAX, NEG_THRES_MIN, - RTModeKwds, _input_width_format, _leak_v_check, _mask, + _RTModeKwds, _spike_width_format, vjt_overflow, ) @@ -57,7 +57,7 @@ class MetaNeuron: """Meta neuron""" - rt_mode_kwds: RTModeKwds + rt_mode_kwds: _RTModeKwds mode: CoreMode def __init__( @@ -96,8 +96,8 @@ def __init__( # check whether the mode is valid self.mode = get_core_mode(input_width, spike_width, snn_en) - if pool_max and self.mode != CoreMode.MODE_ANN: - raise ConfigInvalidError( + if pool_max == True and self.mode != CoreMode.MODE_ANN: + raise NotSupportedError( f"max pooling is only supported in {CoreMode.MODE_ANN.name}, " f"but got {self.mode.name}." ) @@ -487,13 +487,11 @@ def __init__( ), ) - """Non-stateful attributes.""" + """Auxiliary internal stateful attributes for debugging""" self._delay = arg_check_pos(delay, "'delay'") self._tws = arg_check_non_neg(tick_wait_start, "'tick_wait_start'") self._twe = arg_check_non_neg(tick_wait_end, "'tick_wait_end'") self._uf = arg_check_pos(unrolling_factor, "'unrolling_factor'") - # Default dataflow is infinite and continuous, starting at tws+0. - self.oflow_format = DataFlowFormat(0, is_local_time=True) def __len__(self) -> int: return self._n_neuron @@ -531,49 +529,6 @@ def update( def reset_state(self, *args, **kwargs) -> None: self.reset_memory() # Call reset of `StatusMemory`. - def set_oflow_format( - self, - t_1st_vld: Optional[int] = None, - interval: Optional[int] = None, - n_vld: Optional[int] = None, - *, - format_type: type[DataFlowFormat] = DataFlowFormat, - ) -> None: - """Set the attributes of output dataflow format by given arguments.""" - if hasattr(self, "oflow_format"): - _t_1st_vld = ( - t_1st_vld if isinstance(t_1st_vld, int) else self.oflow_format.t_1st_vld - ) - _interval = ( - arg_check_pos(interval, "interval") - if isinstance(interval, int) - else self.oflow_format.interval - ) - _n_vld = ( - arg_check_non_neg(n_vld, "n_vld") - if isinstance(n_vld, int) - else self.oflow_format.n_vld - ) - self._assign_flow_format(_t_1st_vld, _interval, _n_vld) - else: - if not ( - isinstance(interval, int) - and isinstance(n_vld, int) - and isinstance(t_1st_vld, int) - ): - raise ValueError( - "if 'oflow_format' is not set, 't_1st_vld', 'interval' & 'n_vld' must be set." - ) - - self.oflow_format = format_type(t_1st_vld, interval, n_vld) - self.oflow_format._check_after_assign(self.tick_wait_start, self.end_tick) - - def _assign_flow_format(self, t_1st_vld: int, intv: int, n_vld: int) -> None: - self.oflow_format.t_1st_vld = t_1st_vld - self.oflow_format.interval = intv - self.oflow_format.n_vld = n_vld - self.oflow_format._check_after_assign(self.tick_wait_start, self.end_tick) - def __copy__(self) -> "Neuron": """Same as `__deepcopy__`.""" return self.__deepcopy__() diff --git a/paibox/components/neuron/neurons.py b/paibox/components/neuron/neurons.py index 3f7f0279..85e3df47 100644 --- a/paibox/components/neuron/neurons.py +++ b/paibox/components/neuron/neurons.py @@ -8,15 +8,10 @@ from paibox.types import LEAK_V_DTYPE, DataType, Shape from .base import Neuron -from .utils import LEAK_V_MAX, ExtraNeuAttrKwds - -if sys.version_info >= (3, 12): - from typing import Unpack -else: - from typing_extensions import Unpack +from .utils import LEAK_V_MAX if sys.version_info >= (3, 13): - from warnings import deprecated + from typing import deprecated else: from typing_extensions import deprecated @@ -42,7 +37,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs: Unpack[ExtraNeuAttrKwds], + **kwargs, ) -> None: """IF neuron. @@ -98,7 +93,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs: Unpack[ExtraNeuAttrKwds], + **kwargs, ) -> None: """LIF neuron. @@ -157,7 +152,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs: Unpack[ExtraNeuAttrKwds], + **kwargs, ) -> None: """Tonic spiking neuron. @@ -183,7 +178,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs: Unpack[ExtraNeuAttrKwds], + **kwargs, ) -> None: """Phasic spiking neuron. Once the neuron receives `N` spikes and fires, it will reset to \ the negative floor and never fires again. `N` is `fire_step`. @@ -218,7 +213,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs: Unpack[ExtraNeuAttrKwds], + **kwargs, ) -> None: """A neuron that always outputs 1 as long as it starts working. @@ -250,7 +245,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs: Unpack[ExtraNeuAttrKwds], + **kwargs, ) -> None: """Bypass neuron. Output is equal to input. @@ -284,7 +279,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs: Unpack[ExtraNeuAttrKwds], + **kwargs, ) -> None: """General neuron used in ANN mode. Positive threshold = 1, negative threshold = 0.""" kwargs["bit_truncation"] = bit_trunc @@ -304,7 +299,7 @@ def __init__( *, keep_shape: bool = True, name: Optional[str] = None, - **kwargs: Unpack[ExtraNeuAttrKwds], + **kwargs, ) -> None: super().__init__( shape, bias=0, bit_trunc=8, keep_shape=keep_shape, name=name, **kwargs diff --git a/paibox/components/neuron/utils.py b/paibox/components/neuron/utils.py index 349ea050..1d9ea2ff 100644 --- a/paibox/components/neuron/utils.py +++ b/paibox/components/neuron/utils.py @@ -2,12 +2,7 @@ from typing import Literal, TypedDict, Union import numpy as np -from paicorelib import ( - InputWidthFormat, - MaxPoolingEnable, - SNNModeEnable, - SpikeWidthFormat, -) +from paicorelib import InputWidthFormat, SNNModeEnable, SpikeWidthFormat from paicorelib.framelib.utils import _mask from paicorelib.ram_model import ( BIT_TRUNCATE_MAX, @@ -112,24 +107,9 @@ def _get_neu_out_dtype( return NEUOUT_U8_DTYPE -class RTModeKwds(TypedDict): +class _RTModeKwds(TypedDict): """A typed keywords for runtime mode. Only for checking if necessary.""" input_width: InputWidthFormat spike_width: SpikeWidthFormat snn_en: SNNModeEnable - - -class ExtraNeuAttrKwds(TypedDict, total=False): - """A typed keywords for extra neuron attributes.""" - - bit_truncation: int # For ANNNeuron - delay: int - tick_wait_start: int - tick_wait_end: int - input_width: Union[L[1, 8], InputWidthFormat] - spike_width: Union[L[1, 8], SpikeWidthFormat] - snn_en: Union[bool, SNNModeEnable] - pool_max: Union[bool, MaxPoolingEnable] - unrolling_factor: int - overflow_strict: bool diff --git a/paibox/exceptions.py b/paibox/exceptions.py index e5204ad0..55514a43 100644 --- a/paibox/exceptions.py +++ b/paibox/exceptions.py @@ -57,13 +57,7 @@ class GraphConnectionError(GraphBuildError): class NotSupportedError(PAIBoxError, NotImplementedError): - """Exception for unsupported functions.""" - - pass - - -class GraphNotSupportedError(GraphBuildError, NotSupportedError): - """Eception for unsupported structures of graph.""" + """Exception for a certain function not supported.""" pass @@ -80,12 +74,6 @@ class FunctionalError(PAIBoxError, RuntimeError): pass -class CompileError(PAIBoxError, RuntimeError): - """Exception for compilation.""" - - pass - - class RoutingError(PAIBoxError): """Exception for routing tree.""" diff --git a/paibox/network.py b/paibox/network.py index 6db83afd..4f0a4c12 100644 --- a/paibox/network.py +++ b/paibox/network.py @@ -7,7 +7,7 @@ from .base import DynamicSys, SynSys from .collector import Collector from .components import NeuModule, Neuron, Projection -from .components._modules import SemiFoldedDataFlowFormat, _SemiFoldedModule +from .components._modules import SemiFoldedStreamAttr, _SemiFoldedModule from .components.modules import BuiltComponentType from .exceptions import NotSupportedError from .mixin import Container @@ -102,18 +102,18 @@ def build_modules( generated = dict() - # For external input dataflow: - # 1. The start time is 0. - # 2. The interval is 1. - # 3. The #N of data is `INFINITE_DATA_STREAM` since it dosen't effect the subsequent output dataflow. + # For external input stream info: + # 1. The start time is 1 + # 2. The interval is 1 + # 3. The #N of data is -1 since it dosen't effect the subsequent output stream. # TODO Reserve an interface for setting the properties of external input from `FRONTEND_ENV`? - last_vld_output_attr = SemiFoldedDataFlowFormat(t_1st_vld=0) + last_vld_output_attr = SemiFoldedStreamAttr(0, 1) for m in modules: # TODO for the case of the ResBlock, the `pred_dg_semi_ops` will be used. if isinstance(m, _SemiFoldedModule): generated[m] = m.build(self, last_vld_output_attr, **build_options) - last_vld_output_attr = m.oflow_format + last_vld_output_attr = m.ostream_attr else: generated[m] = m.build(self, **build_options) diff --git a/paibox/utils.py b/paibox/utils.py index 906933cf..6c43864b 100644 --- a/paibox/utils.py +++ b/paibox/utils.py @@ -155,33 +155,33 @@ def reverse_16bit(x: int) -> int: return ((x >> 8) | (x << 8)) & 0xFFFF -def _get_desc(desc: Optional[str] = None) -> str: - return "value" if desc is None else desc - - def arg_check_pos(arg: int, desc: Optional[str] = None) -> int: + _desc = "value" if desc is None else f"{desc}" if arg < 1: - raise ValueError(f"{_get_desc(desc)} must be positive, but got {arg}.") + raise ValueError(f"{_desc} must be positive, but got {arg}.") return arg def arg_check_non_pos(arg: int, desc: Optional[str] = None) -> int: + _desc = "value" if desc is None else f"{desc}" if arg > 0: - raise ValueError(f"{_get_desc(desc)} must be non-positive, but got {arg}.") + raise ValueError(f"{_desc} must be non-positive, but got {arg}.") return arg def arg_check_neg(arg: int, desc: Optional[str] = None) -> int: + _desc = "value" if desc is None else f"{desc}" if arg > -1: - raise ValueError(f"{_get_desc(desc)} must be negative, but got {arg}.") + raise ValueError(f"{_desc} must be negative, but got {arg}.") return arg def arg_check_non_neg(arg: int, desc: Optional[str] = None) -> int: + _desc = "value" if desc is None else f"{desc}" if arg < 0: - raise ValueError(f"{_get_desc(desc)} must be non-negative, but got {arg}.") + raise ValueError(f"{_desc} must be non-negative, but got {arg}.") return arg diff --git a/pyproject.toml b/pyproject.toml index 08223a17..71124dab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ homepage = "https://github.com/PAICookers/PAIBox" documentation = "https://github.com/PAICookers/PAIBox#readme" keywords = ["PAICORE 2.0", "PAIBox", "SNN", "Toolchain"] classifiers = [ - "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Operating System :: OS Independent", @@ -25,9 +24,9 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Artificial Intelligence", - "Topic :: Software Development :: Compilers", + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries", ] packages = [{ include = "paibox" }] diff --git a/tests/components/test_functional.py b/tests/components/test_functional.py index 4aae8961..c2c63a82 100644 --- a/tests/components/test_functional.py +++ b/tests/components/test_functional.py @@ -4,7 +4,6 @@ import paibox as pb from paibox.base import DynamicSys from paibox.components import NeuModule -from paibox.components._modules import _SemiFoldedModule from paibox.components.neuron.base import MetaNeuron from paibox.components.synapses.conv_utils import _conv2d_faster, _pair, _single from paibox.network import DynSysGroup @@ -977,12 +976,12 @@ def test_Conv2dSemiFolded_FC_ChainNet( probe_linear = pb.Probe(generated[linear][0], "output") sim1.add_probe(probe_linear) - semi_folded_modules: list[_SemiFoldedModule] = [*conv2d_list, linear] + semi_folded_modules = [*conv2d_list, linear] # The interval & the time o the first valid data of the external input data stream semi_vld_out_intv0 = 1 t_1st_vld_data0 = 0 # The interval & the time of the first valid data of the current layers - semi_vld_out_intv = [m.oflow_format.interval for m in semi_folded_modules] + semi_vld_out_intv = [m.ostream_attr.interval for m in semi_folded_modules] t_1st_vld_data = [0] * n_conv for i in range(n_conv): if i == 0: @@ -1043,16 +1042,6 @@ def test_Conv2dSemiFolded_FC_ChainNet( ], ) - assert conv2d_list[i_conv].tick_wait_start + t_1st_vld_data[ - i_conv - ] + i * semi_vld_out_intv[i_conv] - 1 == conv2d_list[ - i_conv - ].tick_wait_start + conv2d_list[ - i_conv - ].oflow_format.t_at_idx( - i - ) - # x is the reference result of the last convolution. expected_fc_t = _ann_bit_trunc(x.ravel() @ fc_weight.astype(VOLTAGE_DTYPE)) @@ -1060,13 +1049,9 @@ def test_Conv2dSemiFolded_FC_ChainNet( assert np.array_equal( expected_fc_t, sim1.data[probe_linear][ - linear.tick_wait_start + linear.oflow_format.t_last_vld + linear.tick_wait_start + linear.ostream_attr.t_last_vld ], ) - assert ( - linear.oflow_format.get_global_t_1st_vld(linear.tick_wait_start) - == linear.tick_wait_start + linear.oflow_format.t_last_vld - ) @pytest.mark.parametrize( "ishape_chw, n_pool, kshape_hw, stride, padding, out_features, pool_type", @@ -1186,12 +1171,12 @@ def test_Pool2dSemiFolded_FC_ChainNet( probe_linear = pb.Probe(generated[linear][0], "output") sim1.add_probe(probe_linear) - semi_folded_modules: list[_SemiFoldedModule] = [*pool2d_list, linear] + semi_folded_modules = [*pool2d_list, linear] # The interval & the time o the first valid data of the external input data stream semi_vld_out_intv0 = 1 t_1st_vld_data0 = 0 # The interval & the time of the first valid data of the current layers - semi_vld_out_intv = [m.oflow_format.interval for m in semi_folded_modules] + semi_vld_out_intv = [m.ostream_attr.interval for m in semi_folded_modules] t_1st_vld_data = [0] * n_pool for i in range(n_pool): if i == 0: @@ -1239,16 +1224,6 @@ def test_Pool2dSemiFolded_FC_ChainNet( ], ) - assert pool2d_list[i_pool].tick_wait_start + t_1st_vld_data[ - i_pool - ] + i * semi_vld_out_intv[i_pool] - 1 == pool2d_list[ - i_pool - ].tick_wait_start + pool2d_list[ - i_pool - ].oflow_format.t_at_idx( - i - ) - # x is the reference result of the last pooling. expected_fc_t = _ann_bit_trunc(x.ravel() @ fc_weight.astype(VOLTAGE_DTYPE)) @@ -1256,15 +1231,10 @@ def test_Pool2dSemiFolded_FC_ChainNet( assert np.array_equal( expected_fc_t, sim1.data[probe_linear][ - linear.tick_wait_start + linear.oflow_format.t_last_vld + linear.tick_wait_start + linear.ostream_attr.t_last_vld ], ) - assert ( - linear.oflow_format.get_global_t_1st_vld(linear.tick_wait_start) - == linear.tick_wait_start + linear.oflow_format.t_last_vld - ) - @pytest.mark.parametrize( "shape, weight", [ diff --git a/tests/test_base.py b/tests/test_base.py index efe9a2ef..5adb2c7f 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,7 +1,7 @@ import pytest import paibox as pb -from paibox.base import DataFlowFormat, PAIBoxObject +from paibox.base import PAIBoxObject from paibox.exceptions import RegisterError @@ -41,20 +41,3 @@ def test_paiboxobject_nodes(): nodes4 = obj1.nodes(method="absolute", level=-1, include_self=True) assert nodes4["obj111"] == obj1 - - -class TestDataFlowFormat: - def test_dff_infinite_dataflow(self): - with pytest.raises((AssertionError, ValueError)): - dff = DataFlowFormat(1, 0, -1) - _ = dff.t_last_vld - - def test_dff_valid(self): - # 1. t1 >= tws, t_last > endtick - dff1 = DataFlowFormat(10, 3, 10, is_local_time=False) - with pytest.raises(ValueError): - dff1._check_after_assign(8, 36) - - # 2. t1 >= tws, t_last <= endtick - dff2 = DataFlowFormat(10, 3, 10, is_local_time=True) - dff2._check_after_assign(2, 39) From 5dd9a85471041319f4a266cfbcad2665ace53108 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:35:35 +0000 Subject: [PATCH 168/187] :rotating_light: auto fix by pre-commit hooks --- paibox/base.py | 2 +- paibox/components/modules.py | 2 +- paibox/components/neuron/base.py | 2 +- paibox/components/neuron/utils.py | 2 +- tests/test_base.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/paibox/base.py b/paibox/base.py index c47260ff..fbfe3959 100644 --- a/paibox/base.py +++ b/paibox/base.py @@ -1,5 +1,5 @@ -from dataclasses import dataclass import sys +from dataclasses import dataclass from typing import Any, ClassVar, Literal, Optional import numpy as np diff --git a/paibox/components/modules.py b/paibox/components/modules.py index 2c13edef..703c1a00 100644 --- a/paibox/components/modules.py +++ b/paibox/components/modules.py @@ -14,7 +14,7 @@ from paibox.types import NEUOUT_U8_DTYPE, NeuOutType, VoltageType from paibox.utils import check_elem_unique, shape2num -from .neuron.utils import _input_width_format, RTModeKwds, _spike_width_format +from .neuron.utils import RTModeKwds, _input_width_format, _spike_width_format from .projection import InputProj if sys.version_info >= (3, 10): diff --git a/paibox/components/neuron/base.py b/paibox/components/neuron/base.py index bef9976b..635ac3f6 100644 --- a/paibox/components/neuron/base.py +++ b/paibox/components/neuron/base.py @@ -41,10 +41,10 @@ from .utils import ( BIT_TRUNCATE_MAX, NEG_THRES_MIN, + RTModeKwds, _input_width_format, _leak_v_check, _mask, - RTModeKwds, _spike_width_format, vjt_overflow, ) diff --git a/paibox/components/neuron/utils.py b/paibox/components/neuron/utils.py index dd58006f..349ea050 100644 --- a/paibox/components/neuron/utils.py +++ b/paibox/components/neuron/utils.py @@ -4,9 +4,9 @@ import numpy as np from paicorelib import ( InputWidthFormat, + MaxPoolingEnable, SNNModeEnable, SpikeWidthFormat, - MaxPoolingEnable, ) from paicorelib.framelib.utils import _mask from paicorelib.ram_model import ( diff --git a/tests/test_base.py b/tests/test_base.py index cd55346d..efe9a2ef 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,7 +1,7 @@ import pytest import paibox as pb -from paibox.base import PAIBoxObject, DataFlowFormat +from paibox.base import DataFlowFormat, PAIBoxObject from paibox.exceptions import RegisterError From dae4c3ca06522dae22a2010993723b2c075210fb Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Tue, 3 Dec 2024 16:41:11 +0800 Subject: [PATCH 169/187] =?UTF-8?q?=F0=9F=9A=9A=20rename=20`oflow=5Fformat?= =?UTF-8?q?`=20to=20`=5Foflow=5Fformat`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/graphs.py | 2 +- paibox/base.py | 2 +- paibox/components/_modules.py | 2 +- paibox/components/functional.py | 42 ++++++++++++++--------------- paibox/components/neuron/base.py | 26 +++++++++--------- paibox/network.py | 2 +- tests/components/test_functional.py | 20 +++++++------- 7 files changed, 49 insertions(+), 47 deletions(-) diff --git a/paibox/backend/graphs.py b/paibox/backend/graphs.py index 69a117e7..e904d4ab 100644 --- a/paibox/backend/graphs.py +++ b/paibox/backend/graphs.py @@ -533,7 +533,7 @@ def _find_rg_by_cb( def inherent_timestep(self) -> int: self.build_check() return max( - n.oflow_format.get_global_t_1st_vld(n.tick_wait_start) + n._oflow_format.get_global_t_1st_vld(n.tick_wait_start) for n in self.onodes.values() ) diff --git a/paibox/base.py b/paibox/base.py index fbfe3959..82740f00 100644 --- a/paibox/base.py +++ b/paibox/base.py @@ -338,7 +338,7 @@ class NeuDyn(DynamicSys, ReceiveInputProj, TimeRelatedNode): _uf: int """unrolling_factor""" - oflow_format: DataFlowFormat + _oflow_format: DataFlowFormat """The format of output data stream""" def __init__(self, name: Optional[str] = None) -> None: diff --git a/paibox/components/_modules.py b/paibox/components/_modules.py index 991a3f48..907f0a95 100644 --- a/paibox/components/_modules.py +++ b/paibox/components/_modules.py @@ -168,7 +168,7 @@ class _SemiFoldedModule(FunctionalModule): """Functional modules with interfaces in semi-folded form. Use `build()` of class `HasSemiFoldedIntf`.""" inherent_delay = 1 - oflow_format: SemiFoldedDataFlowFormat + _oflow_format: SemiFoldedDataFlowFormat def build( self, diff --git a/paibox/components/functional.py b/paibox/components/functional.py index be7897c9..ec0bb1c8 100644 --- a/paibox/components/functional.py +++ b/paibox/components/functional.py @@ -910,10 +910,10 @@ def build( ) -> BuiltComponentType: assert len(self.source[0].shape_out) == 2 # For semi-folded linear, the valid output is at only one timestep. - self.oflow_format = SemiFoldedDataFlowFormat( + self._oflow_format = SemiFoldedDataFlowFormat( incoming_flow_format.t_last_vld, 1, 1 ) - twe = 1 + self.oflow_format.t_last_vld + twe = 1 + self._oflow_format.t_last_vld ich, ih = self.source[0].shape_out @@ -932,9 +932,9 @@ def build( name=f"nd_{self.name}", ) n_linear.set_oflow_format( - self.oflow_format.t_1st_vld, - self.oflow_format.interval, - self.oflow_format.n_vld, + self._oflow_format.t_1st_vld, + self._oflow_format.interval, + self._oflow_format.n_vld, ) for i in range(ih): @@ -1055,12 +1055,12 @@ def build( _, cin, _, kw = self.kernel.shape _, ow = self.shape_out - self.oflow_format = SemiFoldedDataFlowFormat( + self._oflow_format = SemiFoldedDataFlowFormat( incoming_flow_format.t_at_n(kw - self.padding[0]), incoming_flow_format.interval * self.stride[1], ow, ) - twe = 1 + self.oflow_format.t_last_vld + twe = 1 + self._oflow_format.t_last_vld if build_options.get("check_before_compile"): self._input_buffer_len_check(cin, ih, kw, incoming_flow_format.interval) @@ -1082,9 +1082,9 @@ def build( name=f"nd_{self.name}", ) n_conv2d.set_oflow_format( - self.oflow_format.t_1st_vld, - self.oflow_format.interval, - self.oflow_format.n_vld, + self._oflow_format.t_1st_vld, + self._oflow_format.interval, + self._oflow_format.n_vld, ) for i in range(kw): @@ -1224,12 +1224,12 @@ def build( kh, kw = self.kernel_size _, ow = self.shape_out - self.oflow_format = SemiFoldedDataFlowFormat( + self._oflow_format = SemiFoldedDataFlowFormat( incoming_flow_format.t_at_n(kw), incoming_flow_format.interval * self.stride[1], ow, ) - twe = 1 + self.oflow_format.t_last_vld + twe = 1 + self._oflow_format.t_last_vld if build_options.get("check_before_compile"): self._input_buffer_len_check(cin, ih, kw, incoming_flow_format.interval) @@ -1247,9 +1247,9 @@ def build( name=f"nd_{self.name}", ) n_pool2d.set_oflow_format( - self.oflow_format.t_1st_vld, - self.oflow_format.interval, - self.oflow_format.n_vld, + self._oflow_format.t_1st_vld, + self._oflow_format.interval, + self._oflow_format.n_vld, ) for i in range(kw): @@ -1349,12 +1349,12 @@ def build( kh, kw = self.kernel_size _, ow = self.shape_out - self.oflow_format = SemiFoldedDataFlowFormat( + self._oflow_format = SemiFoldedDataFlowFormat( incoming_flow_format.t_at_n(kw - self.padding[0]), incoming_flow_format.interval * self.stride[1], ow, ) - twe = 1 + self.oflow_format.t_last_vld + twe = 1 + self._oflow_format.t_last_vld if build_options.get("check_before_compile"): self._input_buffer_len_check(cin, ih, kw, incoming_flow_format.interval) @@ -1380,16 +1380,16 @@ def build( n_pool2d = ANNNeuron( self.shape_out, delay=self.delay_relative, - bit_trunc=bit_trunc, + bit_trunc=bt, tick_wait_start=self.tick_wait_start + 1, tick_wait_end=twe, keep_shape=self.keep_shape, name=f"nd_{self.name}", ) n_pool2d.set_oflow_format( - self.oflow_format.t_1st_vld, - self.oflow_format.interval, - self.oflow_format.n_vld, + self._oflow_format.t_1st_vld, + self._oflow_format.interval, + self._oflow_format.n_vld, ) for i in range(kw): diff --git a/paibox/components/neuron/base.py b/paibox/components/neuron/base.py index 635ac3f6..1b96ffa7 100644 --- a/paibox/components/neuron/base.py +++ b/paibox/components/neuron/base.py @@ -493,7 +493,7 @@ def __init__( self._twe = arg_check_non_neg(tick_wait_end, "'tick_wait_end'") self._uf = arg_check_pos(unrolling_factor, "'unrolling_factor'") # Default dataflow is infinite and continuous, starting at tws+0. - self.oflow_format = DataFlowFormat(0, is_local_time=True) + self._oflow_format = DataFlowFormat(0, is_local_time=True) def __len__(self) -> int: return self._n_neuron @@ -540,19 +540,21 @@ def set_oflow_format( format_type: type[DataFlowFormat] = DataFlowFormat, ) -> None: """Set the attributes of output dataflow format by given arguments.""" - if hasattr(self, "oflow_format"): + if hasattr(self, "_oflow_format"): _t_1st_vld = ( - t_1st_vld if isinstance(t_1st_vld, int) else self.oflow_format.t_1st_vld + t_1st_vld + if isinstance(t_1st_vld, int) + else self._oflow_format.t_1st_vld ) _interval = ( arg_check_pos(interval, "interval") if isinstance(interval, int) - else self.oflow_format.interval + else self._oflow_format.interval ) _n_vld = ( arg_check_non_neg(n_vld, "n_vld") if isinstance(n_vld, int) - else self.oflow_format.n_vld + else self._oflow_format.n_vld ) self._assign_flow_format(_t_1st_vld, _interval, _n_vld) else: @@ -562,17 +564,17 @@ def set_oflow_format( and isinstance(t_1st_vld, int) ): raise ValueError( - "if 'oflow_format' is not set, 't_1st_vld', 'interval' & 'n_vld' must be set." + "if '_oflow_format' is not set, 't_1st_vld', 'interval' & 'n_vld' must be set." ) - self.oflow_format = format_type(t_1st_vld, interval, n_vld) - self.oflow_format._check_after_assign(self.tick_wait_start, self.end_tick) + self._oflow_format = format_type(t_1st_vld, interval, n_vld) + self._oflow_format._check_after_assign(self.tick_wait_start, self.end_tick) def _assign_flow_format(self, t_1st_vld: int, intv: int, n_vld: int) -> None: - self.oflow_format.t_1st_vld = t_1st_vld - self.oflow_format.interval = intv - self.oflow_format.n_vld = n_vld - self.oflow_format._check_after_assign(self.tick_wait_start, self.end_tick) + self._oflow_format.t_1st_vld = t_1st_vld + self._oflow_format.interval = intv + self._oflow_format.n_vld = n_vld + self._oflow_format._check_after_assign(self.tick_wait_start, self.end_tick) def __copy__(self) -> "Neuron": """Same as `__deepcopy__`.""" diff --git a/paibox/network.py b/paibox/network.py index 6db83afd..4f6dc190 100644 --- a/paibox/network.py +++ b/paibox/network.py @@ -113,7 +113,7 @@ def build_modules( # TODO for the case of the ResBlock, the `pred_dg_semi_ops` will be used. if isinstance(m, _SemiFoldedModule): generated[m] = m.build(self, last_vld_output_attr, **build_options) - last_vld_output_attr = m.oflow_format + last_vld_output_attr = m._oflow_format else: generated[m] = m.build(self, **build_options) diff --git a/tests/components/test_functional.py b/tests/components/test_functional.py index 4aae8961..a738b460 100644 --- a/tests/components/test_functional.py +++ b/tests/components/test_functional.py @@ -982,7 +982,7 @@ def test_Conv2dSemiFolded_FC_ChainNet( semi_vld_out_intv0 = 1 t_1st_vld_data0 = 0 # The interval & the time of the first valid data of the current layers - semi_vld_out_intv = [m.oflow_format.interval for m in semi_folded_modules] + semi_vld_out_intv = [m._oflow_format.interval for m in semi_folded_modules] t_1st_vld_data = [0] * n_conv for i in range(n_conv): if i == 0: @@ -1049,7 +1049,7 @@ def test_Conv2dSemiFolded_FC_ChainNet( i_conv ].tick_wait_start + conv2d_list[ i_conv - ].oflow_format.t_at_idx( + ]._oflow_format.t_at_idx( i ) @@ -1060,12 +1060,12 @@ def test_Conv2dSemiFolded_FC_ChainNet( assert np.array_equal( expected_fc_t, sim1.data[probe_linear][ - linear.tick_wait_start + linear.oflow_format.t_last_vld + linear.tick_wait_start + linear._oflow_format.t_last_vld ], ) assert ( - linear.oflow_format.get_global_t_1st_vld(linear.tick_wait_start) - == linear.tick_wait_start + linear.oflow_format.t_last_vld + linear._oflow_format.get_global_t_1st_vld(linear.tick_wait_start) + == linear.tick_wait_start + linear._oflow_format.t_last_vld ) @pytest.mark.parametrize( @@ -1191,7 +1191,7 @@ def test_Pool2dSemiFolded_FC_ChainNet( semi_vld_out_intv0 = 1 t_1st_vld_data0 = 0 # The interval & the time of the first valid data of the current layers - semi_vld_out_intv = [m.oflow_format.interval for m in semi_folded_modules] + semi_vld_out_intv = [m._oflow_format.interval for m in semi_folded_modules] t_1st_vld_data = [0] * n_pool for i in range(n_pool): if i == 0: @@ -1245,7 +1245,7 @@ def test_Pool2dSemiFolded_FC_ChainNet( i_pool ].tick_wait_start + pool2d_list[ i_pool - ].oflow_format.t_at_idx( + ]._oflow_format.t_at_idx( i ) @@ -1256,13 +1256,13 @@ def test_Pool2dSemiFolded_FC_ChainNet( assert np.array_equal( expected_fc_t, sim1.data[probe_linear][ - linear.tick_wait_start + linear.oflow_format.t_last_vld + linear.tick_wait_start + linear._oflow_format.t_last_vld ], ) assert ( - linear.oflow_format.get_global_t_1st_vld(linear.tick_wait_start) - == linear.tick_wait_start + linear.oflow_format.t_last_vld + linear._oflow_format.get_global_t_1st_vld(linear.tick_wait_start) + == linear.tick_wait_start + linear._oflow_format.t_last_vld ) @pytest.mark.parametrize( From ec1a83d37dc091d434e367b375f8089887f7424b Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Tue, 3 Dec 2024 16:43:05 +0800 Subject: [PATCH 170/187] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20remove=20bias?= =?UTF-8?q?=20check=20in=20semi-folded=20ops.=20Add=20`bit=5Ftrunc`=20for?= =?UTF-8?q?=20semi-folded=20pooling=20ops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/components/_modules.py | 25 +++++++++---------------- paibox/components/functional.py | 27 ++++++++++++++------------- paibox/components/neuron/base.py | 2 +- 3 files changed, 24 insertions(+), 30 deletions(-) diff --git a/paibox/components/_modules.py b/paibox/components/_modules.py index 907f0a95..b7798402 100644 --- a/paibox/components/_modules.py +++ b/paibox/components/_modules.py @@ -1,3 +1,4 @@ +import math import typing from typing import Literal, Optional, Union @@ -5,9 +6,8 @@ from paicorelib import TM, HwConfig from paibox.base import DataFlowFormat, NeuDyn, NodeList -from paibox.exceptions import ResourceError, ShapeError +from paibox.exceptions import ResourceError from paibox.types import ( - LEAK_V_DTYPE, NEUOUT_U8_DTYPE, WEIGHT_DTYPE, DataType, @@ -216,26 +216,19 @@ def __init__( neuron_s: the input neuron. out_features: the output shape. weights: the weight matrix. - bias: It can be a scalar or an array of the same size as the output. + bias: it can be a scalar or an array of the same size as the output. bit_trunc: the bit truncation position. By default, bits 7 to 0 are truncated. """ self.weights = weights self.bit_trunc = bit_trunc - _shape_out = as_shape(out_features) - - if isinstance(bias, np.ndarray): - _bias = np.atleast_1d(bias).astype(LEAK_V_DTYPE) - if _bias.shape != _shape_out: - raise ShapeError( - f"the shape of bias {_bias.shape} does not match the shape of output {_shape_out}." - ) - else: - _bias = int(bias) - - self.bias = _bias + self.bias = bias super().__init__( - neuron_s, shape_out=_shape_out, keep_shape=keep_shape, name=name, **kwargs + neuron_s, + shape_out=as_shape(out_features), + keep_shape=keep_shape, + name=name, + **kwargs, ) diff --git a/paibox/components/functional.py b/paibox/components/functional.py index ec0bb1c8..92d31f92 100644 --- a/paibox/components/functional.py +++ b/paibox/components/functional.py @@ -996,7 +996,7 @@ def __init__( kernel: convolution kernel in (O,I,H,W) order. stride: the step size of the kernel sliding. It can be a scalar or a tuple of 2 integers. padding: the amount of zero-padding applied to the input. It can be a scalar or a tuple of 2 integers. - bias: It can be a scalar or an array of the same size as the output. + bias: it can be a scalar or an array of the same size as the output. bit_trunc: the bit truncation position. By default, bits 7 to 0 are truncated. """ if kernel.ndim != self._spatial_ndim + 2: @@ -1023,17 +1023,7 @@ def __init__( raise ShapeError(f"the channels mismatch: {in_ch} != {cin}.") _shape_out = (cout, out_h) - - if isinstance(bias, np.ndarray): - _bias = np.atleast_1d(bias).astype(LEAK_V_DTYPE) - if _bias.shape != _shape_out: - raise ShapeError( - f"the shape of bias {_bias.shape} does not match the shape of output {_shape_out}." - ) - else: - _bias = int(bias) - - self.bias = _bias + self.bias = bias super().__init__( neuron_s, shape_out=_shape_out, keep_shape=keep_shape, name=name, **kwargs @@ -1173,6 +1163,7 @@ def __init__( neuron_s: Union[NeuDyn, InputProj], kernel_size: _Size2Type, stride: Optional[_Size2Type] = None, + bit_trunc: int = 8, *, keep_shape: bool = False, name: Optional[str] = None, @@ -1184,6 +1175,7 @@ def __init__( neuron_s: the input neuron to be pooled. kernel_size: the size of the window to take a max over. stride: the stride of the window. Default value is `kernel_size`. + bit_trunc: the bit truncation position. By default, bits 7 to 0 are truncated. NOTE: Since the semi-folded max pooling in the ANN mode is implemented using comparators, it is not \ possible to use negative padding layer to eliminate the incorrect results of the padding part. @@ -1195,6 +1187,7 @@ def __init__( _stride = _pair(stride) self.stride = _stride + self.bit_trunc = bit_trunc assert len(neuron_s.shape_out) == 2 in_ch, in_h = neuron_s.shape_out @@ -1239,6 +1232,7 @@ def build( n_pool2d = ANNNeuron( self.shape_out, + bit_trunc=self.bit_trunc, delay=self.delay_relative, tick_wait_start=self.tick_wait_start + 1, tick_wait_end=twe, @@ -1296,6 +1290,7 @@ def __init__( kernel_size: _Size2Type, stride: Optional[_Size2Type] = None, padding: _Size2Type = 0, + bit_trunc: Optional[int] = None, *, keep_shape: bool = False, name: Optional[str] = None, @@ -1309,6 +1304,7 @@ def __init__( stride: the stride of the window. Default value is `kernel_size`. padding: the amount of zero-padding applied to the input. It can be a scalar or a tuple of 2 \ integers. + bit_trunc: the bit truncation position. By default, bit_trunc = 8 + ksize.bit_length() - 1. """ self.kernel_size = _pair(kernel_size) if stride is None: @@ -1318,6 +1314,7 @@ def __init__( self.stride = _stride self.padding = _pair(padding) + self.bit_trunc = bit_trunc assert len(neuron_s.shape_out) == 2 in_ch, in_h = neuron_s.shape_out @@ -1370,7 +1367,11 @@ def build( # 3. The alternative is bit_tunc=16 for this layer & w*16/9 for the next layer? # NOTE: The resulting linear transformation of weights of the next layer needs to be considered # during quantization. - bit_trunc = 8 + (kh * kw).bit_length() - 1 + bt = ( + self.bit_trunc + if isinstance(self.bit_trunc, int) + else 8 + (kh * kw).bit_length() - 1 + ) n_delays = NodeList() n_neg_padding = NodeList() diff --git a/paibox/components/neuron/base.py b/paibox/components/neuron/base.py index 1b96ffa7..8498c699 100644 --- a/paibox/components/neuron/base.py +++ b/paibox/components/neuron/base.py @@ -160,7 +160,7 @@ def __init__( if bit_truncation > BIT_TRUNCATE_MAX: raise ValueError( - f"'bit_truncation' should be less than or equal to {BIT_TRUNCATE_MAX}." + f"'bit_truncation' should be less than or equal to {BIT_TRUNCATE_MAX}, but got {bit_truncation}." ) def _neuronal_charge( From 5d2dfdd8a820dbee1ab76407b1c7135d201b9a49 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Wed, 4 Dec 2024 10:19:36 +0800 Subject: [PATCH 171/187] =?UTF-8?q?=F0=9F=A4=96=20fix=20workflows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/codecov.yml | 3 +++ .pre-commit-config.yaml | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 35d36c57..1029dada 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -24,6 +24,9 @@ jobs: matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] os: [ubuntu-latest, windows-latest] + exclude: # see https://github.com/python/cpython/issues/125842 + - python-version: "3.13" + os: windows-latest runs-on: ${{ matrix.os }} steps: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5b4b101..bd6e1764 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,6 @@ repos: - id: check-symlinks - id: check-merge-conflict - id: mixed-line-ending - args: [--pytest-test-first] - id: requirements-txt-fixer - id: pretty-format-json args: [--autofix, --indent 2] From 9be2243eb4578eb5e1a548b7a4bf4a3f3b03f19a Mon Sep 17 00:00:00 2001 From: yang1556 <2689162957@qq.com> Date: Tue, 3 Dec 2024 16:58:17 +0800 Subject: [PATCH 172/187] support grouped conv --- paibox/components/functional.py | 20 +++-- paibox/components/synapses/base.py | 5 +- paibox/components/synapses/conv_utils.py | 107 +++++++++++++++++------ paibox/components/synapses/transforms.py | 19 ++-- 4 files changed, 111 insertions(+), 40 deletions(-) diff --git a/paibox/components/functional.py b/paibox/components/functional.py index 92d31f92..98d690e0 100644 --- a/paibox/components/functional.py +++ b/paibox/components/functional.py @@ -983,6 +983,7 @@ def __init__( stride: _Size2Type = 1, padding: _Size2Type = 0, bias: DataType = 0, + groups: int = 1, bit_trunc: int = 8, *, keep_shape: bool = False, @@ -1007,6 +1008,7 @@ def __init__( self.kernel = kernel self.stride = _pair(stride) self.padding = _pair(padding) + self.groups = groups self.bit_trunc = bit_trunc assert len(neuron_s.shape_out) == 2 @@ -1019,7 +1021,11 @@ def __init__( assert self.padding[0] < kh and self.padding[1] < kw - if in_ch != cin: + if in_ch % groups != 0: + raise ValueError('in_channels must be divisible by groups') + if cout % groups != 0: + raise ValueError('out_channels must be divisible by groups') + if in_ch != groups * cin: raise ShapeError(f"the channels mismatch: {in_ch} != {cin}.") _shape_out = (cout, out_h) @@ -1041,7 +1047,7 @@ def build( # self.source[0].shape_out, "CHW" # ) # self.source[0].shape_change((in_ch, in_h)) - _, ih = self.source[0].shape_out + ic, ih = self.source[0].shape_out _, cin, _, kw = self.kernel.shape _, ow = self.shape_out @@ -1079,7 +1085,7 @@ def build( for i in range(kw): neuron = ANNBypassNeuron( - (cin, ih), + (ic, ih), delay=incoming_flow_format.interval * i + 1, tick_wait_start=self.tick_wait_start, tick_wait_end=twe - incoming_flow_format.interval * i, @@ -1090,7 +1096,7 @@ def build( syn1 = FullConnSyn( self.source[0], neuron, - weights=_delay_mapping_mask(ih, cin), + weights=_delay_mapping_mask(ih, ic), conn_type=ConnType.All2All, name=f"s{i}_delay_{self.name}", ) @@ -1102,6 +1108,7 @@ def build( self.kernel[:, :, :, kw - i - 1], self.stride, self.padding, + self.groups, "OIL", name=f"s{i}_{self.name}", ) @@ -1113,7 +1120,7 @@ def build( if incoming_flow_format.t_1st_vld > 0: for p in range(self.padding[0]): neuron = ANNBypassNeuron( - (cin, ih), + (ic, ih), delay=1 + incoming_flow_format.interval * (kw - 1 - p), tick_wait_start=self.tick_wait_start, tick_wait_end=incoming_flow_format.t_1st_vld, @@ -1125,7 +1132,7 @@ def build( syn1 = FullConnSyn( self.source[0], neuron, - weights=_delay_mapping_mask(ih, cin), + weights=_delay_mapping_mask(ih, ic), conn_type=ConnType.All2All, name=f"s{p}_pad_{self.name}", ) @@ -1137,6 +1144,7 @@ def build( -(self.kernel[:, :, :, p]), self.stride, self.padding, + self.groups, "OIL", name=f"neg_s{p}_{self.name}", ) diff --git a/paibox/components/synapses/base.py b/paibox/components/synapses/base.py index 01fd912f..edea85e7 100644 --- a/paibox/components/synapses/base.py +++ b/paibox/components/synapses/base.py @@ -345,6 +345,7 @@ def __init__( kernel: np.ndarray, stride: tuple[int, int], padding: tuple[int, int], + groups: int, order: _KOrder3d, name: Optional[str] = None, ) -> None: @@ -367,7 +368,7 @@ def __init__( in_ch, in_h = source.shape_out out_h = (in_h + 2 * padding[0] - kernel_h) // stride[0] + 1 - if in_ch != in_channels: + if in_ch != groups * in_channels: raise ShapeError(f"input channels mismatch: {in_ch} != {in_channels}.") if (_output_size := out_channels * out_h) != dest.num_in: @@ -377,7 +378,7 @@ def __init__( ) self.comm = Conv2dSemiFoldedForward( - (in_ch, in_h), (out_channels, out_h), _kernel, stride, padding + (in_ch, in_h), (out_channels, out_h), _kernel, stride, padding, groups ) diff --git a/paibox/components/synapses/conv_utils.py b/paibox/components/synapses/conv_utils.py index fd3752d9..d8b541fc 100644 --- a/paibox/components/synapses/conv_utils.py +++ b/paibox/components/synapses/conv_utils.py @@ -189,28 +189,47 @@ def _conv2d_semifolded_unroll( kernel: WeightType, stride: Size2Type, padding: Size2Type, + groups: int, ) -> WeightType: - cout, cin, kh = kernel.shape + cout, ck, kh = kernel.shape + cin = groups * ck ih = in_shape[1] + 2 * padding[0] _, oh = out_shape w_np = np.zeros((cin * in_shape[1], cout * oh), dtype=kernel.dtype) - - for i in range(cout): - for j in range(cin): - # Must recreate `w_block` every time because some rows will be deleted. - w_block = np.zeros((ih, oh), dtype=kernel.dtype) - for k in range(oh): - w_block[k * stride[1] : k * stride[1] + kh, k] = kernel[i, j, :] - - if padding[0] > 0: # H direction - w_block = np.delete( - w_block, - np.hstack((np.arange(padding[0]), np.arange(ih - padding[0], ih))), - axis=0, + for g in range(groups): + for i in range(cout//groups): + for j in range(ck): + # Must recreate `w_block` every time because some rows will be deleted. + w_block = np.zeros((ih, oh), dtype=kernel.dtype) + for k in range(oh): + w_block[k * stride[1] : k * stride[1] + kh, k] = kernel[g*cout//groups+i, j, :] + if padding[0] > 0: # H direction + w_block = np.delete( + w_block, + np.hstack((np.arange(padding[0]), np.arange(ih - padding[0], ih))), + axis=0, + ) + w_np[g*ck*in_shape[1] + j * in_shape[1] : g*ck*in_shape[1]+(j + 1) * in_shape[1], g*oh*cout//groups+i * oh :g*oh*cout//groups+(i + 1) * oh] = ( + w_block ) - w_np[j * in_shape[1] : (j + 1) * in_shape[1], i * oh : (i + 1) * oh] = ( - w_block - ) + + + # for i in range(cout): + # for j in range(cin): + # # Must recreate `w_block` every time because some rows will be deleted. + # w_block = np.zeros((ih, oh), dtype=kernel.dtype) + # for k in range(oh): + # w_block[k * stride[1] : k * stride[1] + kh, k] = kernel[i, j, :] + + # if padding[0] > 0: # H direction + # w_block = np.delete( + # w_block, + # np.hstack((np.arange(padding[0]), np.arange(ih - padding[0], ih))), + # axis=0, + # ) + # w_np[j * in_shape[1] : (j + 1) * in_shape[1], i * oh : (i + 1) * oh] = ( + # w_block + # ) return w_np @@ -252,29 +271,63 @@ def _conv2d_faster( kernel: WeightType, stride: Size2Type, padding: Size2Type, + groups: int = 1, # fm_order: str, ) -> SynOutType: """Faster 2d convolution.""" - cout, _, kh, kw = kernel.shape # (O, I, H, W) + cout, cin, kh, kw = kernel.shape # (O, I, H, W) + if cout % groups != 0: + raise ValueError("Output channels must be divisible by groups.") + + # 计算每个组的通道数 + cin_per_group = cin + cout_per_group = cout // groups + # 将输入张量进行填充 x_padded = np.pad( x_chw, ((0, 0), (padding[0], padding[0]), (padding[1], padding[1])), ) - # kernel: (cout, cin, kh, kw) -> (cout, cin*kh*kw) - col_kernel = kernel.reshape(cout, -1) + # 用于存储最终输出 + out = np.zeros((cout, *out_shape), dtype=np.int64) - # padded: (cin, xh+2*p[0]-kh, xw+2*p[1]-kw) -> (oh*ow, cin*kh*kw) - col_fm = _2d_im2col(x_padded, out_shape[0], out_shape[1], kh, kw, stride) - # out = np.zeros((cout,) + out_shape, dtype=np.int64) - # (oh*ow, cin*kh*kw) * (cout, cin*kh*kw)^T = (oh*ow, cout) - out = col_fm @ col_kernel.T # + self.bias - # (oh*ow, cout) -> (cout, oh*ow) -> (cout, oh, ow) - out = out.T.reshape((cout,) + out_shape) + for g in range(groups): + # 获取当前组的输入和卷积核 + x_group = x_padded[g * cin_per_group:(g + 1) * cin_per_group, :, :] + kernel_group = kernel[g * cout_per_group:(g + 1) * cout_per_group, :, :, :] + + # 重塑卷积核以进行矩阵乘法 + col_kernel = kernel_group.reshape(cout_per_group, -1) + + # 转换当前组的填充图像为列格式 + col_fm = _2d_im2col(x_group, out_shape[0], out_shape[1], kh, kw, stride) + + # 进行矩阵乘法 + out_group = col_fm @ col_kernel.T + # 将组输出重塑并合并到最终输出中 + out[g * cout_per_group:(g + 1) * cout_per_group, :] = out_group.T.reshape((cout_per_group, *out_shape)) return out.astype(VOLTAGE_DTYPE) + # x_padded = np.pad( + # x_chw, + # ((0, 0), (padding[0], padding[0]), (padding[1], padding[1])), + # ) + + # # kernel: (cout, cin, kh, kw) -> (cout, cin*kh*kw) + # col_kernel = kernel.reshape(cout, -1) + + # # padded: (cin, xh+2*p[0]-kh, xw+2*p[1]-kw) -> (oh*ow, cin*kh*kw) + # col_fm = _2d_im2col(x_padded, out_shape[0], out_shape[1], kh, kw, stride) + # # out = np.zeros((cout,) + out_shape, dtype=np.int64) + # # (oh*ow, cin*kh*kw) * (cout, cin*kh*kw)^T = (oh*ow, cout) + # out = col_fm @ col_kernel.T # + self.bias + # # (oh*ow, cout) -> (cout, oh*ow) -> (cout, oh, ow) + # out = out.T.reshape((cout,) + out_shape) + + # return out.astype(VOLTAGE_DTYPE) + def _convtranspose1d_unroll( in_shape: Size1Type, diff --git a/paibox/components/synapses/transforms.py b/paibox/components/synapses/transforms.py index d8448fef..721012df 100644 --- a/paibox/components/synapses/transforms.py +++ b/paibox/components/synapses/transforms.py @@ -399,10 +399,19 @@ def connectivity(self): class Conv2dSemiFoldedForward(_ConvNdForward): - in_shape: Size2Type - out_shape: Size2Type - stride: Size2Type - padding: Size2Type + def __init__( + self, + in_shape: SizeAnyType, + out_shape: SizeAnyType, + kernel: np.ndarray, + stride: _SizeAnyType = 0, + padding: _SizeAnyType = 0, + groups: int = 1, + output_padding: _SizeAnyType = 0, + ) -> None: + self.groups = groups + super().__init__(in_shape, out_shape, kernel, stride, padding, output_padding) + def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: return x @ self.connectivity @@ -410,7 +419,7 @@ def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: @property def connectivity(self): return _conv2d_semifolded_unroll( - self.in_shape, self.out_shape, self.weights, self.stride, self.padding + self.in_shape, self.out_shape, self.weights, self.stride, self.padding, self.groups ) From 5a67041b7a6c3aba94a41ccda0381e371520d3f7 Mon Sep 17 00:00:00 2001 From: yang1556 <2689162957@qq.com> Date: Tue, 3 Dec 2024 16:58:28 +0800 Subject: [PATCH 173/187] add some group tests --- tests/components/test_functional.py | 74 +++++++++++++++++++++-------- tests/shared_networks.py | 5 +- 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/tests/components/test_functional.py b/tests/components/test_functional.py index a738b460..4e4d2765 100644 --- a/tests/components/test_functional.py +++ b/tests/components/test_functional.py @@ -831,20 +831,35 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): mapper.export(fp=ensure_dump_dir) @pytest.mark.parametrize( - "ishape_chw, n_conv, kshape_oihw, stride, padding, out_features", + "ishape_chw, n_conv, kshape_oihw, stride, padding, out_features, groups", [ # n_conv = 1 - ((3, 11, 11), 1, [(1, 3, 3, 3)], [1], [1], (10,)), - ((3, 12, 12), 1, [(12, 3, 3, 3)], [(1, 1)], [2], (10,)), - ((8, 12, 12), 1, [(16, 8, 3, 3)], [(2, 2)], [2], (10,)), - ((8, 12, 12), 1, [(16, 8, 4, 4)], [2], [1], (10,)), - ((4, 12, 12), 1, [(8, 4, 3, 3)], [1], [0], (4, 2)), - ((4, 24, 24), 1, [(8, 4, 3, 3)], [2], [0], 10), - ((12, 12, 12), 1, [(6, 12, 3, 3)], [1], [0], (3, 3)), - ((4, 24, 24), 1, [(8, 4, 4, 4)], [2], [0], (10,)), - ((8, 32, 32), 1, [(4, 8, 3, 3)], [2], [0], 10), + ((3, 11, 11), 1, [(1, 3, 3, 3)], [1], [1], (10,), [1,]), + ((3, 12, 12), 1, [(12, 3, 3, 3)], [(1, 1)], [2], (10,), [1,]), + ((8, 12, 12), 1, [(16, 8, 3, 3)], [(2, 2)], [2], (10,), [1,]), + ((8, 12, 12), 1, [(16, 8, 4, 4)], [2], [1], (10,), [1,]), + ((4, 12, 12), 1, [(8, 4, 3, 3)], [1], [0], (4, 2), [1,]), + ((4, 24, 24), 1, [(8, 4, 3, 3)], [2], [0], 10, [1,]), + ((12, 12, 12), 1, [(6, 12, 3, 3)], [1], [0], (3, 3), [1,]), + ((4, 24, 24), 1, [(8, 4, 4, 4)], [2], [0], (10,), [1,]), + ((8, 32, 32), 1, [(4, 8, 3, 3)], [2], [0], 10, [1,]), + # group + ((8, 32, 32), 1, [(4, 4, 3, 3)], [2], [0], 10, [2,]), + ((8, 32, 32), 1, [(8, 1, 3, 3)], [2], [0], 10, [8,]), # n_conv = 2 - ((1, 5, 5), 2, [(1, 1, 3, 3), (1, 1, 3, 3)], [(1, 1), (1, 1)], [2, 2], 10), + ## group + ((4, 5, 5), 2, [(8, 1, 3, 3), (8, 1, 3, 3)], [(1, 1), (1, 1)], [2, 2], 10, [4, 8]), + ( + (4, 32, 32), + 2, + [(8, 2, 3, 3), (12, 4, 4, 4)], + [(2, 2), (2, 2)], + [1, 1], + 10, + [2, 2] + ), + ## + ((1, 5, 5), 2, [(1, 1, 3, 3), (1, 1, 3, 3)], [(1, 1), (1, 1)], [2, 2], 10, [1, 1]), ( (4, 32, 32), 2, @@ -852,6 +867,7 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): [(2, 2), (2, 2)], [1, 1], 10, + [1, 1] ), ( (4, 32, 32), @@ -860,13 +876,25 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): [(2, 2), (1, 1)], [1, 2], 10, + [1, 1] ), - ((1, 32, 32), 2, [(1, 1, 3, 3), (1, 1, 3, 3)], [2, 2], [2, 2], 10), - ((1, 32, 32), 2, [(1, 1, 4, 4), (1, 1, 4, 4)], [1, 2], [2, 2], 10), - ((1, 32, 32), 2, [(1, 1, 4, 4), (1, 1, 4, 4)], [2, 2], [2, 2], 10), - ((1, 24, 24), 2, [(1, 1, 3, 3), (1, 1, 4, 4)], [1, 2], [2, 1], 10), - ((1, 24, 24), 2, [(1, 1, 3, 3), (1, 1, 3, 3)], [2, 2], [2, 2], 10), + ((1, 32, 32), 2, [(1, 1, 3, 3), (1, 1, 3, 3)], [2, 2], [2, 2], 10, [1, 1]), + ((1, 32, 32), 2, [(1, 1, 4, 4), (1, 1, 4, 4)], [1, 2], [2, 2], 10, [1, 1]), + ((1, 32, 32), 2, [(1, 1, 4, 4), (1, 1, 4, 4)], [2, 2], [2, 2], 10, [1, 1]), + ((1, 24, 24), 2, [(1, 1, 3, 3), (1, 1, 4, 4)], [1, 2], [2, 1], 10, [1, 1]), + ((1, 24, 24), 2, [(1, 1, 3, 3), (1, 1, 3, 3)], [2, 2], [2, 2], 10, [1, 1]), # n_conv = 3 + ## group + ( + (4, 32, 32), + 3, + [(8, 1, 3, 3), (8, 1, 3, 3), (4, 2, 2, 2)], + [1, 1, 1], + [1, 1, 1], + 3, + [4, 8, 4] + ), + ## ( (4, 32, 32), 3, @@ -874,6 +902,7 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): [1, 1, 1], [1, 1, 1], 3, + [1, 1, 1] ), ( (3, 32, 32), @@ -882,6 +911,7 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): [1, 1, 1], [1, 0, 1], 10, + [1, 1, 1] ), ( (1, 224, 224), @@ -890,6 +920,7 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): [2, 2, 2], [3, 2, 1], 10, + [1, 1, 1] ), ( (3, 32, 32), @@ -898,6 +929,7 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): [1, 2, 1], [1, 0, 1], 10, + [1, 1, 1] ), # n_conv = 5 ( @@ -907,7 +939,8 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): [1, 2, 1, 2, 1], [1, 0, 1, 0, 1], 10, - ), + [1, 1, 1, 1, 1] + ), ], ) def test_Conv2dSemiFolded_FC_ChainNet( @@ -918,14 +951,14 @@ def test_Conv2dSemiFolded_FC_ChainNet( stride, padding, out_features, + groups, fixed_rng: np.random.Generator, ): """Test the network with N semi-folded conv2d + 1 semi-folded linear.""" from tests.shared_networks import Conv2dSemiFolded_FC_ChainNetN assert n_conv == len(kshape_oihw) == len(stride) - assert ishape_chw[0] == kshape_oihw[0][1] - + assert ishape_chw[0] == groups[0]*kshape_oihw[0][1] kernels = [] strides = [] paddings = [] @@ -960,7 +993,7 @@ def test_Conv2dSemiFolded_FC_ChainNet( ) net1 = Conv2dSemiFolded_FC_ChainNetN( - ishape_chw[:2], kernels, strides, paddings, out_features, fc_weight + ishape_chw[:2], kernels, strides, paddings, out_features, fc_weight, groups ) # `net1.conv_list` will be removed in `build_fmodule` conv2d_list = net1.conv_list.copy() @@ -1028,6 +1061,7 @@ def test_Conv2dSemiFolded_FC_ChainNet( kernels[i_conv], strides[i_conv], paddings[i_conv], + groups[i_conv], ) ) diff --git a/tests/shared_networks.py b/tests/shared_networks.py index 6a0d5d8e..992f2511 100644 --- a/tests/shared_networks.py +++ b/tests/shared_networks.py @@ -255,13 +255,13 @@ def __init__(self, shape, axes): class Conv2dSemiFolded_FC_ChainNetN(pb.DynSysGroup): - def __init__(self, shape, kernels, strides, paddings, out_features, weight): + def __init__(self, shape, kernels, strides, paddings, out_features, weight, groups): super().__init__() self.i1 = pb.InputProj(input=_out_bypass1, shape_out=shape) self.conv_list = NodeList() - for i, (kernel, stride, padding) in enumerate(zip(kernels, strides, paddings)): + for i, (kernel, stride, padding, g) in enumerate(zip(kernels, strides, paddings, groups)): self.conv_list.append( pb.Conv2dSemiFolded( self.conv_list[-1] if i > 0 else self.i1, @@ -269,6 +269,7 @@ def __init__(self, shape, kernels, strides, paddings, out_features, weight): stride, padding, tick_wait_start=1 + 2 * i, + groups=g, ) ) From bedb3b5777f2869e1e08b29b5d58b75ac208dfd6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 09:29:15 +0000 Subject: [PATCH 174/187] :rotating_light: auto fix by pre-commit hooks --- paibox/components/functional.py | 4 +- paibox/components/synapses/conv_utils.py | 30 ++-- paibox/components/synapses/transforms.py | 12 +- tests/components/test_functional.py | 174 +++++++++++++++++++---- tests/shared_networks.py | 4 +- 5 files changed, 183 insertions(+), 41 deletions(-) diff --git a/paibox/components/functional.py b/paibox/components/functional.py index 98d690e0..a6e33f94 100644 --- a/paibox/components/functional.py +++ b/paibox/components/functional.py @@ -1022,9 +1022,9 @@ def __init__( assert self.padding[0] < kh and self.padding[1] < kw if in_ch % groups != 0: - raise ValueError('in_channels must be divisible by groups') + raise ValueError("in_channels must be divisible by groups") if cout % groups != 0: - raise ValueError('out_channels must be divisible by groups') + raise ValueError("out_channels must be divisible by groups") if in_ch != groups * cin: raise ShapeError(f"the channels mismatch: {in_ch} != {cin}.") diff --git a/paibox/components/synapses/conv_utils.py b/paibox/components/synapses/conv_utils.py index d8b541fc..0c72cd0f 100644 --- a/paibox/components/synapses/conv_utils.py +++ b/paibox/components/synapses/conv_utils.py @@ -197,22 +197,30 @@ def _conv2d_semifolded_unroll( _, oh = out_shape w_np = np.zeros((cin * in_shape[1], cout * oh), dtype=kernel.dtype) for g in range(groups): - for i in range(cout//groups): + for i in range(cout // groups): for j in range(ck): # Must recreate `w_block` every time because some rows will be deleted. w_block = np.zeros((ih, oh), dtype=kernel.dtype) for k in range(oh): - w_block[k * stride[1] : k * stride[1] + kh, k] = kernel[g*cout//groups+i, j, :] + w_block[k * stride[1] : k * stride[1] + kh, k] = kernel[ + g * cout // groups + i, j, : + ] if padding[0] > 0: # H direction w_block = np.delete( w_block, - np.hstack((np.arange(padding[0]), np.arange(ih - padding[0], ih))), + np.hstack( + (np.arange(padding[0]), np.arange(ih - padding[0], ih)) + ), axis=0, ) - w_np[g*ck*in_shape[1] + j * in_shape[1] : g*ck*in_shape[1]+(j + 1) * in_shape[1], g*oh*cout//groups+i * oh :g*oh*cout//groups+(i + 1) * oh] = ( - w_block - ) - + w_np[ + g * ck * in_shape[1] + + j * in_shape[1] : g * ck * in_shape[1] + + (j + 1) * in_shape[1], + g * oh * cout // groups + + i * oh : g * oh * cout // groups + + (i + 1) * oh, + ] = w_block # for i in range(cout): # for j in range(cin): @@ -294,8 +302,8 @@ def _conv2d_faster( for g in range(groups): # 获取当前组的输入和卷积核 - x_group = x_padded[g * cin_per_group:(g + 1) * cin_per_group, :, :] - kernel_group = kernel[g * cout_per_group:(g + 1) * cout_per_group, :, :, :] + x_group = x_padded[g * cin_per_group : (g + 1) * cin_per_group, :, :] + kernel_group = kernel[g * cout_per_group : (g + 1) * cout_per_group, :, :, :] # 重塑卷积核以进行矩阵乘法 col_kernel = kernel_group.reshape(cout_per_group, -1) @@ -307,7 +315,9 @@ def _conv2d_faster( out_group = col_fm @ col_kernel.T # 将组输出重塑并合并到最终输出中 - out[g * cout_per_group:(g + 1) * cout_per_group, :] = out_group.T.reshape((cout_per_group, *out_shape)) + out[g * cout_per_group : (g + 1) * cout_per_group, :] = out_group.T.reshape( + (cout_per_group, *out_shape) + ) return out.astype(VOLTAGE_DTYPE) # x_padded = np.pad( diff --git a/paibox/components/synapses/transforms.py b/paibox/components/synapses/transforms.py index 721012df..cb687eeb 100644 --- a/paibox/components/synapses/transforms.py +++ b/paibox/components/synapses/transforms.py @@ -400,7 +400,7 @@ def connectivity(self): class Conv2dSemiFoldedForward(_ConvNdForward): def __init__( - self, + self, in_shape: SizeAnyType, out_shape: SizeAnyType, kernel: np.ndarray, @@ -408,18 +408,22 @@ def __init__( padding: _SizeAnyType = 0, groups: int = 1, output_padding: _SizeAnyType = 0, - ) -> None: + ) -> None: self.groups = groups super().__init__(in_shape, out_shape, kernel, stride, padding, output_padding) - def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: return x @ self.connectivity @property def connectivity(self): return _conv2d_semifolded_unroll( - self.in_shape, self.out_shape, self.weights, self.stride, self.padding, self.groups + self.in_shape, + self.out_shape, + self.weights, + self.stride, + self.padding, + self.groups, ) diff --git a/tests/components/test_functional.py b/tests/components/test_functional.py index 4e4d2765..6e86a1ab 100644 --- a/tests/components/test_functional.py +++ b/tests/components/test_functional.py @@ -834,21 +834,139 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): "ishape_chw, n_conv, kshape_oihw, stride, padding, out_features, groups", [ # n_conv = 1 - ((3, 11, 11), 1, [(1, 3, 3, 3)], [1], [1], (10,), [1,]), - ((3, 12, 12), 1, [(12, 3, 3, 3)], [(1, 1)], [2], (10,), [1,]), - ((8, 12, 12), 1, [(16, 8, 3, 3)], [(2, 2)], [2], (10,), [1,]), - ((8, 12, 12), 1, [(16, 8, 4, 4)], [2], [1], (10,), [1,]), - ((4, 12, 12), 1, [(8, 4, 3, 3)], [1], [0], (4, 2), [1,]), - ((4, 24, 24), 1, [(8, 4, 3, 3)], [2], [0], 10, [1,]), - ((12, 12, 12), 1, [(6, 12, 3, 3)], [1], [0], (3, 3), [1,]), - ((4, 24, 24), 1, [(8, 4, 4, 4)], [2], [0], (10,), [1,]), - ((8, 32, 32), 1, [(4, 8, 3, 3)], [2], [0], 10, [1,]), + ( + (3, 11, 11), + 1, + [(1, 3, 3, 3)], + [1], + [1], + (10,), + [ + 1, + ], + ), + ( + (3, 12, 12), + 1, + [(12, 3, 3, 3)], + [(1, 1)], + [2], + (10,), + [ + 1, + ], + ), + ( + (8, 12, 12), + 1, + [(16, 8, 3, 3)], + [(2, 2)], + [2], + (10,), + [ + 1, + ], + ), + ( + (8, 12, 12), + 1, + [(16, 8, 4, 4)], + [2], + [1], + (10,), + [ + 1, + ], + ), + ( + (4, 12, 12), + 1, + [(8, 4, 3, 3)], + [1], + [0], + (4, 2), + [ + 1, + ], + ), + ( + (4, 24, 24), + 1, + [(8, 4, 3, 3)], + [2], + [0], + 10, + [ + 1, + ], + ), + ( + (12, 12, 12), + 1, + [(6, 12, 3, 3)], + [1], + [0], + (3, 3), + [ + 1, + ], + ), + ( + (4, 24, 24), + 1, + [(8, 4, 4, 4)], + [2], + [0], + (10,), + [ + 1, + ], + ), + ( + (8, 32, 32), + 1, + [(4, 8, 3, 3)], + [2], + [0], + 10, + [ + 1, + ], + ), # group - ((8, 32, 32), 1, [(4, 4, 3, 3)], [2], [0], 10, [2,]), - ((8, 32, 32), 1, [(8, 1, 3, 3)], [2], [0], 10, [8,]), + ( + (8, 32, 32), + 1, + [(4, 4, 3, 3)], + [2], + [0], + 10, + [ + 2, + ], + ), + ( + (8, 32, 32), + 1, + [(8, 1, 3, 3)], + [2], + [0], + 10, + [ + 8, + ], + ), # n_conv = 2 ## group - ((4, 5, 5), 2, [(8, 1, 3, 3), (8, 1, 3, 3)], [(1, 1), (1, 1)], [2, 2], 10, [4, 8]), + ( + (4, 5, 5), + 2, + [(8, 1, 3, 3), (8, 1, 3, 3)], + [(1, 1), (1, 1)], + [2, 2], + 10, + [4, 8], + ), ( (4, 32, 32), 2, @@ -856,10 +974,18 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): [(2, 2), (2, 2)], [1, 1], 10, - [2, 2] + [2, 2], ), ## - ((1, 5, 5), 2, [(1, 1, 3, 3), (1, 1, 3, 3)], [(1, 1), (1, 1)], [2, 2], 10, [1, 1]), + ( + (1, 5, 5), + 2, + [(1, 1, 3, 3), (1, 1, 3, 3)], + [(1, 1), (1, 1)], + [2, 2], + 10, + [1, 1], + ), ( (4, 32, 32), 2, @@ -867,7 +993,7 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): [(2, 2), (2, 2)], [1, 1], 10, - [1, 1] + [1, 1], ), ( (4, 32, 32), @@ -876,7 +1002,7 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): [(2, 2), (1, 1)], [1, 2], 10, - [1, 1] + [1, 1], ), ((1, 32, 32), 2, [(1, 1, 3, 3), (1, 1, 3, 3)], [2, 2], [2, 2], 10, [1, 1]), ((1, 32, 32), 2, [(1, 1, 4, 4), (1, 1, 4, 4)], [1, 2], [2, 2], 10, [1, 1]), @@ -892,7 +1018,7 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): [1, 1, 1], [1, 1, 1], 3, - [4, 8, 4] + [4, 8, 4], ), ## ( @@ -902,7 +1028,7 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): [1, 1, 1], [1, 1, 1], 3, - [1, 1, 1] + [1, 1, 1], ), ( (3, 32, 32), @@ -911,7 +1037,7 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): [1, 1, 1], [1, 0, 1], 10, - [1, 1, 1] + [1, 1, 1], ), ( (1, 224, 224), @@ -920,7 +1046,7 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): [2, 2, 2], [3, 2, 1], 10, - [1, 1, 1] + [1, 1, 1], ), ( (3, 32, 32), @@ -929,7 +1055,7 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): [1, 2, 1], [1, 0, 1], 10, - [1, 1, 1] + [1, 1, 1], ), # n_conv = 5 ( @@ -939,8 +1065,8 @@ def test_Transpose3d_mapping(self, ensure_dump_dir): [1, 2, 1, 2, 1], [1, 0, 1, 0, 1], 10, - [1, 1, 1, 1, 1] - ), + [1, 1, 1, 1, 1], + ), ], ) def test_Conv2dSemiFolded_FC_ChainNet( @@ -958,7 +1084,7 @@ def test_Conv2dSemiFolded_FC_ChainNet( from tests.shared_networks import Conv2dSemiFolded_FC_ChainNetN assert n_conv == len(kshape_oihw) == len(stride) - assert ishape_chw[0] == groups[0]*kshape_oihw[0][1] + assert ishape_chw[0] == groups[0] * kshape_oihw[0][1] kernels = [] strides = [] paddings = [] diff --git a/tests/shared_networks.py b/tests/shared_networks.py index 992f2511..d5ecc26b 100644 --- a/tests/shared_networks.py +++ b/tests/shared_networks.py @@ -261,7 +261,9 @@ def __init__(self, shape, kernels, strides, paddings, out_features, weight, grou self.i1 = pb.InputProj(input=_out_bypass1, shape_out=shape) self.conv_list = NodeList() - for i, (kernel, stride, padding, g) in enumerate(zip(kernels, strides, paddings, groups)): + for i, (kernel, stride, padding, g) in enumerate( + zip(kernels, strides, paddings, groups) + ): self.conv_list.append( pb.Conv2dSemiFolded( self.conv_list[-1] if i > 0 else self.i1, From 8a05f2775f624b376872419d4f6c9be2429567d3 Mon Sep 17 00:00:00 2001 From: Joustrd <17739386485@163.com> Date: Tue, 10 Dec 2024 11:40:41 +0800 Subject: [PATCH 175/187] [feat] support snn grouped conv --- paibox/components/synapses/base.py | 12 +- paibox/components/synapses/conv_utils.py | 188 ++++++++++--------- paibox/components/synapses/synapses.py | 4 + paibox/components/synapses/transforms.py | 18 +- tests/components/synapses/test_synapses.py | 28 +-- tests/components/synapses/test_transforms.py | 104 +++++----- 6 files changed, 201 insertions(+), 153 deletions(-) diff --git a/paibox/components/synapses/base.py b/paibox/components/synapses/base.py index edea85e7..d8721c3f 100644 --- a/paibox/components/synapses/base.py +++ b/paibox/components/synapses/base.py @@ -252,6 +252,7 @@ def __init__( stride: tuple[int], padding: tuple[int], dilation: tuple[int], + groups: int, order: _KOrder3d, name: Optional[str] = None, ) -> None: @@ -268,7 +269,8 @@ def __init__( _kernel = kernel.copy() # O,I,L - out_channels, in_channels, kernel_l = _kernel.shape + out_channels, group_in_channels, kernel_l = _kernel.shape + in_channels = groups * group_in_channels # C,L in_ch, in_l = _fm_ndim1_check(source.shape_out, "CL") out_l = (in_l + 2 * padding[0] - dilation[0] * (kernel_l - 1) - 1) // stride[ @@ -281,7 +283,7 @@ def __init__( if (_output_size := out_channels * out_l) != dest.num_in: raise ShapeError(f"output size mismatch: {_output_size} != {dest.num_in}.") - self.comm = Conv1dForward((in_l,), (out_l,), _kernel, stride, padding) + self.comm = Conv1dForward((in_l,), (out_l,), _kernel, stride, padding, groups=groups) class Conv2dSyn(FullConnectedSyn): @@ -295,6 +297,7 @@ def __init__( stride: tuple[int, int], padding: tuple[int, int], dilation: tuple[int, int], + groups: int, order: _KOrder4d, name: Optional[str] = None, ) -> None: @@ -311,7 +314,8 @@ def __init__( _kernel = kernel.copy() # O,I,H,W - out_channels, in_channels, kernel_h, kernel_w = _kernel.shape + out_channels, group_in_channels, kernel_h, kernel_w = _kernel.shape + in_channels = groups * group_in_channels # C,H,W in_ch, in_h, in_w = _fm_ndim2_check(source.shape_out, "CHW") out_h = (in_h + 2 * padding[0] - dilation[0] * (kernel_h - 1) - 1) // stride[ @@ -331,7 +335,7 @@ def __init__( ) self.comm = Conv2dForward( - (in_h, in_w), (out_h, out_w), _kernel, stride, padding + (in_h, in_w), (out_h, out_w), _kernel, stride, padding, groups=groups ) diff --git a/paibox/components/synapses/conv_utils.py b/paibox/components/synapses/conv_utils.py index 0c72cd0f..7cbd6763 100644 --- a/paibox/components/synapses/conv_utils.py +++ b/paibox/components/synapses/conv_utils.py @@ -67,46 +67,51 @@ def _conv1d_unroll( kernel: WeightType, stride: Size1Type, padding: Size1Type, + groups: int, ) -> WeightType: """Unroll the kernel of 1d convolution into a matrix.""" - cout, cin, kl = kernel.shape + cout, group_cin, kl = kernel.shape + group_cout = cout // groups + kernel = kernel.reshape(groups, group_cout, group_cin, kl) il = in_shape[0] + 2 * padding[0] ol = out_shape[0] # weight unrolled without considering parameter padding : weight unrolled no padding - w_unrolled_np = np.zeros((cin * il, cout * ol), dtype=kernel.dtype) - zeros_image = np.zeros((cin * il, cout, ol), dtype=kernel.dtype) - - for i in range(ol): - zeros_image.fill(0) - for ch_idx in np.ndindex(kernel.shape[:2]): - # [0] -> o_ch, [1] -> i_ch - zeros_image[ - i * stride[0] + ch_idx[1] * il : i * stride[0] + ch_idx[1] * il + kl, - ch_idx[0], - i, - ] = kernel[ch_idx[0], ch_idx[1], :] - - # if fm_order == "CL": - # (cin*il, cout) -> (cout, cin*il) - temp = zeros_image[:, :, i].T - # else: - # # (cin*il, cout) -> (cout, il, cin) - # temp = zeros_image[:, :, i].reshape(cin, il, cout).transpose() + w_unrolled_np = np.zeros((groups, group_cin * il, group_cout * ol), dtype=kernel.dtype) + zeros_image = np.zeros((groups, group_cin * il, group_cout, ol), dtype=kernel.dtype) + for g in range(groups): + for i in range(ol): + zeros_image[g].fill(0) + for ch_idx in np.ndindex(kernel.shape[1:3]): + # [0] -> o_ch, [1] -> i_ch + zeros_image[ + g, + i * stride[0] + ch_idx[1] * il : i * stride[0] + ch_idx[1] * il + kl, + ch_idx[0], + i, + ] = kernel[g, ch_idx[0], ch_idx[1], :] + + # if fm_order == "CL": + # (cin*il, cout) -> (cout, cin*il) + temp = zeros_image[g, :, :, i].T + # else: + # # (cin*il, cout) -> (cout, il, cin) + # temp = zeros_image[:, :, i].reshape(cin, il, cout).transpose() - for o_ch in range(cout): - w_unrolled_np[:, i + o_ch * ol] = temp[o_ch].ravel() + for o_ch in range(group_cout): + w_unrolled_np[g, :, i + o_ch * ol] = temp[o_ch].ravel() # Remove the part of the padding in the w_unrolled_no_padding # That is, remove useless weight in the w_unrolled_no_padding nil = in_shape[0] - w_unrolled = np.zeros((cin * nil, cout * ol), dtype=kernel.dtype) - for i in range(cin): - w_unrolled[i * nil : i * nil + nil, :] = w_unrolled_np[ - i * il + padding[0] : i * il + il - padding[0], : - ] + w_unrolled = np.zeros((groups, group_cin * nil, group_cout * ol), dtype=kernel.dtype) + for g in range(groups): + for i in range(group_cin): + w_unrolled[g, i * nil : i * nil + nil, :] = w_unrolled_np[ + g, i * il + padding[0] : i * il + il - padding[0], : + ] - return w_unrolled + return w_unrolled.reshape(group_cin * nil, cout * ol) def _conv2d_unroll( @@ -115,9 +120,13 @@ def _conv2d_unroll( kernel: WeightType, stride: Size2Type, padding: Size2Type, + groups: int, ) -> WeightType: """Unroll the kernel of 2d convolution into a matrix.""" - cout, cin, kh, kw = kernel.shape + cout, group_cin, kh, kw = kernel.shape + cin = group_cin * groups + group_cout = cout // groups + kernel = kernel.reshape(groups, group_cout, group_cin, kh, kw) ih = in_shape[0] + 2 * padding[0] iw = in_shape[1] + 2 * padding[1] oh, ow = out_shape @@ -125,62 +134,66 @@ def _conv2d_unroll( out_size = oh * ow # weight unrolled without considering parameter padding - w_unrolled_np = np.zeros((cin * in_size, cout * out_size), dtype=kernel.dtype) - zeros_image = np.zeros((cin * ih, iw * cout, out_size), dtype=kernel.dtype) + w_unrolled_np = np.zeros((groups, group_cin * in_size, group_cout * out_size), dtype=kernel.dtype) + zeros_image = np.zeros((groups, group_cin * ih, iw * group_cout, out_size), dtype=kernel.dtype) - for i in range(oh): - for j in range(ow): - for ch_idx in np.ndindex(kernel.shape[:2]): - # [0] -> o_ch, [1] -> i_ch - zeros_image[ - i * stride[0] - + ch_idx[1] * ih : i * stride[0] - + ch_idx[1] * ih - + kh, - j * stride[1] - + ch_idx[0] * iw : j * stride[1] - + ch_idx[0] * iw - + kw, - i * ow + j, - ] = kernel[ch_idx[0], ch_idx[1], :, :] - - temp = ( - zeros_image[:, :, i * ow + j] - .reshape(cin * ih, cout, iw) - .transpose(1, 0, 2) - ) - # else: - # # (cin*ih, cout, iw) -> (cout, cin, ih, iw) - # temp = ( - # zeros_image[:, :, i * ow + j] - # .reshape(cin, ih, cout, iw) - # .transpose(2, 1, 3, 0) - # ) - - for o_ch in range(cout): - w_unrolled_np[:, i * ow + j + o_ch * out_size] = temp[o_ch].ravel() + for g in range(groups): + for i in range(oh): + for j in range(ow): + for ch_idx in np.ndindex(kernel.shape[1:3]): + # [0] -> o_ch, [1] -> i_ch + zeros_image[ + g, + i * stride[0] + + ch_idx[1] * ih : i * stride[0] + + ch_idx[1] * ih + + kh, + j * stride[1] + + ch_idx[0] * iw : j * stride[1] + + ch_idx[0] * iw + + kw, + i * ow + j, + ] = kernel[g, ch_idx[0], ch_idx[1], :, :] + + temp = ( + zeros_image[g, :, :, i * ow + j] + .reshape(group_cin * ih, group_cout, iw) + .transpose(1, 0, 2) + ) + # else: + # # (cin*ih, cout, iw) -> (cout, cin, ih, iw) + # temp = ( + # zeros_image[:, :, i * ow + j] + # .reshape(cin, ih, cout, iw) + # .transpose(2, 1, 3, 0) + # ) + + for o_ch in range(group_cout): + w_unrolled_np[g, :, i * ow + j + o_ch * out_size] = temp[o_ch].ravel() # Remove the part of the padding in the w_unrolled_no_padding # That is, remove useless weight in the w_unrolled_no_padding nih, niw = in_shape nin_size = nih * niw - w_unrolled = np.zeros((cin * nin_size, cout * out_size), dtype=kernel.dtype) + w_unrolled = np.zeros((groups, group_cin * nin_size, group_cout * out_size), dtype=kernel.dtype) - for i in range(cin): - for j in range(nih): - w_unrolled[i * nin_size + j * niw : i * nin_size + j * niw + niw, :] = ( - w_unrolled_np[ - i * in_size - + (padding[0] + j) * iw - + padding[1] : i * in_size - + (padding[0] + j) * iw - + padding[1] - + niw, - :, - ] - ) + for g in range(groups): + for i in range(group_cin): + for j in range(nih): + w_unrolled[g, i * nin_size + j * niw : i * nin_size + j * niw + niw, :] = ( + w_unrolled_np[ + g, + i * in_size + + (padding[0] + j) * iw + + padding[1] : i * in_size + + (padding[0] + j) * iw + + padding[1] + + niw, + :, + ] + ) - return w_unrolled + return w_unrolled.reshape(group_cin * nin_size, cout * out_size) def _conv2d_semifolded_unroll( @@ -253,24 +266,29 @@ def _conv1d_faster( kernel: WeightType, stride: Size1Type, padding: Size1Type, + groups: int, ) -> SynOutType: """Faster 1d convolution.""" - cout, _, kl = kernel.shape # (O, I, L) + cout, group_cin, kl = kernel.shape # (O, I, L) + cin = group_cin * groups + local_cout = cout // groups x_padded = np.pad(x_cl, ((0, 0), (padding[0], padding[0]))) + x_padded = x_padded.reshape(groups, group_cin, -1) - # kernel: (cout, cin, kl) -> (cout, cin*kl) - col_kernel = kernel.reshape(cout, -1) + # kernel: (cout, local_cin, kl) -> (groups, local_cout, local_cin*kl) + col_kernel = kernel.reshape(groups, local_cout, -1) - # padded: (cin, xl+2*p[0]-kl) -> (ol, cin*kl) - col_fm = _1d_im2col(x_padded, out_shape[0], kl, stride) + # padded: (groups, local_cin, xl+2*p[0]-kl) -> (groups, ol, local_cin*kl) + col_fm = [_1d_im2col(x_padded[i], out_shape[0], kl, stride) for i in range(groups)] # out = np.zeros((cout,) + out_shape, dtype=np.int64) # (ol, cin*kl) * (cout, cin*kl)^T = (ol, cout) - out = col_fm @ col_kernel.T # + self.bias + out = [col_fm[i] @ col_kernel[i].T for i in range(groups)] # + self.bias + out = [arr.T for arr in out] + out_arr = np.concatenate(out, axis=0) - # (ol, cout) -> (cout, ol) - return out.T.astype(VOLTAGE_DTYPE) + return out_arr.astype(VOLTAGE_DTYPE) def _conv2d_faster( diff --git a/paibox/components/synapses/synapses.py b/paibox/components/synapses/synapses.py index 20e9b63d..66e210d4 100644 --- a/paibox/components/synapses/synapses.py +++ b/paibox/components/synapses/synapses.py @@ -78,6 +78,7 @@ def __init__( *, stride: _Size1Type = 1, padding: _Size1Type = 0, + groups: int = 1, kernel_order: _KOrder3d = "OIL", name: Optional[str] = None, ) -> None: @@ -106,6 +107,7 @@ def __init__( _single(stride), _single(padding), _single(1), + groups, kernel_order, name, ) @@ -120,6 +122,7 @@ def __init__( *, stride: _Size2Type = 1, padding: _Size2Type = 0, + groups: int = 1, kernel_order: _KOrder4d = "OIHW", name: Optional[str] = None, ) -> None: @@ -148,6 +151,7 @@ def __init__( _pair(stride), _pair(padding), _pair(1), + groups, kernel_order, name, ) diff --git a/paibox/components/synapses/transforms.py b/paibox/components/synapses/transforms.py index cb687eeb..c93df9fd 100644 --- a/paibox/components/synapses/transforms.py +++ b/paibox/components/synapses/transforms.py @@ -335,12 +335,14 @@ def __init__( stride: _SizeAnyType = 0, padding: _SizeAnyType = 0, output_padding: _SizeAnyType = 0, + groups: int = 1, ) -> None: self.in_shape = in_shape self.out_shape = out_shape self.stride = stride self.padding = padding self.output_padding = output_padding + self.groups = groups super().__init__(kernel) @@ -351,9 +353,10 @@ class Conv1dForward(_ConvNdForward): out_shape: Size1Type stride: Size1Type padding: Size1Type + groups: int def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: - cin = self.weights.shape[1] + cin = self.weights.shape[1] * self.groups # if self.fm_order == "LC": # # (N,) -> (L, C) -> (C, L) @@ -362,13 +365,13 @@ def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: _x = x.reshape((cin,) + self.in_shape) return _conv1d_faster( - _x, self.out_shape, self.weights, self.stride, self.padding + _x, self.out_shape, self.weights, self.stride, self.padding, self.groups ) @property def connectivity(self): - return _conv1d_unroll( - self.in_shape, self.out_shape, self.weights, self.stride, self.padding + return _conv1d_unroll( + self.in_shape, self.out_shape, self.weights, self.stride, self.padding, self.groups ) @@ -377,9 +380,10 @@ class Conv2dForward(_ConvNdForward): out_shape: Size2Type stride: Size2Type padding: Size2Type + groups: int def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: - cin = self.weights.shape[1] + cin = self.weights.shape[1] * self.groups # if self.fm_order == "HWC": # # (N,) -> (H, W, C) -> (C, H, W) @@ -388,13 +392,13 @@ def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: _x = x.reshape((cin,) + self.in_shape) return _conv2d_faster( - _x, self.out_shape, self.weights, self.stride, self.padding + _x, self.out_shape, self.weights, self.stride, self.padding, self.groups ) @property def connectivity(self): return _conv2d_unroll( - self.in_shape, self.out_shape, self.weights, self.stride, self.padding + self.in_shape, self.out_shape, self.weights, self.stride, self.padding, self.groups ) diff --git a/tests/components/synapses/test_synapses.py b/tests/components/synapses/test_synapses.py index 506963ac..de5741e0 100644 --- a/tests/components/synapses/test_synapses.py +++ b/tests/components/synapses/test_synapses.py @@ -273,6 +273,7 @@ def test_Conv1d_instance(self): kernel_size = (5,) stride = 2 padding = 1 + groups = 2 out_shape = ((32 + 2 - 5) // 2 + 1,) in_channels = 8 out_channels = 16 @@ -282,16 +283,16 @@ def test_Conv1d_instance(self): n2 = pb.IF((out_channels,) + out_shape, 3) weight = np.random.randint( - -128, 128, size=(in_channels, out_channels) + kernel_size, dtype=np.int8 + -128, 128, size=(in_channels // groups, out_channels) + kernel_size, dtype=np.int8 ) s1 = pb.Conv1d( - n1, n2, weight, stride=stride, padding=padding, kernel_order=korder + n1, n2, weight, stride=stride, padding=padding, kernel_order=korder, groups=groups ) assert s1.num_in == in_channels * shape2num(in_shape) assert s1.connectivity.dtype == WEIGHT_DTYPE assert s1.connectivity.shape == ( - in_channels * shape2num(in_shape), + in_channels // groups * shape2num(in_shape), out_channels * shape2num(out_shape), ) @@ -300,6 +301,7 @@ def test_Conv2d_instance(self): kernel_size = (5, 5) padding = (1, 1) stride = 2 + groups = 2 out_shape = ((32 + 2 - 5) // 2 + 1, (32 + 2 - 5) // 2 + 1) in_channels = 8 out_channels = 16 @@ -310,16 +312,16 @@ def test_Conv2d_instance(self): n2 = pb.IF((out_channels * out_shape[0] * out_shape[1],), 3) weight = np.random.randint( - -8, 8, size=(in_channels, out_channels) + kernel_size, dtype=np.int32 + -8, 8, size=(in_channels // groups, out_channels) + kernel_size, dtype=np.int32 ) s1 = pb.Conv2d( - n1, n2, weight, stride=stride, padding=padding, kernel_order=korder + n1, n2, weight, stride=stride, padding=padding, kernel_order=korder, groups=groups ) assert s1.num_in == in_channels * shape2num(in_shape) assert s1.connectivity.dtype == WEIGHT_DTYPE assert s1.connectivity.shape == ( - in_channels * shape2num(in_shape), + in_channels // groups * shape2num(in_shape), out_channels * shape2num(out_shape), ) @@ -328,6 +330,7 @@ def test_Conv1d_inchannel_omitted(self): kernel_size = (5,) stride = 2 out_shape = ((32 - 5) // 2 + 1,) + groups = 1 in_channels = 1 # omit it out_channels = 4 korder = "IOL" @@ -336,14 +339,14 @@ def test_Conv1d_inchannel_omitted(self): n2 = pb.IF((out_channels,) + out_shape, 3) weight = np.random.randint( - -128, 128, size=(in_channels, out_channels) + kernel_size, dtype=np.int64 + -128, 128, size=(in_channels // groups, out_channels) + kernel_size, dtype=np.int64 ) - s1 = pb.Conv1d(n1, n2, weight, stride=stride, kernel_order=korder) + s1 = pb.Conv1d(n1, n2, weight, stride=stride, kernel_order=korder, groups=groups) assert s1.num_in == in_channels * shape2num(in_shape) assert s1.connectivity.dtype == WEIGHT_DTYPE assert s1.connectivity.shape == ( - in_channels * shape2num(in_shape), + in_channels // groups * shape2num(in_shape), out_channels * shape2num(out_shape), ) @@ -351,6 +354,7 @@ def test_Conv2d_inchannel_omitted(self): in_shape = (32, 32) kernel_size = (5, 5) stride = 2 + groups = 1 out_shape = ((32 - 5) // 2 + 1, (32 - 5) // 2 + 1) in_channels = 1 # omit it out_channels = 4 @@ -360,13 +364,13 @@ def test_Conv2d_inchannel_omitted(self): n2 = pb.IF((out_channels,) + out_shape, 3) weight = np.random.randint( - -128, 128, size=(in_channels, out_channels) + kernel_size, dtype=np.int8 + -128, 128, size=(in_channels // groups, out_channels) + kernel_size, dtype=np.int8 ) - s1 = pb.Conv2d(n1, n2, weight, stride=stride, kernel_order=korder) + s1 = pb.Conv2d(n1, n2, weight, stride=stride, kernel_order=korder, groups=groups) assert s1.num_in == in_channels * shape2num(in_shape) assert s1.connectivity.shape == ( - in_channels * shape2num(in_shape), + in_channels // groups * shape2num(in_shape), out_channels * shape2num(out_shape), ) diff --git a/tests/components/synapses/test_transforms.py b/tests/components/synapses/test_transforms.py index 2171cf79..8ad46964 100644 --- a/tests/components/synapses/test_transforms.py +++ b/tests/components/synapses/test_transforms.py @@ -240,26 +240,26 @@ def test_MaskedLinear( assert f.connectivity.shape == (x.size, y.size) @pytest.mark.parametrize( - "xdtype, in_shape, in_channels, out_channels, kernel_size, stride, padding, kdtype", + "xdtype, in_shape, in_channels, out_channels, kernel_size, stride, padding, groups, kdtype", [ - (np.bool_, (8,), 16, 8, (3,), (1,), (1,), np.int8), - (np.bool_, (28,), 16, 8, (3,), (1,), (1,), np.bool_), - (np.bool_, (28,), 24, 12, (3,), (2,), (2,), np.bool_), - (np.bool_, (28,), 24, 12, (5,), (2,), (2,), np.bool_), - (np.bool_, (16,), 8, 16, (3,), (2,), (0,), np.bool_), - (np.bool_, (28,), 16, 8, (3,), (1,), (0,), np.int8), - (np.bool_, (28,), 24, 12, (3,), (2,), (0,), np.int8), - (np.bool_, (28,), 24, 12, (5,), (2,), (0,), np.int8), - (np.bool_, (16,), 8, 16, (3,), (2,), (0,), np.int8), - (np.int8, (8,), 16, 8, (3,), (1,), (1,), np.int8), - (np.int8, (28,), 16, 8, (3,), (1,), (1,), np.bool_), - (np.int8, (28,), 24, 12, (3,), (2,), (2,), np.bool_), - (np.int8, (28,), 24, 12, (5,), (2,), (2,), np.bool_), - (np.int8, (16,), 8, 16, (3,), (2,), (0,), np.bool_), - (np.int8, (28,), 16, 8, (3,), (1,), (0,), np.int8), - (np.int8, (28,), 24, 12, (3,), (2,), (0,), np.int8), - (np.int8, (28,), 24, 12, (5,), (2,), (0,), np.int8), - (np.int8, (16,), 8, 16, (3,), (2,), (0,), np.int8), + (np.bool_, (8,), 16, 8, (3,), (1,), (1,), 2, np.int8), + (np.bool_, (28,), 16, 8, (3,), (1,), (1,), 4, np.bool_), + (np.bool_, (28,), 24, 12, (3,), (2,), (2,), 3, np.bool_), + (np.bool_, (28,), 24, 12, (5,), (2,), (2,), 6, np.bool_), + (np.bool_, (16,), 8, 16, (3,), (2,), (0,), 8, np.bool_), + (np.bool_, (28,), 16, 8, (3,), (1,), (0,), 1, np.int8), + (np.bool_, (28,), 24, 12, (3,), (2,), (0,), 1,np.int8), + (np.bool_, (28,), 24, 12, (5,), (2,), (0,), 4, np.int8), + (np.bool_, (16,), 8, 16, (3,), (2,), (0,), 2, np.int8), + (np.int8, (8,), 16, 8, (3,), (1,), (1,), 2, np.int8), + (np.int8, (28,), 16, 8, (3,), (1,), (1,), 4, np.bool_), + (np.int8, (28,), 24, 12, (3,), (2,), (2,), 3, np.bool_), + (np.int8, (28,), 24, 12, (5,), (2,), (2,), 3, np.bool_), + (np.int8, (16,), 8, 16, (3,), (2,), (0,), 8, np.bool_), + (np.int8, (28,), 16, 8, (3,), (1,), (0,), 1, np.int8), + (np.int8, (28,), 24, 12, (3,), (2,), (0,), 1, np.int8), + (np.int8, (28,), 24, 12, (5,), (2,), (0,), 4, np.int8), + (np.int8, (16,), 8, 16, (3,), (2,), (0,), 8, np.int8), # ((28,), 16, 8, (3,), (1,), (0,), "LC"), # ((24,), 8, 8, (3,), (2,), (0,), "LC"), # ((24,), 8, 16, (7,), (2,), (0,), "LC"), @@ -275,17 +275,20 @@ def test_Conv1dForward( kernel_size, stride, padding, + groups, kdtype, ): + group_in_channels = in_channels // groups + group_out_channels = out_channels // groups if kdtype == np.bool_: kernel = np.random.randint( - 0, 2, size=(out_channels, in_channels) + kernel_size, dtype=np.bool_ + 0, 2, size=(out_channels, group_in_channels) + kernel_size, dtype=np.bool_ ) else: kernel = np.random.randint( np.iinfo(kdtype).min, np.iinfo(kdtype).max + 1, - size=(out_channels, in_channels) + kernel_size, + size=(out_channels, group_in_channels) + kernel_size, dtype=kdtype, ) @@ -301,18 +304,21 @@ def test_Conv1dForward( ) out_shape = ((in_shape[0] + 2 * padding[0] - kernel_size[0]) // stride[0] + 1,) + f = tfm.Conv1dForward(in_shape, out_shape, kernel, stride, padding, groups=groups) - f = tfm.Conv1dForward(in_shape, out_shape, kernel, stride, padding) - - x = np.random.randint(0, 2, size=fm_shape, dtype=np.bool_) + # x = np.random.randint(0, 2, size=fm_shape, dtype=np.bool_) xf = x.ravel() + xg = xf.reshape(groups, -1) # The result of __call__ using traditional conv y1 = f(xf) # The result of matmul using the unrolled matrix - y2 = xf @ f.connectivity.astype(np.int32) + fkernel = f.connectivity.astype(np.int32) + fkernel = fkernel.reshape(groups, group_in_channels * in_shape[0], group_out_channels * out_shape[0]) + y2 = [xg[i] @ fkernel[i] for i in range(groups)] + y2 = np.concatenate(y2, axis=0) - expected = _conv1d_faster(x, out_shape, kernel, stride, padding) + expected = _conv1d_faster(x, out_shape, kernel, stride, padding, groups=groups) assert np.array_equal(y1, expected) assert np.array_equal(y2, expected.ravel()) @@ -322,20 +328,20 @@ def test_Conv1dForward( ) @pytest.mark.parametrize( - "xdtype, in_shape, in_channels, out_channels, kernel_size, stride, padding, kdtype", + "xdtype, in_shape, in_channels, out_channels, kernel_size, stride, padding, groups, kdtype", [ - (np.bool_, (28, 28), 16, 8, (3, 3), (1, 1), (1, 1), np.bool_), - (np.bool_, (28, 28), 24, 12, (3, 3), (2, 2), (2, 1), np.bool_), - (np.bool_, (28, 28), 16, 8, (3, 3), (1, 1), (2, 3), np.bool_), - (np.bool_, (28, 28), 24, 12, (3, 3), (2, 2), (0, 0), np.int8), - (np.bool_, (28, 28), 24, 12, (5, 5), (2, 1), (0, 0), np.int8), - (np.bool_, (8, 8), 8, 16, (3, 3), (2, 2), (1, 1), np.int8), - (np.int8, (28, 28), 16, 8, (3, 3), (1, 1), (1, 1), np.bool_), - (np.int8, (28, 28), 24, 12, (3, 3), (2, 2), (2, 1), np.bool_), - (np.int8, (28, 28), 16, 8, (3, 3), (1, 1), (2, 3), np.bool_), - (np.int8, (28, 28), 24, 12, (3, 3), (2, 2), (0, 0), np.int8), - (np.int8, (28, 28), 24, 12, (5, 5), (2, 1), (0, 0), np.int8), - (np.int8, (8, 8), 8, 16, (3, 3), (2, 2), (1, 1), np.int8), + (np.bool_, (28, 28), 16, 8, (3, 3), (1, 1), (1, 1), 2, np.bool_), + (np.bool_, (28, 28), 24, 12, (3, 3), (2, 2), (2, 1), 3, np.bool_), + (np.bool_, (28, 28), 16, 8, (3, 3), (1, 1), (2, 3), 8, np.bool_), + (np.bool_, (28, 28), 24, 12, (3, 3), (2, 2), (0, 0), 4, np.int8), + (np.bool_, (28, 28), 24, 12, (5, 5), (2, 1), (0, 0), 4, np.int8), + (np.bool_, (8, 8), 8, 16, (3, 3), (2, 2), (1, 1), 1, np.int8), + (np.int8, (28, 28), 16, 8, (3, 3), (1, 1), (1, 1), 8, np.bool_), + (np.int8, (28, 28), 24, 12, (3, 3), (2, 2), (2, 1), 1, np.bool_), + (np.int8, (28, 28), 16, 8, (3, 3), (1, 1), (2, 3), 4, np.bool_), + (np.int8, (28, 28), 24, 12, (3, 3), (2, 2), (0, 0), 12, np.int8), + (np.int8, (28, 28), 24, 12, (5, 5), (2, 1), (0, 0), 3, np.int8), + (np.int8, (8, 8), 8, 16, (3, 3), (2, 2), (1, 1), 2, np.int8), # ((28, 28), 16, 8, (3, 3), (1, 1), (0, 0), "HWC", np.bool_), # ((24, 32), 8, 8, (3, 4), (2, 1), (0, 0), "HWC", np.bool_), # ((24, 24), 8, 16, (7, 7), (2, 2), (0, 0), "HWC", np.bool_), @@ -353,17 +359,21 @@ def test_Conv2dForward( kernel_size, stride, padding, + groups, kdtype, ): + group_in_channels = in_channels // groups + group_out_channels = out_channels // groups + if kdtype == np.bool_: kernel = np.random.randint( - 0, 2, size=(out_channels, in_channels) + kernel_size, dtype=np.bool_ + 0, 2, size=(out_channels, group_in_channels) + kernel_size, dtype=np.bool_ ) else: kernel = np.random.randint( np.iinfo(kdtype).min, np.iinfo(kdtype).max + 1, - size=(out_channels, in_channels) + kernel_size, + size=(out_channels, group_in_channels) + kernel_size, dtype=kdtype, ) @@ -383,16 +393,20 @@ def test_Conv2dForward( (in_shape[1] + 2 * padding[1] - kernel_size[1]) // stride[1] + 1, ) - f = tfm.Conv2dForward(in_shape, out_shape, kernel, stride, padding) + f = tfm.Conv2dForward(in_shape, out_shape, kernel, stride, padding, groups=groups) xf = x.ravel() - + xg = xf.reshape(groups, -1) + # The result of __call__ using traditional conv y1 = f(xf) # The result of matmul using the unrolled matrix - y2 = xf @ f.connectivity.astype(np.int32) + fkernel = f.connectivity.astype(np.int32) + fkernel = fkernel.reshape(groups, group_in_channels * in_shape[0] * in_shape[1], group_out_channels * out_shape[0] * out_shape[1]) + y2 = [xg[i] @ fkernel[i] for i in range(groups)] + y2 = np.concatenate(y2, axis=0) - expected = _conv2d_faster(x, out_shape, kernel, stride, padding) + expected = _conv2d_faster(x, out_shape, kernel, stride, padding, groups=groups) assert np.array_equal(y1, expected) assert np.array_equal(y2, expected.ravel()) From c6206be4f0ad70e85c996b40d6b26fbeea4cd0cd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 03:43:59 +0000 Subject: [PATCH 176/187] :rotating_light: auto fix by pre-commit hooks --- paibox/components/synapses/base.py | 4 +- paibox/components/synapses/conv_utils.py | 53 +++++++++++++------- paibox/components/synapses/transforms.py | 16 ++++-- tests/components/synapses/test_synapses.py | 44 +++++++++++++--- tests/components/synapses/test_transforms.py | 32 +++++++++--- 5 files changed, 110 insertions(+), 39 deletions(-) diff --git a/paibox/components/synapses/base.py b/paibox/components/synapses/base.py index d8721c3f..1db3ad3f 100644 --- a/paibox/components/synapses/base.py +++ b/paibox/components/synapses/base.py @@ -283,7 +283,9 @@ def __init__( if (_output_size := out_channels * out_l) != dest.num_in: raise ShapeError(f"output size mismatch: {_output_size} != {dest.num_in}.") - self.comm = Conv1dForward((in_l,), (out_l,), _kernel, stride, padding, groups=groups) + self.comm = Conv1dForward( + (in_l,), (out_l,), _kernel, stride, padding, groups=groups + ) class Conv2dSyn(FullConnectedSyn): diff --git a/paibox/components/synapses/conv_utils.py b/paibox/components/synapses/conv_utils.py index 7cbd6763..41f5ba83 100644 --- a/paibox/components/synapses/conv_utils.py +++ b/paibox/components/synapses/conv_utils.py @@ -77,7 +77,9 @@ def _conv1d_unroll( ol = out_shape[0] # weight unrolled without considering parameter padding : weight unrolled no padding - w_unrolled_np = np.zeros((groups, group_cin * il, group_cout * ol), dtype=kernel.dtype) + w_unrolled_np = np.zeros( + (groups, group_cin * il, group_cout * ol), dtype=kernel.dtype + ) zeros_image = np.zeros((groups, group_cin * il, group_cout, ol), dtype=kernel.dtype) for g in range(groups): for i in range(ol): @@ -86,7 +88,10 @@ def _conv1d_unroll( # [0] -> o_ch, [1] -> i_ch zeros_image[ g, - i * stride[0] + ch_idx[1] * il : i * stride[0] + ch_idx[1] * il + kl, + i * stride[0] + + ch_idx[1] * il : i * stride[0] + + ch_idx[1] * il + + kl, ch_idx[0], i, ] = kernel[g, ch_idx[0], ch_idx[1], :] @@ -104,7 +109,9 @@ def _conv1d_unroll( # Remove the part of the padding in the w_unrolled_no_padding # That is, remove useless weight in the w_unrolled_no_padding nil = in_shape[0] - w_unrolled = np.zeros((groups, group_cin * nil, group_cout * ol), dtype=kernel.dtype) + w_unrolled = np.zeros( + (groups, group_cin * nil, group_cout * ol), dtype=kernel.dtype + ) for g in range(groups): for i in range(group_cin): w_unrolled[g, i * nil : i * nil + nil, :] = w_unrolled_np[ @@ -134,8 +141,12 @@ def _conv2d_unroll( out_size = oh * ow # weight unrolled without considering parameter padding - w_unrolled_np = np.zeros((groups, group_cin * in_size, group_cout * out_size), dtype=kernel.dtype) - zeros_image = np.zeros((groups, group_cin * ih, iw * group_cout, out_size), dtype=kernel.dtype) + w_unrolled_np = np.zeros( + (groups, group_cin * in_size, group_cout * out_size), dtype=kernel.dtype + ) + zeros_image = np.zeros( + (groups, group_cin * ih, iw * group_cout, out_size), dtype=kernel.dtype + ) for g in range(groups): for i in range(oh): @@ -169,29 +180,33 @@ def _conv2d_unroll( # ) for o_ch in range(group_cout): - w_unrolled_np[g, :, i * ow + j + o_ch * out_size] = temp[o_ch].ravel() + w_unrolled_np[g, :, i * ow + j + o_ch * out_size] = temp[ + o_ch + ].ravel() # Remove the part of the padding in the w_unrolled_no_padding # That is, remove useless weight in the w_unrolled_no_padding nih, niw = in_shape nin_size = nih * niw - w_unrolled = np.zeros((groups, group_cin * nin_size, group_cout * out_size), dtype=kernel.dtype) + w_unrolled = np.zeros( + (groups, group_cin * nin_size, group_cout * out_size), dtype=kernel.dtype + ) for g in range(groups): for i in range(group_cin): for j in range(nih): - w_unrolled[g, i * nin_size + j * niw : i * nin_size + j * niw + niw, :] = ( - w_unrolled_np[ - g, - i * in_size - + (padding[0] + j) * iw - + padding[1] : i * in_size - + (padding[0] + j) * iw - + padding[1] - + niw, - :, - ] - ) + w_unrolled[ + g, i * nin_size + j * niw : i * nin_size + j * niw + niw, : + ] = w_unrolled_np[ + g, + i * in_size + + (padding[0] + j) * iw + + padding[1] : i * in_size + + (padding[0] + j) * iw + + padding[1] + + niw, + :, + ] return w_unrolled.reshape(group_cin * nin_size, cout * out_size) diff --git a/paibox/components/synapses/transforms.py b/paibox/components/synapses/transforms.py index c93df9fd..9f0b483b 100644 --- a/paibox/components/synapses/transforms.py +++ b/paibox/components/synapses/transforms.py @@ -370,8 +370,13 @@ def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: @property def connectivity(self): - return _conv1d_unroll( - self.in_shape, self.out_shape, self.weights, self.stride, self.padding, self.groups + return _conv1d_unroll( + self.in_shape, + self.out_shape, + self.weights, + self.stride, + self.padding, + self.groups, ) @@ -398,7 +403,12 @@ def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: @property def connectivity(self): return _conv2d_unroll( - self.in_shape, self.out_shape, self.weights, self.stride, self.padding, self.groups + self.in_shape, + self.out_shape, + self.weights, + self.stride, + self.padding, + self.groups, ) diff --git a/tests/components/synapses/test_synapses.py b/tests/components/synapses/test_synapses.py index de5741e0..24776fa2 100644 --- a/tests/components/synapses/test_synapses.py +++ b/tests/components/synapses/test_synapses.py @@ -283,10 +283,19 @@ def test_Conv1d_instance(self): n2 = pb.IF((out_channels,) + out_shape, 3) weight = np.random.randint( - -128, 128, size=(in_channels // groups, out_channels) + kernel_size, dtype=np.int8 + -128, + 128, + size=(in_channels // groups, out_channels) + kernel_size, + dtype=np.int8, ) s1 = pb.Conv1d( - n1, n2, weight, stride=stride, padding=padding, kernel_order=korder, groups=groups + n1, + n2, + weight, + stride=stride, + padding=padding, + kernel_order=korder, + groups=groups, ) assert s1.num_in == in_channels * shape2num(in_shape) @@ -312,10 +321,19 @@ def test_Conv2d_instance(self): n2 = pb.IF((out_channels * out_shape[0] * out_shape[1],), 3) weight = np.random.randint( - -8, 8, size=(in_channels // groups, out_channels) + kernel_size, dtype=np.int32 + -8, + 8, + size=(in_channels // groups, out_channels) + kernel_size, + dtype=np.int32, ) s1 = pb.Conv2d( - n1, n2, weight, stride=stride, padding=padding, kernel_order=korder, groups=groups + n1, + n2, + weight, + stride=stride, + padding=padding, + kernel_order=korder, + groups=groups, ) assert s1.num_in == in_channels * shape2num(in_shape) @@ -339,9 +357,14 @@ def test_Conv1d_inchannel_omitted(self): n2 = pb.IF((out_channels,) + out_shape, 3) weight = np.random.randint( - -128, 128, size=(in_channels // groups, out_channels) + kernel_size, dtype=np.int64 + -128, + 128, + size=(in_channels // groups, out_channels) + kernel_size, + dtype=np.int64, + ) + s1 = pb.Conv1d( + n1, n2, weight, stride=stride, kernel_order=korder, groups=groups ) - s1 = pb.Conv1d(n1, n2, weight, stride=stride, kernel_order=korder, groups=groups) assert s1.num_in == in_channels * shape2num(in_shape) assert s1.connectivity.dtype == WEIGHT_DTYPE @@ -364,9 +387,14 @@ def test_Conv2d_inchannel_omitted(self): n2 = pb.IF((out_channels,) + out_shape, 3) weight = np.random.randint( - -128, 128, size=(in_channels // groups, out_channels) + kernel_size, dtype=np.int8 + -128, + 128, + size=(in_channels // groups, out_channels) + kernel_size, + dtype=np.int8, + ) + s1 = pb.Conv2d( + n1, n2, weight, stride=stride, kernel_order=korder, groups=groups ) - s1 = pb.Conv2d(n1, n2, weight, stride=stride, kernel_order=korder, groups=groups) assert s1.num_in == in_channels * shape2num(in_shape) assert s1.connectivity.shape == ( diff --git a/tests/components/synapses/test_transforms.py b/tests/components/synapses/test_transforms.py index 8ad46964..b5a3f937 100644 --- a/tests/components/synapses/test_transforms.py +++ b/tests/components/synapses/test_transforms.py @@ -248,7 +248,7 @@ def test_MaskedLinear( (np.bool_, (28,), 24, 12, (5,), (2,), (2,), 6, np.bool_), (np.bool_, (16,), 8, 16, (3,), (2,), (0,), 8, np.bool_), (np.bool_, (28,), 16, 8, (3,), (1,), (0,), 1, np.int8), - (np.bool_, (28,), 24, 12, (3,), (2,), (0,), 1,np.int8), + (np.bool_, (28,), 24, 12, (3,), (2,), (0,), 1, np.int8), (np.bool_, (28,), 24, 12, (5,), (2,), (0,), 4, np.int8), (np.bool_, (16,), 8, 16, (3,), (2,), (0,), 2, np.int8), (np.int8, (8,), 16, 8, (3,), (1,), (1,), 2, np.int8), @@ -282,7 +282,10 @@ def test_Conv1dForward( group_out_channels = out_channels // groups if kdtype == np.bool_: kernel = np.random.randint( - 0, 2, size=(out_channels, group_in_channels) + kernel_size, dtype=np.bool_ + 0, + 2, + size=(out_channels, group_in_channels) + kernel_size, + dtype=np.bool_, ) else: kernel = np.random.randint( @@ -304,7 +307,9 @@ def test_Conv1dForward( ) out_shape = ((in_shape[0] + 2 * padding[0] - kernel_size[0]) // stride[0] + 1,) - f = tfm.Conv1dForward(in_shape, out_shape, kernel, stride, padding, groups=groups) + f = tfm.Conv1dForward( + in_shape, out_shape, kernel, stride, padding, groups=groups + ) # x = np.random.randint(0, 2, size=fm_shape, dtype=np.bool_) xf = x.ravel() @@ -314,7 +319,9 @@ def test_Conv1dForward( y1 = f(xf) # The result of matmul using the unrolled matrix fkernel = f.connectivity.astype(np.int32) - fkernel = fkernel.reshape(groups, group_in_channels * in_shape[0], group_out_channels * out_shape[0]) + fkernel = fkernel.reshape( + groups, group_in_channels * in_shape[0], group_out_channels * out_shape[0] + ) y2 = [xg[i] @ fkernel[i] for i in range(groups)] y2 = np.concatenate(y2, axis=0) @@ -367,7 +374,10 @@ def test_Conv2dForward( if kdtype == np.bool_: kernel = np.random.randint( - 0, 2, size=(out_channels, group_in_channels) + kernel_size, dtype=np.bool_ + 0, + 2, + size=(out_channels, group_in_channels) + kernel_size, + dtype=np.bool_, ) else: kernel = np.random.randint( @@ -393,16 +403,22 @@ def test_Conv2dForward( (in_shape[1] + 2 * padding[1] - kernel_size[1]) // stride[1] + 1, ) - f = tfm.Conv2dForward(in_shape, out_shape, kernel, stride, padding, groups=groups) + f = tfm.Conv2dForward( + in_shape, out_shape, kernel, stride, padding, groups=groups + ) xf = x.ravel() xg = xf.reshape(groups, -1) - + # The result of __call__ using traditional conv y1 = f(xf) # The result of matmul using the unrolled matrix fkernel = f.connectivity.astype(np.int32) - fkernel = fkernel.reshape(groups, group_in_channels * in_shape[0] * in_shape[1], group_out_channels * out_shape[0] * out_shape[1]) + fkernel = fkernel.reshape( + groups, + group_in_channels * in_shape[0] * in_shape[1], + group_out_channels * out_shape[0] * out_shape[1], + ) y2 = [xg[i] @ fkernel[i] for i in range(groups)] y2 = np.concatenate(y2, axis=0) From 27550ced4da934e2eaa092a4d1fe628ea28d3358 Mon Sep 17 00:00:00 2001 From: Joustrd <17739386485@163.com> Date: Tue, 10 Dec 2024 13:53:11 +0800 Subject: [PATCH 177/187] [fix] conv init bugs --- paibox/components/synapses/transforms.py | 36 ++++++++++++++++-------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/paibox/components/synapses/transforms.py b/paibox/components/synapses/transforms.py index 9f0b483b..62db5036 100644 --- a/paibox/components/synapses/transforms.py +++ b/paibox/components/synapses/transforms.py @@ -335,25 +335,30 @@ def __init__( stride: _SizeAnyType = 0, padding: _SizeAnyType = 0, output_padding: _SizeAnyType = 0, - groups: int = 1, ) -> None: self.in_shape = in_shape self.out_shape = out_shape self.stride = stride self.padding = padding self.output_padding = output_padding - self.groups = groups super().__init__(kernel) class Conv1dForward(_ConvNdForward): - in_shape: Size1Type - out_shape: Size1Type - stride: Size1Type - padding: Size1Type - groups: int + def __init__( + self, + in_shape: SizeAnyType, + out_shape: SizeAnyType, + kernel: np.ndarray, + stride: _SizeAnyType = 0, + padding: _SizeAnyType = 0, + groups: int = 1, + output_padding: _SizeAnyType = 0, + ) -> None: + self.groups = groups + super().__init__(in_shape, out_shape, kernel, stride, padding, output_padding) def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: cin = self.weights.shape[1] * self.groups @@ -381,11 +386,18 @@ def connectivity(self): class Conv2dForward(_ConvNdForward): - in_shape: Size2Type - out_shape: Size2Type - stride: Size2Type - padding: Size2Type - groups: int + def __init__( + self, + in_shape: SizeAnyType, + out_shape: SizeAnyType, + kernel: np.ndarray, + stride: _SizeAnyType = 0, + padding: _SizeAnyType = 0, + groups: int = 1, + output_padding: _SizeAnyType = 0, + ) -> None: + self.groups = groups + super().__init__(in_shape, out_shape, kernel, stride, padding, output_padding) def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: cin = self.weights.shape[1] * self.groups From e37ffda3ac6868c2f8fbeb1f445b6ce509c2eb10 Mon Sep 17 00:00:00 2001 From: Joustrd <17739386485@163.com> Date: Tue, 10 Dec 2024 23:46:15 +0800 Subject: [PATCH 178/187] [fix] Unify groups parameter initialization and variable names --- paibox/components/synapses/base.py | 7 +-- paibox/components/synapses/conv_utils.py | 35 ++++----------- paibox/components/synapses/transforms.py | 56 +++++++++--------------- 3 files changed, 32 insertions(+), 66 deletions(-) diff --git a/paibox/components/synapses/base.py b/paibox/components/synapses/base.py index 1db3ad3f..374a0dcb 100644 --- a/paibox/components/synapses/base.py +++ b/paibox/components/synapses/base.py @@ -368,13 +368,14 @@ def __init__( _kernel = kernel.copy() # O,I,H - out_channels, in_channels, kernel_h = _kernel.shape + out_channels, group_in_channels, kernel_h = _kernel.shape + in_channels = groups * group_in_channels # I,H assert len(source.shape_out) == 2 in_ch, in_h = source.shape_out out_h = (in_h + 2 * padding[0] - kernel_h) // stride[0] + 1 - if in_ch != groups * in_channels: + if in_ch != in_channels: raise ShapeError(f"input channels mismatch: {in_ch} != {in_channels}.") if (_output_size := out_channels * out_h) != dest.num_in: @@ -384,7 +385,7 @@ def __init__( ) self.comm = Conv2dSemiFoldedForward( - (in_ch, in_h), (out_channels, out_h), _kernel, stride, padding, groups + (in_ch, in_h), (out_channels, out_h), _kernel, stride, padding, groups=groups ) diff --git a/paibox/components/synapses/conv_utils.py b/paibox/components/synapses/conv_utils.py index 41f5ba83..d3048695 100644 --- a/paibox/components/synapses/conv_utils.py +++ b/paibox/components/synapses/conv_utils.py @@ -316,13 +316,12 @@ def _conv2d_faster( # fm_order: str, ) -> SynOutType: """Faster 2d convolution.""" - cout, cin, kh, kw = kernel.shape # (O, I, H, W) + cout, group_cin, kh, kw = kernel.shape # (O, I, H, W) if cout % groups != 0: raise ValueError("Output channels must be divisible by groups.") - # 计算每个组的通道数 - cin_per_group = cin - cout_per_group = cout // groups + # 计算每个组的通道数 + group_cout = cout // groups # 将输入张量进行填充 x_padded = np.pad( @@ -335,11 +334,11 @@ def _conv2d_faster( for g in range(groups): # 获取当前组的输入和卷积核 - x_group = x_padded[g * cin_per_group : (g + 1) * cin_per_group, :, :] - kernel_group = kernel[g * cout_per_group : (g + 1) * cout_per_group, :, :, :] + x_group = x_padded[g * group_cin : (g + 1) * group_cin, :, :] + kernel_group = kernel[g * group_cout : (g + 1) * group_cout, :, :, :] # 重塑卷积核以进行矩阵乘法 - col_kernel = kernel_group.reshape(cout_per_group, -1) + col_kernel = kernel_group.reshape(group_cout, -1) # 转换当前组的填充图像为列格式 col_fm = _2d_im2col(x_group, out_shape[0], out_shape[1], kh, kw, stride) @@ -348,29 +347,11 @@ def _conv2d_faster( out_group = col_fm @ col_kernel.T # 将组输出重塑并合并到最终输出中 - out[g * cout_per_group : (g + 1) * cout_per_group, :] = out_group.T.reshape( - (cout_per_group, *out_shape) + out[g * group_cout : (g + 1) * group_cout, :] = out_group.T.reshape( + (group_cout, *out_shape) ) return out.astype(VOLTAGE_DTYPE) - # x_padded = np.pad( - # x_chw, - # ((0, 0), (padding[0], padding[0]), (padding[1], padding[1])), - # ) - - # # kernel: (cout, cin, kh, kw) -> (cout, cin*kh*kw) - # col_kernel = kernel.reshape(cout, -1) - - # # padded: (cin, xh+2*p[0]-kh, xw+2*p[1]-kw) -> (oh*ow, cin*kh*kw) - # col_fm = _2d_im2col(x_padded, out_shape[0], out_shape[1], kh, kw, stride) - # # out = np.zeros((cout,) + out_shape, dtype=np.int64) - # # (oh*ow, cin*kh*kw) * (cout, cin*kh*kw)^T = (oh*ow, cout) - # out = col_fm @ col_kernel.T # + self.bias - # # (oh*ow, cout) -> (cout, oh*ow) -> (cout, oh, ow) - # out = out.T.reshape((cout,) + out_shape) - - # return out.astype(VOLTAGE_DTYPE) - def _convtranspose1d_unroll( in_shape: Size1Type, diff --git a/paibox/components/synapses/transforms.py b/paibox/components/synapses/transforms.py index 62db5036..1a296fa0 100644 --- a/paibox/components/synapses/transforms.py +++ b/paibox/components/synapses/transforms.py @@ -335,30 +335,26 @@ def __init__( stride: _SizeAnyType = 0, padding: _SizeAnyType = 0, output_padding: _SizeAnyType = 0, + groups: int = 1, ) -> None: self.in_shape = in_shape self.out_shape = out_shape self.stride = stride self.padding = padding self.output_padding = output_padding + self.groups = groups super().__init__(kernel) class Conv1dForward(_ConvNdForward): - def __init__( - self, - in_shape: SizeAnyType, - out_shape: SizeAnyType, - kernel: np.ndarray, - stride: _SizeAnyType = 0, - padding: _SizeAnyType = 0, - groups: int = 1, - output_padding: _SizeAnyType = 0, - ) -> None: - self.groups = groups - super().__init__(in_shape, out_shape, kernel, stride, padding, output_padding) + in_shape: Size1Type + out_shape: Size1Type + stride: Size1Type + padding: Size1Type + groups: int + def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: cin = self.weights.shape[1] * self.groups @@ -386,18 +382,12 @@ def connectivity(self): class Conv2dForward(_ConvNdForward): - def __init__( - self, - in_shape: SizeAnyType, - out_shape: SizeAnyType, - kernel: np.ndarray, - stride: _SizeAnyType = 0, - padding: _SizeAnyType = 0, - groups: int = 1, - output_padding: _SizeAnyType = 0, - ) -> None: - self.groups = groups - super().__init__(in_shape, out_shape, kernel, stride, padding, output_padding) + + in_shape: Size2Type + out_shape: Size2Type + stride: Size2Type + padding: Size2Type + groups: int def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: cin = self.weights.shape[1] * self.groups @@ -425,18 +415,12 @@ def connectivity(self): class Conv2dSemiFoldedForward(_ConvNdForward): - def __init__( - self, - in_shape: SizeAnyType, - out_shape: SizeAnyType, - kernel: np.ndarray, - stride: _SizeAnyType = 0, - padding: _SizeAnyType = 0, - groups: int = 1, - output_padding: _SizeAnyType = 0, - ) -> None: - self.groups = groups - super().__init__(in_shape, out_shape, kernel, stride, padding, output_padding) + + in_shape: Size2Type + out_shape: Size2Type + stride: Size2Type + padding: Size2Type + groups: int def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: return x @ self.connectivity From e7e38c5a7119b95e05fb05970c690389d0c045dc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:46:59 +0000 Subject: [PATCH 179/187] :rotating_light: auto fix by pre-commit hooks --- paibox/components/synapses/base.py | 7 ++++++- paibox/components/synapses/transforms.py | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/paibox/components/synapses/base.py b/paibox/components/synapses/base.py index 374a0dcb..552cc2f3 100644 --- a/paibox/components/synapses/base.py +++ b/paibox/components/synapses/base.py @@ -385,7 +385,12 @@ def __init__( ) self.comm = Conv2dSemiFoldedForward( - (in_ch, in_h), (out_channels, out_h), _kernel, stride, padding, groups=groups + (in_ch, in_h), + (out_channels, out_h), + _kernel, + stride, + padding, + groups=groups, ) diff --git a/paibox/components/synapses/transforms.py b/paibox/components/synapses/transforms.py index 1a296fa0..da334afd 100644 --- a/paibox/components/synapses/transforms.py +++ b/paibox/components/synapses/transforms.py @@ -355,7 +355,6 @@ class Conv1dForward(_ConvNdForward): padding: Size1Type groups: int - def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: cin = self.weights.shape[1] * self.groups From 08217627302460dfb6a86de0ff20adb3fa284516 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 9 Dec 2024 09:50:31 +0800 Subject: [PATCH 180/187] =?UTF-8?q?=E2=A4=B5=EF=B8=8F=20revert=20pr#146=20?= =?UTF-8?q?about=20twe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/components/functional.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/paibox/components/functional.py b/paibox/components/functional.py index a6e33f94..d32d095a 100644 --- a/paibox/components/functional.py +++ b/paibox/components/functional.py @@ -942,7 +942,7 @@ def build( shape=(ich, ih), delay=incoming_flow_format.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe - incoming_flow_format.interval * i, + tick_wait_end=twe, keep_shape=self.keep_shape, name=f"n{i}_{self.name}", ) @@ -1088,7 +1088,7 @@ def build( (ic, ih), delay=incoming_flow_format.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe - incoming_flow_format.interval * i, + tick_wait_end=twe, name=f"n{i}_delay_{self.name}", ) n_delays.append(neuron) @@ -1259,7 +1259,7 @@ def build( (cin, ih), delay=incoming_flow_format.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe - incoming_flow_format.interval * i, + tick_wait_end=twe, keep_shape=self.keep_shape, name=f"n{i}_{self.name}", ) @@ -1406,7 +1406,7 @@ def build( (cin, ih), delay=incoming_flow_format.interval * i + 1, tick_wait_start=self.tick_wait_start, - tick_wait_end=twe - incoming_flow_format.interval * i, + tick_wait_end=twe, keep_shape=self.keep_shape, name=f"n{i}_{self.name}", ) From abea1a70a62abaa768630dd0c8cbee8ba9ee75f0 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 16 Dec 2024 15:08:09 +0800 Subject: [PATCH 181/187] format(conv): removed unused comments, improved format --- paibox/components/synapses/conv_utils.py | 128 ++++++++++++----------- 1 file changed, 69 insertions(+), 59 deletions(-) diff --git a/paibox/components/synapses/conv_utils.py b/paibox/components/synapses/conv_utils.py index d3048695..216dd052 100644 --- a/paibox/components/synapses/conv_utils.py +++ b/paibox/components/synapses/conv_utils.py @@ -1,6 +1,7 @@ from collections.abc import Iterable from functools import partial from itertools import repeat +from typing import Optional import numpy as np from numpy.typing import NDArray @@ -224,14 +225,16 @@ def _conv2d_semifolded_unroll( ih = in_shape[1] + 2 * padding[0] _, oh = out_shape w_np = np.zeros((cin * in_shape[1], cout * oh), dtype=kernel.dtype) + + cout_per_grp = cout // groups for g in range(groups): - for i in range(cout // groups): + for i in range(cout_per_grp): for j in range(ck): # Must recreate `w_block` every time because some rows will be deleted. w_block = np.zeros((ih, oh), dtype=kernel.dtype) for k in range(oh): w_block[k * stride[1] : k * stride[1] + kh, k] = kernel[ - g * cout // groups + i, j, : + g * cout_per_grp + i, j, : ] if padding[0] > 0: # H direction w_block = np.delete( @@ -245,28 +248,11 @@ def _conv2d_semifolded_unroll( g * ck * in_shape[1] + j * in_shape[1] : g * ck * in_shape[1] + (j + 1) * in_shape[1], - g * oh * cout // groups - + i * oh : g * oh * cout // groups + g * oh * cout_per_grp + + i * oh : g * oh * cout_per_grp + (i + 1) * oh, ] = w_block - # for i in range(cout): - # for j in range(cin): - # # Must recreate `w_block` every time because some rows will be deleted. - # w_block = np.zeros((ih, oh), dtype=kernel.dtype) - # for k in range(oh): - # w_block[k * stride[1] : k * stride[1] + kh, k] = kernel[i, j, :] - - # if padding[0] > 0: # H direction - # w_block = np.delete( - # w_block, - # np.hstack((np.arange(padding[0]), np.arange(ih - padding[0], ih))), - # axis=0, - # ) - # w_np[j * in_shape[1] : (j + 1) * in_shape[1], i * oh : (i + 1) * oh] = ( - # w_block - # ) - return w_np @@ -281,29 +267,40 @@ def _conv1d_faster( kernel: WeightType, stride: Size1Type, padding: Size1Type, - groups: int, + groups: int = 1, + bias: Optional[WeightType] = None, ) -> SynOutType: """Faster 1d convolution.""" - cout, group_cin, kl = kernel.shape # (O, I, L) - cin = group_cin * groups - local_cout = cout // groups + cout, cin_per_grp, kl = kernel.shape # (O, I, L) + assert x_cl.shape[0] == cin_per_grp * groups + assert cout % groups == 0 + + cout_per_grp = cout // groups x_padded = np.pad(x_cl, ((0, 0), (padding[0], padding[0]))) - x_padded = x_padded.reshape(groups, group_cin, -1) + out = np.zeros((cout, *out_shape), dtype=np.int64) - # kernel: (cout, local_cin, kl) -> (groups, local_cout, local_cin*kl) - col_kernel = kernel.reshape(groups, local_cout, -1) + for g in range(groups): + x_grp = x_padded[g * cin_per_grp : (g + 1) * cin_per_grp, :] + kernel_grp = kernel[g * cout_per_grp : (g + 1) * cout_per_grp, :, :] + # kernel: (cout_per_grp, cin, kl) -> (cout_per_grp, cin*kl) + col_kernel = kernel_grp.reshape(cout_per_grp, -1) + # padded: (cin, xl+2*p[0]-kl) -> (ol, cin*kl) + col_fm = _1d_im2col(x_grp, out_shape[0], kl, stride) + # (ol, cin*kl) * (cout, cin*kl)^T = (ol, cout_per_grp) + out_grp = col_fm @ col_kernel.T + + out[g * cout_per_grp : (g + 1) * cout_per_grp, :] = out_grp.T.reshape( + (cout_per_grp, *out_shape) + ) - # padded: (groups, local_cin, xl+2*p[0]-kl) -> (groups, ol, local_cin*kl) - col_fm = [_1d_im2col(x_padded[i], out_shape[0], kl, stride) for i in range(groups)] + if bias: + _bias = bias.squeeze() + assert _bias.shape == (cout,) - # out = np.zeros((cout,) + out_shape, dtype=np.int64) - # (ol, cin*kl) * (cout, cin*kl)^T = (ol, cout) - out = [col_fm[i] @ col_kernel[i].T for i in range(groups)] # + self.bias - out = [arr.T for arr in out] - out_arr = np.concatenate(out, axis=0) + out += _bias - return out_arr.astype(VOLTAGE_DTYPE) + return out.astype(VOLTAGE_DTYPE) def _conv2d_faster( @@ -313,43 +310,42 @@ def _conv2d_faster( stride: Size2Type, padding: Size2Type, groups: int = 1, - # fm_order: str, + bias: Optional[WeightType] = None, ) -> SynOutType: """Faster 2d convolution.""" - cout, group_cin, kh, kw = kernel.shape # (O, I, H, W) - if cout % groups != 0: - raise ValueError("Output channels must be divisible by groups.") + cout, cin_per_grp, kh, kw = kernel.shape # (O, I, H, W) - # 计算每个组的通道数 - group_cout = cout // groups + assert x_chw.shape[0] == cin_per_grp * groups + assert cout % groups == 0 + + cout_per_grp = cout // groups - # 将输入张量进行填充 x_padded = np.pad( x_chw, ((0, 0), (padding[0], padding[0]), (padding[1], padding[1])), ) - - # 用于存储最终输出 out = np.zeros((cout, *out_shape), dtype=np.int64) for g in range(groups): - # 获取当前组的输入和卷积核 - x_group = x_padded[g * group_cin : (g + 1) * group_cin, :, :] - kernel_group = kernel[g * group_cout : (g + 1) * group_cout, :, :, :] - - # 重塑卷积核以进行矩阵乘法 - col_kernel = kernel_group.reshape(group_cout, -1) + x_grp = x_padded[g * cin_per_grp : (g + 1) * cin_per_grp, :, :] + kernel_grp = kernel[g * cout_per_grp : (g + 1) * cout_per_grp, :, :, :] + # kernel: (cout_per_grp, cin, kh, kw) -> (cout_per_grp, cin*kh*kw) + col_kernel = kernel_grp.reshape(cout_per_grp, -1) + # padded: (cin, xh+2*p[0]-kh, xw+2*p[1]-kw) -> (oh*ow, cin*kh*kw) + col_fm = _2d_im2col(x_grp, out_shape[0], out_shape[1], kh, kw, stride) + # (oh*ow, cin*kh*kw) * (cout, cin*kh*kw)^T = (oh*ow, cout_per_grp) + out_grp = col_fm @ col_kernel.T + + out[g * cout_per_grp : (g + 1) * cout_per_grp, :] = out_grp.T.reshape( + (cout_per_grp, *out_shape) + ) - # 转换当前组的填充图像为列格式 - col_fm = _2d_im2col(x_group, out_shape[0], out_shape[1], kh, kw, stride) + if bias: + _bias = bias.squeeze() + assert _bias.shape == (cout,) - # 进行矩阵乘法 - out_group = col_fm @ col_kernel.T + out += _bias - # 将组输出重塑并合并到最终输出中 - out[g * group_cout : (g + 1) * group_cout, :] = out_group.T.reshape( - (group_cout, *out_shape) - ) return out.astype(VOLTAGE_DTYPE) @@ -524,6 +520,7 @@ def _convtranspose1d_faster( stride: Size1Type, padding: Size1Type, output_padding: Size1Type, + bias: Optional[WeightType] = None, ) -> SynOutType: # (C, L) xc, xl = x_cl.shape @@ -567,6 +564,12 @@ def _convtranspose1d_faster( # output_padding out = np.pad(out, ((0, 0), (0, output_padding[0]))) + if bias: + _bias = bias.squeeze() + assert _bias.shape == (cout,) + + out += _bias + return out.astype(VOLTAGE_DTYPE) @@ -577,6 +580,7 @@ def _convtranspose2d_faster( stride: Size2Type, padding: Size2Type, output_padding: Size2Type, + bias: Optional[WeightType] = None, ) -> SynOutType: # (C, H, W) xc, xh, xw = x_chw.shape @@ -626,6 +630,12 @@ def _convtranspose2d_faster( ] # output_padding out = np.pad(out, ((0, 0), (0, output_padding[0]), (0, output_padding[1]))) + + if bias: + _bias = bias.squeeze() + assert _bias.shape == (cout,) + + out += _bias return out From 03b02aa2b8baf32d5ad902b01ce75a1607216fd6 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 16 Dec 2024 15:09:28 +0800 Subject: [PATCH 182/187] format(synapses): improved the prompt text for input channel number mismatch errors --- paibox/components/synapses/base.py | 61 ++++++++++---------- paibox/components/synapses/transforms.py | 11 ++-- tests/components/synapses/test_transforms.py | 4 +- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/paibox/components/synapses/base.py b/paibox/components/synapses/base.py index 552cc2f3..10196b41 100644 --- a/paibox/components/synapses/base.py +++ b/paibox/components/synapses/base.py @@ -269,18 +269,19 @@ def __init__( _kernel = kernel.copy() # O,I,L - out_channels, group_in_channels, kernel_l = _kernel.shape - in_channels = groups * group_in_channels + o_ch, grp_in_ch, kernel_l = _kernel.shape # C,L in_ch, in_l = _fm_ndim1_check(source.shape_out, "CL") out_l = (in_l + 2 * padding[0] - dilation[0] * (kernel_l - 1) - 1) // stride[ 0 ] + 1 - if in_ch != in_channels: - raise ShapeError(f"input channels mismatch: {in_ch} != {in_channels}.") + if in_ch != (_cur_in_ch := groups * grp_in_ch): + in_ch_mismatch_text = f"input channels mismatch: {in_ch} != {_cur_in_ch}" + in_ch_mismatch_text += f" ({groups}*{grp_in_ch})." if groups > 1 else "." + raise ShapeError(in_ch_mismatch_text) - if (_output_size := out_channels * out_l) != dest.num_in: + if (_output_size := o_ch * out_l) != dest.num_in: raise ShapeError(f"output size mismatch: {_output_size} != {dest.num_in}.") self.comm = Conv1dForward( @@ -316,8 +317,7 @@ def __init__( _kernel = kernel.copy() # O,I,H,W - out_channels, group_in_channels, kernel_h, kernel_w = _kernel.shape - in_channels = groups * group_in_channels + o_ch, grp_in_ch, kernel_h, kernel_w = _kernel.shape # C,H,W in_ch, in_h, in_w = _fm_ndim2_check(source.shape_out, "CHW") out_h = (in_h + 2 * padding[0] - dilation[0] * (kernel_h - 1) - 1) // stride[ @@ -327,12 +327,14 @@ def __init__( 1 ] + 1 - if in_ch != in_channels: - raise ShapeError(f"input channels mismatch: {in_ch} != {in_channels}.") + if in_ch != (_cur_in_ch := groups * grp_in_ch): + in_ch_mismatch_text = f"input channels mismatch: {in_ch} != {_cur_in_ch}" + in_ch_mismatch_text += f" ({groups}*{grp_in_ch})." if groups > 1 else "." + raise ShapeError(in_ch_mismatch_text) - if (_output_size := out_channels * out_h * out_w) != dest.num_in: + if (_output_size := o_ch * out_h * out_w) != dest.num_in: raise ShapeError( - f"output size mismatch: {_output_size} ({out_channels}*{out_h}*{out_w}) " + f"output size mismatch: {_output_size} ({o_ch}*{out_h}*{out_w}) " f"!= {dest.num_in}." ) @@ -368,29 +370,25 @@ def __init__( _kernel = kernel.copy() # O,I,H - out_channels, group_in_channels, kernel_h = _kernel.shape - in_channels = groups * group_in_channels + o_ch, grp_in_ch, kernel_h = _kernel.shape # I,H assert len(source.shape_out) == 2 in_ch, in_h = source.shape_out out_h = (in_h + 2 * padding[0] - kernel_h) // stride[0] + 1 - if in_ch != in_channels: - raise ShapeError(f"input channels mismatch: {in_ch} != {in_channels}.") + if in_ch != (_cur_in_ch := groups * grp_in_ch): + in_ch_mismatch_text = f"input channels mismatch: {in_ch} != {_cur_in_ch}" + in_ch_mismatch_text += f" ({groups}*{grp_in_ch})." if groups > 1 else "." + raise ShapeError(in_ch_mismatch_text) - if (_output_size := out_channels * out_h) != dest.num_in: + if (_output_size := o_ch * out_h) != dest.num_in: raise ShapeError( - f"output size mismatch: {_output_size} ({out_channels}*{out_h}) " + f"output size mismatch: {_output_size} ({o_ch}*{out_h}) " f"!= {dest.num_in}." ) self.comm = Conv2dSemiFoldedForward( - (in_ch, in_h), - (out_channels, out_h), - _kernel, - stride, - padding, - groups=groups, + (in_ch, in_h), (o_ch, out_h), _kernel, stride, padding, groups=groups ) @@ -422,7 +420,7 @@ def __init__( _kernel = kernel.copy() # O,I,L - out_channels, in_channels, kernel_l = _kernel.shape + o_ch, in_channels, kernel_l = _kernel.shape # C,L in_ch, in_l = _fm_ndim1_check(source.shape_out, "CL") out_l = ( @@ -436,11 +434,11 @@ def __init__( if in_ch != in_channels: raise ShapeError(f"input channels mismatch: {in_ch} != {in_channels}.") - if (_output_size := out_channels * out_l) != dest.num_in: + if (_output_size := o_ch * out_l) != dest.num_in: raise ShapeError(f"output size mismatch: {_output_size} != {dest.num_in}.") self.comm = ConvTranspose1dForward( - (in_l,), (out_l,), _kernel, stride, padding, output_padding + (in_l,), (out_l,), _kernel, stride, padding, output_padding=output_padding ) @@ -472,7 +470,7 @@ def __init__( _kernel = kernel.copy() # O,I,H,W - out_channels, in_channels, kernel_h, kernel_w = _kernel.shape + o_ch, in_channels, kernel_h, kernel_w = _kernel.shape # C,H,W in_ch, in_h, in_w = _fm_ndim2_check(source.shape_out, "CHW") out_h = ( @@ -493,11 +491,16 @@ def __init__( if in_ch != in_channels: raise ShapeError(f"input channels mismatch: {in_ch} != {in_channels}.") - if (_output_size := out_channels * out_h * out_w) != dest.num_in: + if (_output_size := o_ch * out_h * out_w) != dest.num_in: raise ShapeError(f"output size mismatch: {_output_size} != {dest.num_in}.") self.comm = ConvTranspose2dForward( - (in_h, in_w), (out_h, out_w), _kernel, stride, padding, output_padding + (in_h, in_w), + (out_h, out_w), + _kernel, + stride, + padding, + output_padding=output_padding, ) diff --git a/paibox/components/synapses/transforms.py b/paibox/components/synapses/transforms.py index da334afd..860aaeab 100644 --- a/paibox/components/synapses/transforms.py +++ b/paibox/components/synapses/transforms.py @@ -334,15 +334,15 @@ def __init__( kernel: np.ndarray, stride: _SizeAnyType = 0, padding: _SizeAnyType = 0, - output_padding: _SizeAnyType = 0, groups: int = 1, + output_padding: _SizeAnyType = 0, ) -> None: self.in_shape = in_shape self.out_shape = out_shape self.stride = stride self.padding = padding - self.output_padding = output_padding self.groups = groups + self.output_padding = output_padding super().__init__(kernel) @@ -414,7 +414,6 @@ def connectivity(self): class Conv2dSemiFoldedForward(_ConvNdForward): - in_shape: Size2Type out_shape: Size2Type stride: Size2Type @@ -441,10 +440,11 @@ class ConvTranspose1dForward(_ConvNdForward): out_shape: Size1Type stride: Size1Type padding: Size1Type + groups: int output_padding: Size1Type def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: - cin = self.weights.shape[1] + cin = self.weights.shape[1] * self.groups # if self.fm_order == "LC": # # (N,) -> (L, C) -> (C, L) @@ -478,10 +478,11 @@ class ConvTranspose2dForward(_ConvNdForward): out_shape: Size2Type stride: Size2Type padding: Size2Type + groups: int output_padding: Size2Type def __call__(self, x: NeuOutType, *args, **kwargs) -> SynOutType: - cin = self.weights.shape[1] + cin = self.weights.shape[1] * self.groups # if self.fm_order == "HWC": # # (N,) -> (H, W, C) -> (C, H, W) diff --git a/tests/components/synapses/test_transforms.py b/tests/components/synapses/test_transforms.py index b5a3f937..48807836 100644 --- a/tests/components/synapses/test_transforms.py +++ b/tests/components/synapses/test_transforms.py @@ -553,7 +553,7 @@ def test_ConvTranspose1dForward( + output_padding[0], ) f = tfm.ConvTranspose1dForward( - in_shape, out_shape, kernel, stride, padding, output_padding + in_shape, out_shape, kernel, stride, padding, output_padding=output_padding ) xf = x.ravel() @@ -713,7 +713,7 @@ def test_ConvTranspose2dForward( ) f = tfm.ConvTranspose2dForward( - in_shape, out_shape, kernel, stride, padding, output_padding + in_shape, out_shape, kernel, stride, padding, output_padding=output_padding ) x = np.random.randint(0, 2, size=fm_shape, dtype=np.bool_) From 1aa21fd6192c7ef63c66e210e19d32afa533eea0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 07:11:42 +0000 Subject: [PATCH 183/187] :rotating_light: auto fix by pre-commit hooks --- paibox/components/synapses/conv_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paibox/components/synapses/conv_utils.py b/paibox/components/synapses/conv_utils.py index 216dd052..7c8c996c 100644 --- a/paibox/components/synapses/conv_utils.py +++ b/paibox/components/synapses/conv_utils.py @@ -630,7 +630,7 @@ def _convtranspose2d_faster( ] # output_padding out = np.pad(out, ((0, 0), (0, output_padding[0]), (0, output_padding[1]))) - + if bias: _bias = bias.squeeze() assert _bias.shape == (cout,) From 79890e4a8fb80399ac02aa8152724b584512f959 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 16 Dec 2024 15:27:39 +0800 Subject: [PATCH 184/187] =?UTF-8?q?=F0=9F=8E=A8=20format(mapper):=20format?= =?UTF-8?q?=20improved?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paibox/backend/mapper.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/paibox/backend/mapper.py b/paibox/backend/mapper.py index e0bfb715..fb358726 100644 --- a/paibox/backend/mapper.py +++ b/paibox/backend/mapper.py @@ -130,31 +130,30 @@ def compile( """Compile the network with optimization options. Args: - - weight_bit_optimization: whether to optimize weight precision. For example, weights declared as \ - INT8 are treated as smaller precision based on their actual values (when the weight are all \ + weight_bit_optimization (bool): whether to optimize weight precision. For example, weights declared \ + as INT8 are treated as smaller precision based on their actual values (when the weight are all \ between [-8, 7], they can be treated as INT4). By default, it is specified by the corresponding \ compile option in the backend configuration item. Default is true. - - grouping_optim_target: specify the optimization goal of neuron grouping, which can be `latency`, \ - `core` or `both`, which respectively represent the optimization goal of delay/throughput, \ - occupied cores, or both. The default is specified by the corresponding compilation option in the\ - backend configuration item. Default is 'both'. - - no_twisted_branch (for advanced use): when parsing the network topology, whether or not to prohibit intersecting \ - branch structures will cause such structures to be processed. For example: + grouping_optim_target ("latency", "core", "both"): specify the optimization goal of neuron grouping,\ + which can be `latency`, `core` or `both` which respectively represent the optimization goal of \ + delay/throughput, occupied cores, or both. The default is specified by the corresponding \ + compilation option in the backend configuration item. Default is 'both'. + no_twisted_branch (bool): only for advanced use. when parsing the network topology, whether or not \ + to prohibit intersecting branch structures will cause such structures to be processed. \ + For example: I -> A -> B -> C ------> - The out-degree of node A is > 1, and its successor node C has an in-degree > 1. If `no_twisted_branch` \ - is true, A will be copied & denoted as A', whose forward connection is preserved. + The out-degree of node A is > 1, and its successor node C has an in-degree > 1. If true, A will \ + be copied & denoted as A', whose forward connection is preserved. I -> A -> B -> C -> A'------> - Default is false. - - - multicast_optim (in dev): whether to perform multicast optimization. If true, the optimization is \ - performed on all nodes in the network. If a node list is passed, the optimization is attempted \ - on the specified nodes only. Default is false. + multicast_optim (bool, Sequence[NodeType]): whether to perform multicast optimization. If true, the \ + optimization is performed on all nodes in the network. If passing a node list, the optimization \ + is attempted on the specified nodes only. Default is false. TODO A description of it is to be added Return: network information after compilation in dictionary format. From 545689336c1167a362f18ad0bca92e0721f1aeb2 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 16 Dec 2024 15:30:44 +0800 Subject: [PATCH 185/187] =?UTF-8?q?=F0=9F=93=9D=20docs:=20update=20the=20g?= =?UTF-8?q?uide=20&=20changelog=20for=20`v1.2.0`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 10 +++- docs/Guide-of-PAIBox.md | 130 +++++++++++++++++++++++++++++----------- 2 files changed, 103 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5473a4ee..39c02c7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,7 +66,7 @@ 1. 子网络现在直接在主网络内部 `self.subnet=...` 例化即可 2. 编译选项现在直接通过 `paibox.Mapper.compile(...)` 传入,默认配置不变 - 3. 在 `paibox.Mapper.export()` 中使用 `split_by_chip` 指定配置帧文件是否以芯片分割,默认不分割。原 `split_by_coord` 弃用 + 3. 在 `paibox.Mapper.export()` 中通过 `split_by_chip` 选项指定配置帧文件是否以芯片分割,默认不分割。原 `split_by_coord` 选项移除 ## v1.1.1 @@ -82,3 +82,11 @@ - 提高 `paicorelib` 依赖版本至 `>=1.3.1` - 支持1D脉冲平均/最大池化算子 - 重构路由算法,支持嵌套路由 + +## v1.2.0 + +- 支持 ANN 模式下半折叠算子的构建与部署 +- 支持分组卷积,包括 SNN、全展开形式与半折叠形式卷积 +- 优化后端构建网络的流程 +- 修复当在编译时开启 `core_estimate_only` 选项后导出时将报错的错误。现在在开启此选项后无法导出 +- 修复了编译后网络的属性 `inherent_timestep` (即第一次输出有效计算结果的时刻)错误的计算方法。现在将通过标注数据流格式的方式计算得到该属性 diff --git a/docs/Guide-of-PAIBox.md b/docs/Guide-of-PAIBox.md index 9c648cce..06db932b 100644 --- a/docs/Guide-of-PAIBox.md +++ b/docs/Guide-of-PAIBox.md @@ -6,7 +6,7 @@ python = "^3.9" pydantic = "^2.0.3" numpy = "^1.26.0" -paicorelib = "~1.3" +paicorelib = ">=1.3.1" ``` 可选依赖: @@ -59,7 +59,7 @@ n1 = pb.IF(shape=10, threshold=127, reset_v=0, neg_threshold=-100, keep_shape=Tr - `shape`:代表神经元组的尺寸,其形式可以是整形标量、元组或列表。 - `threshold`:神经元阈值,其形式为整数。 -- `reset_v`:神经元的复位电位,可选参数。当指定时,神经元在发放后,进行硬复位( `v = resetv` );当未指定时,进行软复位( `v -= pos_threshold` )。默认进行软复位。 +- `reset_v`:神经元的复位电位,可选参数。当指定时,神经元在发放后,进行硬复位( `v=resetv` );当未指定时,进行软复位( `v-=pos_thres` )。默认进行软复位。 - `neg_threshold`:负阈值,神经元膜电位所允许的最小值,必须是非正整数。当未指定时,默认为硬件所允许的最小负整数。 - `delay`:设定神经元输出的延迟。默认为1,即本时间步的计算结果,**下一时间步**传递至后继节点。 - `tick_wait_start`:设定神经元启动时间。神经元将在第 `T` 个时间步时启动。0表示不启动。默认为1。 @@ -107,7 +107,7 @@ n2 = pb.LIF(shape=128, threshold=10, reset_v=1, bias=-1, name='n2') ``` - `leak_v`:泄露,有符号数。 -- `bias`:偏置,有符号数。神经元将**在阈值比较前泄露**,从而实现“偏置”的效果。 `bias` 与 `leak_v` 效果将叠加。支持数组形式的偏置,这通常用于实现卷积的分通道偏置,偏置的尺寸应与神经元尺寸相关,这取决于偏置的实际含义:可以为标量(例如,线性层的偏置)、`(C,)` 数组(其中 `C` 为通道数)或 `(C,H,W)` 数组。 +- `bias`:偏置,有符号数。神经元将**在阈值比较前泄露**,从而实现“偏置”的效果。`bias` 与 `leak_v` 效果将叠加。支持数组形式的偏置,这通常用于实现卷积的分通道偏置,偏置的尺寸应与神经元尺寸相关,这取决于偏置的实际含义:可以为标量、`(C,)` 数组(其中 `C` 为通道数或输出特征数)或与本层神经元尺寸相同的数组。 - 其他参数含义与 IF 相同。 #### Tonic Spiking @@ -214,7 +214,7 @@ s1= pb.FullConn(source=n1, dest=n2, weights=weight1, conn_type=pb.SynConnType.Al 其权重以标量的形式储存。 -- 数组:尺寸要求为 `(N2,)`,可以自定义每组对应神经元之间的连接权重。如下例所示,设置 `weights` 为 `[1, 2, 3, 4, 5]`, +- 数组:尺寸要求为 `(N2,)`,可以自定义每组对应神经元之间的连接权重。如下例所示,设置 `weights` 为 `[1,2,3,4,5]`, ```python n1 = pb.IF(shape=5, threshold=1) @@ -234,16 +234,16 @@ s1= pb.FullConn(source=n1, dest=n2, weights=weight1, conn_type=pb.SynConnType.Al ##### Identity 恒等映射 -具有缩放因子的单对单连接,即 `One2One` 中权重项为标量的特殊情况。 +具有标量缩放因子的单对单连接,即 `One2One` 中权重项为标量的特殊情况。 #### 2D矩阵乘法 MatMul2d 专门用于表示二维矩阵乘法, $y=x\cdot w$ 或 $y=x^T\cdot w$ -- 例如,输入尺寸为 `(n, k)` ,权重尺寸为 `(k, m)`,输出尺寸为 `(n, m)` -- 当输入尺寸为 `(k, n)` 时,会**自动进行转置** +- 例如,输入尺寸为 `(n,k)` ,权重尺寸为 `(k,m)`,输出尺寸为 `(n,m)` +- 当输入尺寸为 `(k,n)` 时,会**自动进行转置** - 输入维度最大为2维 -- 当输入维度小于2维,将自动补齐,即 `(N, )` 补齐为 `(1, N)` +- 当输入维度小于2维,将自动补齐,即 `(N,)` 补齐为 `(1,N)` ```python n1 = pb.IF(shape=(8, 16), threshold=1) @@ -251,14 +251,14 @@ n2 = pb.IF(shape=(8, 10), threshold=1) s1 = pb.MatMul2d(source=n1, dest=n2, weights=np.ones((16, 10), dtype=np.int8)) ``` -⚠️ 不要与 `FullConn` 混淆。`FullConn` 需要传入 `N*M` 矩阵,其中 `N` 为前向神经元组数目,`M` 为后向神经元组数目。而 `MatMul2d` 中传入的矩阵尺寸并非 `N*M` ,它最终将展开为 `N*M` 矩阵。如下式所示,由于输入/输出数据在芯片中只能以一维表示,因此,它在芯片中的实现为: +⚠️ 不要与 `FullConn` 混淆。`FullConn` 需要传入 $N*M$ 矩阵,其中 $N$ 为前向神经元组数目,$M$ 为后向神经元组数目。而 `MatMul2d` 中传入的矩阵尺寸并非 $N*M$ ,它最终将展开为 $N*M$ 矩阵。如下式所示,由于输入/输出数据在芯片中只能以一维表示,因此,它在芯片中的实现为: $$ \begin{bmatrix}x_{11}& x_{12}& x_{13}\\ x_{21}& x_{22}& x_{23}\end{bmatrix}\cdot\begin{bmatrix}w_{11}& w_{12}\\ w_{21}& w_{22}\\ w_{31}& w_{32}\end{bmatrix}=\begin{bmatrix}y_{11}& y_{12}\\ y_{21}& y_{22}\end{bmatrix} $$ $$ -\begin{bmatrix}x_{11}\\ x_{12}\\ x_{13}\\ x_{21}\\ x_{22}\\ x_{23}\end{bmatrix}^T\cdot\begin{bmatrix}w_{11}& w_{12}& 0& 0\\ w_{21}& w_{22}& 0& 0\\ w_{31}& w_{32}& 0& 0\\0& 0& w_{11}& w_{12}\\0& 0& w_{21}& w_{22}\\0& 0& w_{31}& w_{32}\end{bmatrix}=\begin{bmatrix}y_{11}\\ y_{12}\\ y_{21}\\ y_{22}\end{bmatrix}^T +\begin{bmatrix}x_{11}\\ x_{12}\\ x_{13}\\ x_{21}\\ x_{22}\\ x_{23}\end{bmatrix}^T\cdot\begin{bmatrix}w_{11}& w_{12}& 0& 0\\ w_{21}& w_{22}& 0& 0\\ w_{31}& w_{32}& 0& 0\\ 0& 0& w_{11}& w_{12}\\ 0& 0& w_{21}& w_{22}\\ 0& 0& w_{31}& w_{32}\end{bmatrix}=\begin{bmatrix}y_{11}\\ y_{12}\\ y_{21}\\ y_{22}\end{bmatrix}^T $$ 对于 $y=x^T\cdot w$,将转置作用于 $w$ 即可等效实现。例如: @@ -268,7 +268,7 @@ $$ $$ $$ -\begin{bmatrix}x_{11}\\x_{12}\\x_{21}\\x_{22}\\x_{31}\\x_{32}\end{bmatrix}^T\cdot\begin{bmatrix}w_{11}& w_{12}& 0& 0\\0& 0& w_{11}& w_{12}\\ w_{21}& w_{22}& 0& 0\\ 0& 0& w_{21}& w_{22}\\ w_{31}& w_{32}& 0& 0\\0& 0& w_{31}& w_{32}\end{bmatrix}=\begin{bmatrix}y_{11}\\ y_{12}\\ y_{21}\\ y_{22}\end{bmatrix}^T +\begin{bmatrix}x_{11}\\ x_{12}\\ x_{21}\\ x_{22}\\ x_{31}\\ x_{32}\end{bmatrix}^T\cdot\begin{bmatrix}w_{11}& w_{12}& 0& 0\\ 0& 0& w_{11}& w_{12}\\ w_{21}& w_{22}& 0& 0\\ 0& 0& w_{21}& w_{22}\\ w_{31}& w_{32}& 0& 0\\ 0& 0& w_{31}& w_{32}\end{bmatrix}=\begin{bmatrix}y_{11}\\ y_{12}\\ y_{21}\\ y_{22}\end{bmatrix}^T $$ #### 1D卷积 @@ -277,7 +277,7 @@ $$ - `kernel`:卷积核权重。 - `stride`:步长,标量。默认为1。 -- `padding`:填充,标量。 +- `padding`:填充,标量。默认为0。 - `kernel_order`:指定卷积核维度顺序为 `OIL` 或 `IOL` 排列。默认为 `OIL`。 - 神经元维度顺序仅支持 `CL`。 @@ -294,8 +294,8 @@ conv1d = pb.Conv1d(n1, n2, kernel=kernel, stride=1, padding=0, kernel_order="OIL 全展开形式2D卷积为全连接突触的一种特殊表达。需**严格指定**输入神经元的尺寸与维度、卷积核权重、卷积核维度顺序与步长。对于输出神经元的具体尺寸不做严格要求。 - `kernel`:卷积核权重。 -- `stride`:步长,标量或元组格式。当为标量时,对应为 `(x, x)`;当为元组时,则对应为 `(x, y)`。默认为1。 -- `padding`:填充,可以为标量或元组。当为标量时,对应为 `(x, x)`;当为元组时,则对应为 `(x, y)`。 +- `stride`:步长,标量或元组格式。当为标量时,对应为 `(x,x)`;当为元组时,则对应为 `(x,y)`。默认为1。 +- `padding`:填充,标量或元组格式。当为标量时,对应为 `(x,x)`;当为元组时,则对应为 `(x,y)`。默认为0。 - `kernel_order`:指定卷积核维度顺序为 `OIHW` 或 `IOHW` 排列。默认为 `OIHW`。 - 神经元维度顺序仅支持 `CHW`。 @@ -334,9 +334,9 @@ convt1d = pb.ConvTranspose1d(n1, n2, kernel=kernel, stride=1, padding=0, output_ 全展开形式2D转置卷积为全连接突触的一种特殊表达。需**严格指定**输入神经元的尺寸与维度、卷积核权重、卷积核维度顺序与步长。对于输出神经元的具体尺寸不做严格要求。 - `kernel`:卷积核权重。 -- `stride`:步长,可以为标量或元组。当为标量时,对应为 `(x, x)`;当为元组时,则对应为 `(x, y)`。 -- `padding`:填充,可以为标量或元组。当为标量时,对应为 `(x, x)`;当为元组时,则对应为 `(x, y)`。 -- `output_padding`:对输出特征图的一侧进行额外的填充,可以为标量或元组。当为标量时,对应为 `(x, x)`;当为元组时,则对应为 `(x, y)`。 +- `stride`:步长,可以为标量或元组。当为标量时,对应为 `(x,x)`;当为元组时,则对应为 `(x,y)`。 +- `padding`:填充,可以为标量或元组。当为标量时,对应为 `(x,x)`;当为元组时,则对应为 `(x,y)`。 +- `output_padding`:对输出特征图的一侧进行额外的填充,可以为标量或元组。当为标量时,对应为 `(x,x)`;当为元组时,则对应为 `(x,y)`。 - `kernel_order`:指定卷积核维度顺序为 `OIHW` 或 `IOHW` 排列。 - 神经元维度顺序仅支持 `CHW`。 - 参数详细含义参见:[pytorch/ConvTranspose2d](https://pytorch.org/docs/stable/generated/torch.nn.ConvTranspose2d.html#torch.nn.ConvTranspose2d) @@ -401,8 +401,8 @@ for t in range(20): 其中, - `kernel`:卷积核权重。 -- `stride`:步长,可以为标量或元组。当为标量时,对应为 `(x, x)`;当为元组时,则对应为 `(x, y)`。 -- `padding`:对输入进行填充,可以为标量或元组。当为标量时,对应为 `(x, x)`;当为元组时,则对应为 `(x, y)`。 +- `stride`:步长,可以为标量或元组。当为标量时,对应为 `(x,x)`;当为元组时,则对应为 `(x,y)`。 +- `padding`:对输入进行填充,可以为标量或元组。当为标量时,对应为 `(x,x)`;当为元组时,则对应为 `(x,y)`。 - `kernel_order`:指定卷积核维度顺序为 `OIHW` 或 `IOHW` 排列。 - `tau`:膜电位时间常数。 - `decay_input`:输入是否也会参与衰减。 @@ -645,8 +645,8 @@ class Net(pb.DynSysGroup): - `neuron_a`:第一个操作数。 - `neuron_b`:第二个操作数。 - `delay`:设定模块输出的延迟。默认为1,即本时间步的计算结果,**下一时间步**传递至后继节点。 -- `tick_wait_start`:设定模块启动时间。模块将在第 `T` 个时间步时启动。0表示不启动。默认为1。 -- `tick_wait_end`:设定模块持续工作时长。模块将持续工作 `T` 个时间步。0表示**持续工作**。默认为0。 +- `tick_wait_start`:设定模块启动时刻。模块将在第 `T` 个时间步时启动。0表示不启动。默认为1。 +- `tick_wait_end`:设定模块**持续工作**时长。模块将持续工作 `T` 个时间步。0表示**持续工作**。默认为0。 - `keep_shape`:是否在仿真记录数据时保持尺寸信息,默认为 `False`。实际进行运算的尺寸仍视为一维。 - `name`:模块的名称。可选参数。 @@ -668,14 +668,14 @@ s3 = pb.FullConn(p2d, n2, conn_type=pb.SynConnType.One2One) 其中: - `neuron`:待池化的神经元。 -- `kernel_size`:池化窗口的尺寸,标量或元组格式。当为标量时,对应为 `(x, x)`;当为元组时,则对应为 `(x, y)`。 +- `kernel_size`:池化窗口的尺寸,标量或元组格式。当为标量时,对应为 `(x,x)`;当为元组时,则对应为 `(x,y)`。 - `stride`:步长,可选参数,标量或元组格式,默认为 `None`,即池化窗口的尺寸。 -- `padding`:填充,可以为标量或元组。当为标量时,对应为 `(x, x)`;当为元组时,则对应为 `(x, y)`。默认为0。 +- `padding`:填充,标量或元组格式。当为标量时,对应为 `(x,x)`;当为元组时,则对应为 `(x,y)`。默认为0。 - 神经元维度顺序仅支持 `CHW`。 对于平均池化 `SpikingAvgPool2d`,它还有如下参数可配置: -- `threshold`:平均池化的比较阈值,芯片需要通过神经元的阈值比较间接地实现除法。当不指定时,阈值为 $\text{round}(\text{kernel\_size}/2)$。池化窗口的输入做累加后与该阈值进行比较,可等价于平均池化的操作,即 $o_j=\sum^{k-1}_{i=0}x_{ij} \ge V_{th,pos}$,其中 $k$ 为池化窗口尺寸,$x_{ij}$ 为每个池化窗口内的输入特征图元素,$o_j$ 为第 $j$ 个输出特征图元素。 +- `threshold`:平均池化的比较阈值,芯片需要通过神经元的阈值比较间接地实现除法。当不指定时,阈值为 $\text{round}(\text{ksize}/2)$。池化窗口的输入做累加后与该阈值进行比较,可等价于平均池化的操作,即 $o_j=\sum^{k-1}_{i=0}x_{ij} \ge V_{th,pos}$,其中 $k$ 为池化窗口尺寸,$x_{ij}$ 为每个池化窗口内的输入特征图元素,$o_j$ 为第 $j$ 个输出特征图元素。 ### 2D平均池化(膜电位相关) @@ -709,7 +709,7 @@ $$ V_{pre} + a\cdot f_a + b\cdot f_b \ge V_{th,pos} $$ -对于 `SpikingSub`,$f_a=1$,$f_b=-1$. +对于 `SpikingSub`, $f_a=1$, $f_b=-1$。 ```python n1 = pb.IF((10,), 1, 0, delay=1, tick_wait_start=1) @@ -722,10 +722,9 @@ sub1 = pb.SpikingSub(n1, n2, overflow_strict=False, delay=1, tick_wait_start=2) - `neuron_a`:第一个操作数。 - `neuron_b`:第二个操作数。在减法中作被减数。 -- `factor_a`:第一个操作数的缩放因子,正整数标量。默认为1,仅在 `SpikingAdd` 中使用。 -- `factor_b`:第一个操作数的缩放因子,正整数标量。默认为1,仅在 `SpikingAdd` 中使用。 +- `factor_a/b`:第一/二个操作数的缩放因子,正整数标量。默认为1,仅在 `SpikingAdd` 中使用。 - `pos_thres`:正阈值。默认为1,仅在 `SpikingAdd` 中使用。 -- `reset_v`:复位电位,可选参数。当指定时,神经元在发放后,进行硬复位( `v = resetv` );当未指定时,进行软复位( `v -= pos_threshold` )。默认进行软复位,仅在 `SpikingAdd` 中使用。 +- `reset_v`:复位电位,可选参数。当指定时,神经元在发放后,进行硬复位( `v=resetv` );当未指定时,进行软复位( `v-=pos_thres` )。默认进行软复位,仅在 `SpikingAdd` 中使用。 - `overflow_strict`:是否严格检查运算结果溢出。如果启用,则在仿真中,当脉冲加、减运算结果溢出时将报错。默认为 `False`。 ### 2D/3D转置 @@ -747,6 +746,65 @@ t3d = pb.Transpose3d(n2, axes=(1, 2, 0), tick_wait_start=2) - `neuron`:待转置其输出脉冲的神经元。对于二维转置,支持输入尺寸为1或2维;对于三维转置,支持输入尺寸为2或3维。尺寸不足时,自动补1。 - `axes`:(仅三维转置)如果指定,则必须是包含 `[0,1,…,N-1]` 排列的元组或列表,其中 `N` 是矩阵的轴(维度)数。返回数组的第 `i` 轴将对应于输入的编号为 `axes[i]` 的轴。若未指定,则默认为 `range(N)[::-1]`,这将反转轴的顺序。具体参数含义参见:[numpy.transpose](https://numpy.org/doc/1.26/reference/generated/numpy.transpose.html#numpy.transpose) +### 线性层 + +适用于 ANN 的线性层。 + +```python +n1 = pb.ANNNeuron((1024,)) +l1 = pb.Linear(n1, 10, w, bias=10, bit_trunc=8) +``` + +其中: + +- `neuron_s`:输入特征图(神经元)。 +- `out_features`:输出特征,可以理解为输出神经元。 +- `weights`:权重矩阵。 +- `bias`:偏置,有符号数。可以为标量或 `(out_features,)` 数组。 +- `bit_trunc`:神经元输出的8位无符号数的截断位置。默认为8,即截取 [7:0] 位。 + +### 半折叠形式算子 + +以下算子仅适用于ANN,且对数据流形式有严格要求,因此要求神经网络中的所有算子均为半折叠形式。 + +当神经网络采用半折叠形式时,对于尺寸为 `(C,H,W)` 的特征图,将展开为 `W*(C,H)` 的形式输入,即对于一张特征图需要 `W` 个时间步完成输入(`H` 与 `W` 地位相同,可以互换)。在例化半折叠形式的算子时,卷积核的尺寸依然为(本层的) `(O,I,K,K)`,然而中间特征图的尺寸却减小为 `(C,H)`,`W` 维度被折叠。这显著减少了芯片内所需存储的中间特征图尺寸。作为代价,半折叠形式的卷积(类)算子需至少 `Ow `个时间步才完全输出,其中 `Ow` 为本层的输出特征图宽度。这使得网络模型的推理(得到第一次有效输出数据的)耗时增加。 + +#### 半折叠2D卷积 + +- `neuron_s`:输入特征图(神经元),要求为半折叠算子或输入节点。 +- `kernel`:卷积核权重,维度顺序为 `OIHW`。 +- `stride`:步长,标量或元组格式。当为标量时,对应为 `(x,x)`;当为元组时,则对应为 `(x,y)`。默认为1。 + +- `bias`:偏置,有符号数。可以为标量或 `(C,)` 数组。默认为0。 +- `bit_trunc`:神经元输出的8位无符号数的截断位置。默认为8,即截取 [7:0] 位。 + +#### 半折叠2D最大池化 + +- `neuron_s`:输入特征图(神经元),要求为半折叠算子或输入节点。 +- `kernel_size`:池化窗口的尺寸,标量或元组格式。当为标量时,对应为 `(x,x)`;当为元组时,则对应为 `(x, y)`。 +- `stride`:步长,标量或元组格式。当为标量时,对应为 `(x,x)`;当为元组时,则对应为 `(x,y)`。默认为 `None`,即池化窗口的尺寸。 +- `bit_trunc`:神经元输出的8位无符号数的截断位置。默认为8,即截取 [7:0] 位。 + +⚠️ 半折叠最大池化不支持 padding。 + +#### 半折叠2D平均池化 + +- `neuron_s`:输入特征图(神经元),要求为半折叠算子或输入节点。 +- `kernel_size`:池化窗口的尺寸,标量或元组格式。当为标量时,对应为 `(x,x)`;当为元组时,则对应为 `(x, y)`。 +- `stride`:步长,标量或元组格式。当为标量时,对应为 `(x,x)`;当为元组时,则对应为 `(x,y)`。默认为 `None`,即池化窗口的尺寸。 + +- `bit_trunc`:神经元输出的8位无符号数的截断位置。默认为 `8+ksize.bit_length()-1`,其中 `ksize` 为池化窗口的尺寸。注意,由于平均池化依赖除法实现,而芯片计算核只能通过右移位实现2的整数幂除法。当池化窗口尺寸不为2的整数幂时,只能近似通过上式有损计算除法。例如,当池化窗口为 (3,3) 时,最终将 /8,而非 /9。这样的近似误差可以考虑在量化阶段,为后续层的权重 $w\cdot 8/9$ 而减小。 + +#### 半折叠线性层 + +半折叠线性层接受的输入为半折叠形式,而其计算结果在单一时间步上输出。即它将半折叠形式的数据流转换为全展开形式的数据流。 + +- `neuron_s`:输入特征图(神经元),要求为半折叠算子或输入节点。 +- `out_features`:输出特征,可以理解为输出神经元。 +- `weights`:权重矩阵。 +- `bias`:偏置,有符号数。可以为标量或 `(out_features,)` 数组。 +- `bit_trunc`:神经元输出的8位无符号数的截断位置。默认为8,即截取 [7:0] 位。 + ## 网络模型 在 PAIBox 中,可以通过继承 `DynSysGroup`(或 `Network`)来实现,并在其中例化基础组件与功能模块,完成网络模型的构建。以一个简单的两层全连接网络为例: @@ -915,14 +973,14 @@ sim.reset() 调用 `run` 运行仿真,其中: -- `duration`:指定仿真时间步长。请注意,仿真时需要计算网络的最长路径(delay),并计入仿真步长中以获取有效的输出。 +- `duration`:指定仿真时间步长。请注意,仿真步长需要大于网络模型的有效层数,才会得到并记录有效的仿真数据。 - `reset`:是否对网络模型中组件进行复位。默认为 `False`。这可实现在一次仿真的不同时间步,输入不同的数据。 ## 编译、映射与导出 模型映射将完成网络拓扑解析、分割、路由坐标分配、配置信息与帧文件导出等一系列工作。 -例化 `Mapper`,传入所构建的网络模型,编译,最后导出。 +例化 `Mapper`,传入所构建的网络模型,构建、编译、导出。 ```python mapper = pb.Mapper() @@ -939,19 +997,19 @@ mapper.clear() 其中,编译时有如下参数可指定: -- `core_estimate_only`:仅导出预估所需核数目,不进行后续部署。默认关闭。 -- `weight_bit_optimization`: 是否对权重精度进行优化处理。这将使得声明时为 INT8 的权重根据实际值当作更小的精度处理(当权重的值均在 [-8, 7] 之间,则可当作 INT4 进行处理)。默认开启。 +- `core_estimate_only`:仅导出预估所需核数目,不进行后续部署。默认关闭。当启用此项时,编译工作未全部进行,因此无法导出任何信息。 +- `weight_bit_optimization`: 是否对权重精度进行优化处理。这将使得声明时为 INT8 的权重根据实际值当作更小的精度处理。例如,当权重的值均在 [-8, 7] 之间,则可当作 INT4 进行处理。默认开启。 - `grouping_optim_target`:指定神经元分组的优化目标,可以为 `"latency"`,`"core"` 或 `"both"`,分别代表以延时/吞吐率、占用核资源为优化目标、或二者兼顾。默认 `both`。 -- 同时,该方法将返回字典形式的编译后网络的信息。 +- 将返回字典形式的编译后网络的信息。 导出时有如下参数可指定: - `write_to_file`: 是否将配置帧导出为文件。默认为 `True`。 - `fp`:导出目录。若未指定,则默认为后端配置选项 `build_directory` 所设置的目录(当前工作目录)。 - `format`:导出交换文件格式,可以为 `bin`、`npy` 或 `txt`。默认为 `bin`。 -- `split_by_chip`:是否将配置帧以芯片坐标进行分割,由此生成的配置帧文件命名形如"config_chip0_core0"、"config_chip0_core1"、"config_chip1_core0"。默认为 `False`,即最终导出为一个文件 "config_all"。 +- `split_by_chip`:是否将配置帧以芯片坐标进行分割,由此生成的配置帧文件命名形如"config_chip0_core0.format"、"config_chip0_core1.format"、"config_chip1_core0.format"。默认为 `False`,即最终导出为一个文件 "config_all.format"。 - `export_core_params`:是否导出实际使用核参数至 json 文件,以直观显示实际使用核的配置信息。默认为 `False`。 -- `export_clk_en_L2`:是否导出L2簇时钟串口数据。默认为 `False`。 +- `export_clk_en_L2`:是否导出 L2 簇时钟串口数据。默认为 `False`。硬件平台可根据该数据关闭芯片其他未使用的 L2 簇时钟以降低功耗。 - `use_hw_sim`:是否使用硬件仿真器。若使用,将额外导出 `bin` 格式的配置帧文件。默认为 `True`。 同时,该方法将返回模型的配置项字典 `GraphInfo`,包括: @@ -959,7 +1017,7 @@ mapper.clear() - `input`:输入节点信息字典。 - `output`:输出目的地信息字典。 - `members`:中间层所在物理核的配置项字典。 -- `inherent_timestep`:网络的最长时间步。 +- `inherent_timestep`:网络的最长时间步,即得到网络第一个有效输出数据的用时。 - `n_core_required`:网络**需要**的物理核数目。 - `n_core_occupied`:网络**实际占用**的物理核数目。 - `misc`:其他杂项信息。例如,编译后的网络名称;上述L2簇时钟串口数据在该键 `["clk_en_L2"]` 中。 From 43499f6d4b80c42d52c191fe63f683b9482ab6e7 Mon Sep 17 00:00:00 2001 From: KafCoppelia Date: Mon, 16 Dec 2024 15:30:53 +0800 Subject: [PATCH 186/187] =?UTF-8?q?=F0=9F=94=96=20v1.2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 08223a17..d08e6666 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "paibox" -version = "1.2.0a2" +version = "1.2.0" description = "Toolchain of PAICORE 2.0" authors = ["Ziru Pan "] maintainers = [ From b87de42eed9153be847101ca4b420eccb048c41c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 07:32:10 +0000 Subject: [PATCH 187/187] :rotating_light: auto fix by pre-commit hooks --- docs/Guide-of-PAIBox.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Guide-of-PAIBox.md b/docs/Guide-of-PAIBox.md index 06db932b..9003b4e1 100644 --- a/docs/Guide-of-PAIBox.md +++ b/docs/Guide-of-PAIBox.md @@ -767,7 +767,7 @@ l1 = pb.Linear(n1, 10, w, bias=10, bit_trunc=8) 以下算子仅适用于ANN,且对数据流形式有严格要求,因此要求神经网络中的所有算子均为半折叠形式。 -当神经网络采用半折叠形式时,对于尺寸为 `(C,H,W)` 的特征图,将展开为 `W*(C,H)` 的形式输入,即对于一张特征图需要 `W` 个时间步完成输入(`H` 与 `W` 地位相同,可以互换)。在例化半折叠形式的算子时,卷积核的尺寸依然为(本层的) `(O,I,K,K)`,然而中间特征图的尺寸却减小为 `(C,H)`,`W` 维度被折叠。这显著减少了芯片内所需存储的中间特征图尺寸。作为代价,半折叠形式的卷积(类)算子需至少 `Ow `个时间步才完全输出,其中 `Ow` 为本层的输出特征图宽度。这使得网络模型的推理(得到第一次有效输出数据的)耗时增加。 +当神经网络采用半折叠形式时,对于尺寸为 `(C,H,W)` 的特征图,将展开为 `W*(C,H)` 的形式输入,即对于一张特征图需要 `W` 个时间步完成输入(`H` 与 `W` 地位相同,可以互换)。在例化半折叠形式的算子时,卷积核的尺寸依然为(本层的) `(O,I,K,K)`,然而中间特征图的尺寸却减小为 `(C,H)`,`W` 维度被折叠。这显著减少了芯片内所需存储的中间特征图尺寸。作为代价,半折叠形式的卷积(类)算子需至少 `Ow `个时间步才完全输出,其中 `Ow` 为本层的输出特征图宽度。这使得网络模型的推理(得到第一次有效输出数据的)耗时增加。 #### 半折叠2D卷积