sparsepixels 0.1.1__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sparsepixels
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Efficient convolution for sparse data on FPGAs
5
5
  Home-page: https://github.com/hftsoi/sparse-pixels
6
6
  Author: Ho Fung Tsoi
@@ -8,13 +8,12 @@ Author-email: ho.fung.tsoi@cern.ch
8
8
  License: MIT
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: License :: OSI Approved :: MIT License
11
- Requires-Python: <3.12,>=3.10
11
+ Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
- Requires-Dist: tensorflow<2.15.0,>=2.14.0
15
- Requires-Dist: keras<2.15.0,>=2.14.0
16
- Requires-Dist: qkeras<0.10.0,>=0.9.0
17
- Requires-Dist: pyparsing<3.4.0,>=3.3.0
14
+ Requires-Dist: tensorflow
15
+ Requires-Dist: keras>=3.0
16
+ Requires-Dist: HGQ2
18
17
  Dynamic: license-file
19
18
 
20
19
  <p align="center">
@@ -35,39 +34,65 @@ Dynamic: license-file
35
34
  [![arXiv](https://img.shields.io/badge/arXiv-2512.06208-b31b1b.svg?style=flat-square)](https://arxiv.org/abs/2512.06208)
36
35
  [![PyPI - Version](https://img.shields.io/pypi/v/sparsepixels?color=orange&style=flat-square)](https://pypi.org/project/sparsepixels)
37
36
 
38
- > **Note:** we are actively working on making this usable soon, before integrating into hls4ml (first qkeras with keras2, and then HGQ with keras3), we are also working on a major upgrade with partial paralleliztion and streaming for sparse layers in HLS. stay tuned!!
37
+ > **Note:** We are actively working on hls4ml integration to auto-convert sparse models to HLS, along with a major upgrade with partial parallelization and streaming for sparse layers in HLS. Stay tuned!
39
38
 
40
39
  ## Installation
41
- With Python 3.10 or 3.11 (for now):
40
+
41
+ With Python >= 3.10:
42
+
42
43
  ```
43
44
  pip install sparsepixels
44
45
  ```
45
46
 
46
47
  ## Getting Started
47
- On the model training in Python, import sparse layers:
48
- ```
49
- from sparsepixels.layers import *
50
- ```
51
- Sparse input reduction:
52
- ```
53
- x_in = keras.Input(shape=(x_train.shape[1], x_train.shape[2], x_train.shape[3]), name='x_in')
54
- x, keep_mask = InputReduce(n_max_pixels=n_max_pixels, threshold=threshold, name='input_reduce')(x_in)
55
- ```
56
- Sparse convolution:
57
- ```
58
- x = QConv2DSparse(filters=1, kernel_size=7, use_bias=True, name='conv1', padding='same', strides=1,
59
- kernel_quantizer=quantizer, bias_quantizer=quantizer)([x, keep_mask])
60
- ```
61
- Sparse pooling:
48
+
49
+ Import sparse layers and quantization library (HGQ2):
50
+
51
+ ```python
52
+ import keras
53
+ from keras.layers import Flatten, Activation, ReLU
54
+ from hgq.layers import QConv2D, QDense
55
+ from hgq.config import QuantizerConfigScope, LayerConfigScope
56
+ from hgq.quantizer.config import QuantizerConfig
57
+ from sparsepixels.layers import InputReduce, QConv2DSparse, AveragePooling2DSparse
62
58
  ```
63
- x, keep_mask = AveragePooling2DSparse(4, name='pool1')([x, keep_mask])
59
+
60
+ Build an example sparse CNN within HGQ2 quantization scopes:
61
+
62
+ ```python
63
+ with (
64
+ QuantizerConfigScope(place='all', default_q_type='kbi', overflow_mode='SAT_SYM'),
65
+ QuantizerConfigScope(place='datalane', default_q_type='kif', overflow_mode='WRAP'),
66
+ LayerConfigScope(enable_ebops=False, enable_iq=False),
67
+ ):
68
+ x_in = keras.Input(shape=(x_train.shape[1], x_train.shape[2], x_train.shape[3]), name='x_in')
69
+
70
+ # Sparse input reduction: retain up to n_max_pixels active pixels
71
+ x, keep_mask = InputReduce(n_max_pixels=20, threshold=0.1, name='input_reduce')(x_in)
72
+
73
+ # Sparse convolution
74
+ x = QConv2DSparse(filters=3, kernel_size=3, name='conv1', padding='same', strides=1,
75
+ bq_conf=QuantizerConfig('default', 'bias'))([x, keep_mask])
76
+ x = ReLU(name='relu1')(x)
77
+
78
+ # Sparse pooling
79
+ x, keep_mask = AveragePooling2DSparse(2, name='pool1')([x, keep_mask])
80
+
81
+ x = Flatten(name='flatten')(x)
82
+ x = QDense(10, name='dense1', activation='relu')(x)
83
+ x = Activation('softmax', name='softmax')(x)
84
+
85
+ model = keras.Model(x_in, x)
64
86
  ```
87
+
65
88
  We are working on hls4ml integration that auto parses the sparse layers into HLS.
66
89
 
67
90
  ## Documentation
68
91
 
69
92
  ## Citation
93
+
70
94
  If you find this useful in your research, please consider citing:
95
+
71
96
  ```
72
97
  @article{Tsoi:2025nvg,
73
98
  author = "Tsoi, Ho Fung and Rankin, Dylan and Loncar, Vladimir and Harris, Philip",
@@ -79,3 +104,4 @@ If you find this useful in your research, please consider citing:
79
104
  year = "2025"
80
105
  }
81
106
  ```
107
+
@@ -0,0 +1,89 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/logo.png" width="300" />
3
+ </p>
4
+
5
+ <p align="center">
6
+ <img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/sparsepixels.png" width="900"/>
7
+ </p>
8
+
9
+ <p align="center">
10
+ <img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/cnn_standard.gif" width="400" />
11
+ <img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/cnn_sparse.gif" width="400" />
12
+ </p>
13
+
14
+ # SparsePixels: Efficient convolution for sparse data on FPGAs
15
+
16
+ [![arXiv](https://img.shields.io/badge/arXiv-2512.06208-b31b1b.svg?style=flat-square)](https://arxiv.org/abs/2512.06208)
17
+ [![PyPI - Version](https://img.shields.io/pypi/v/sparsepixels?color=orange&style=flat-square)](https://pypi.org/project/sparsepixels)
18
+
19
+ > **Note:** We are actively working on hls4ml integration to auto-convert sparse models to HLS, along with a major upgrade with partial parallelization and streaming for sparse layers in HLS. Stay tuned!
20
+
21
+ ## Installation
22
+
23
+ With Python >= 3.10:
24
+
25
+ ```
26
+ pip install sparsepixels
27
+ ```
28
+
29
+ ## Getting Started
30
+
31
+ Import sparse layers and quantization library (HGQ2):
32
+
33
+ ```python
34
+ import keras
35
+ from keras.layers import Flatten, Activation, ReLU
36
+ from hgq.layers import QConv2D, QDense
37
+ from hgq.config import QuantizerConfigScope, LayerConfigScope
38
+ from hgq.quantizer.config import QuantizerConfig
39
+ from sparsepixels.layers import InputReduce, QConv2DSparse, AveragePooling2DSparse
40
+ ```
41
+
42
+ Build an example sparse CNN within HGQ2 quantization scopes:
43
+
44
+ ```python
45
+ with (
46
+ QuantizerConfigScope(place='all', default_q_type='kbi', overflow_mode='SAT_SYM'),
47
+ QuantizerConfigScope(place='datalane', default_q_type='kif', overflow_mode='WRAP'),
48
+ LayerConfigScope(enable_ebops=False, enable_iq=False),
49
+ ):
50
+ x_in = keras.Input(shape=(x_train.shape[1], x_train.shape[2], x_train.shape[3]), name='x_in')
51
+
52
+ # Sparse input reduction: retain up to n_max_pixels active pixels
53
+ x, keep_mask = InputReduce(n_max_pixels=20, threshold=0.1, name='input_reduce')(x_in)
54
+
55
+ # Sparse convolution
56
+ x = QConv2DSparse(filters=3, kernel_size=3, name='conv1', padding='same', strides=1,
57
+ bq_conf=QuantizerConfig('default', 'bias'))([x, keep_mask])
58
+ x = ReLU(name='relu1')(x)
59
+
60
+ # Sparse pooling
61
+ x, keep_mask = AveragePooling2DSparse(2, name='pool1')([x, keep_mask])
62
+
63
+ x = Flatten(name='flatten')(x)
64
+ x = QDense(10, name='dense1', activation='relu')(x)
65
+ x = Activation('softmax', name='softmax')(x)
66
+
67
+ model = keras.Model(x_in, x)
68
+ ```
69
+
70
+ We are working on hls4ml integration that auto parses the sparse layers into HLS.
71
+
72
+ ## Documentation
73
+
74
+ ## Citation
75
+
76
+ If you find this useful in your research, please consider citing:
77
+
78
+ ```
79
+ @article{Tsoi:2025nvg,
80
+ author = "Tsoi, Ho Fung and Rankin, Dylan and Loncar, Vladimir and Harris, Philip",
81
+ title = "{SparsePixels: Efficient Convolution for Sparse Data on FPGAs}",
82
+ eprint = "2512.06208",
83
+ archivePrefix = "arXiv",
84
+ primaryClass = "cs.AR",
85
+ month = "12",
86
+ year = "2025"
87
+ }
88
+ ```
89
+
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = sparsepixels
3
- version = 0.1.1
3
+ version = 0.2.0
4
4
  description = Efficient convolution for sparse data on FPGAs
5
5
  author = Ho Fung Tsoi
6
6
  author_email = ho.fung.tsoi@cern.ch
@@ -15,12 +15,11 @@ classifiers =
15
15
 
16
16
  [options]
17
17
  packages = find_namespace:
18
- python_requires = >=3.10,<3.12
18
+ python_requires = >=3.10
19
19
  install_requires =
20
- tensorflow>=2.14.0,<2.15.0
21
- keras>=2.14.0,<2.15.0
22
- qkeras>=0.9.0,<0.10.0
23
- pyparsing>=3.3.0,<3.4.0
20
+ tensorflow
21
+ keras>=3.0
22
+ HGQ2
24
23
  include_package_data = True
25
24
 
26
25
  [options.package_data]
@@ -1,93 +1,97 @@
1
- import tensorflow as tf
2
- from qkeras import QConv2D, quantizers
3
- from tensorflow.keras.layers import AveragePooling2D, MaxPooling2D
1
+ import keras
2
+ from keras import ops
3
+ from keras.layers import AveragePooling2D, MaxPooling2D
4
+ from hgq.layers import QConv2D
5
+ from hgq.quantizer import Quantizer
6
+ from hgq.quantizer.config import QuantizerConfig
4
7
 
5
- class InputReduce(tf.keras.layers.Layer):
8
+
9
+ class InputReduce(keras.layers.Layer):
6
10
  def __init__(self, n_max_pixels, threshold, **kwargs):
7
- super(InputReduce, self).__init__(**kwargs)
11
+ super().__init__(**kwargs)
8
12
  self.n_max_pixels = n_max_pixels
9
13
  self.threshold = threshold
10
14
 
11
15
  def call(self, inputs):
12
- batch_size = tf.shape(inputs)[0]
13
- h = tf.shape(inputs)[1]
14
- w = tf.shape(inputs)[2]
15
-
16
- '''
17
- if self.threshold is not None:
18
- cond = inputs > self.threshold
19
- else:
20
- cond = inputs != 0
21
- active_flag = tf.cast(tf.reduce_any(cond, axis=-1), tf.int32)
22
- '''
16
+ batch_size = ops.shape(inputs)[0]
17
+ h = ops.shape(inputs)[1]
18
+ w = ops.shape(inputs)[2]
23
19
 
24
20
  # to be consistent with hls, check only the first input channel
25
21
  if self.threshold is not None:
26
- active_flag = tf.cast(inputs[..., 0] > self.threshold, tf.int32)
22
+ active_flag = ops.cast(inputs[..., 0] > self.threshold, "int32")
27
23
  else:
28
- active_flag = tf.cast(inputs[..., 0] != 0, tf.int32)
24
+ active_flag = ops.cast(inputs[..., 0] != 0, "int32")
29
25
 
30
- active_flag_flat = tf.reshape(active_flag, [batch_size, h * w])
31
- active_count = tf.cumsum(active_flag_flat, axis=1)
26
+ active_flag_flat = ops.reshape(active_flag, [batch_size, h * w])
27
+ active_count = ops.cumsum(active_flag_flat, axis=1)
32
28
 
33
- keep_mask_flat = tf.cast(tf.logical_and(active_flag_flat == 1, active_count <= self.n_max_pixels), inputs.dtype)
34
- keep_mask = tf.reshape(keep_mask_flat, [batch_size, h, w, 1])
29
+ keep_mask_flat = ops.cast(
30
+ ops.logical_and(active_flag_flat == 1, active_count <= self.n_max_pixels),
31
+ inputs.dtype,
32
+ )
33
+ keep_mask = ops.reshape(keep_mask_flat, [batch_size, h, w, 1])
35
34
 
36
35
  inputs_reduced = inputs * keep_mask
37
36
  return inputs_reduced, keep_mask
38
37
 
39
38
  def get_config(self):
40
- config = super(InputReduce, self).get_config()
39
+ config = super().get_config()
41
40
  config.update({
42
41
  "n_max_pixels": self.n_max_pixels,
43
- "threshold": self.threshold
42
+ "threshold": self.threshold,
44
43
  })
45
44
  return config
46
-
47
45
 
48
- class RemoveDilatedPixels(tf.keras.layers.Layer):
46
+
47
+ class RemoveDilatedPixels(keras.layers.Layer):
49
48
  def __init__(self, **kwargs):
50
- super(RemoveDilatedPixels, self).__init__(**kwargs)
49
+ super().__init__(**kwargs)
51
50
 
52
51
  def call(self, inputs):
53
52
  x, mask = inputs
54
- mask = tf.cast(mask, x.dtype)
55
- removed = x * mask
56
- return removed
53
+ mask = ops.cast(mask, x.dtype)
54
+ return x * mask
57
55
 
58
56
  def get_config(self):
59
- config = super(RemoveDilatedPixels, self).get_config()
60
- return config
57
+ return super().get_config()
61
58
 
62
59
 
63
- class QConv2DSparse(tf.keras.layers.Layer):
60
+ class QConv2DSparse(keras.layers.Layer):
64
61
  def __init__(self, *conv_args, **conv_kwargs):
65
62
  super().__init__(name=conv_kwargs.get("name", None))
66
- self._bias_quant_cfg = conv_kwargs.pop("bias_quantizer", None)
67
- self._bias_quantizer = (quantizers.get_quantizer(self._bias_quant_cfg) if self._bias_quant_cfg is not None else None)
63
+ self._bq_conf = conv_kwargs.pop("bq_conf", None)
68
64
 
69
65
  conv_kwargs["use_bias"] = False
66
+ conv_kwargs.setdefault("enable_iq", False)
70
67
  self.conv = QConv2D(*conv_args, **conv_kwargs)
71
- self.bias = self.add_weight(
72
- name = "bias",
73
- shape = (self.conv.filters,),
74
- initializer = "zeros",
75
- trainable = True,
76
- dtype = self.conv.dtype,
77
- )
78
68
  self.masker = RemoveDilatedPixels()
79
69
 
70
+ def build(self, input_shape):
71
+ self.sparse_bias = self.add_weight(
72
+ name="sparse_bias",
73
+ shape=(self.conv.filters,),
74
+ initializer="zeros",
75
+ trainable=True,
76
+ )
77
+ if self._bq_conf is not None:
78
+ self._bq = Quantizer(self._bq_conf, name=f"{self.name}_bq")
79
+ self._bq.build((self.conv.filters,))
80
+ else:
81
+ self._bq = None
82
+ super().build(input_shape)
83
+
80
84
  def call(self, inputs, **kwargs):
81
85
  x, keep_mask = inputs
82
86
  x = self.masker((x, keep_mask))
83
87
  y = self.conv(x, **kwargs)
84
88
 
85
- b = self.bias
86
- if self._bias_quantizer is not None:
87
- b = self._bias_quantizer(b)
88
- b = tf.reshape(b, shape=(1, 1, 1, -1))
89
+ b = self.sparse_bias
90
+ if self._bq is not None:
91
+ b = self._bq(b)
92
+ b = ops.reshape(b, (1, 1, 1, -1))
89
93
 
90
- non_zero = tf.cast(tf.not_equal(y, 0), y.dtype)
94
+ non_zero = ops.cast(y != 0, y.dtype)
91
95
  y = y + b * non_zero
92
96
 
93
97
  y = self.masker((y, keep_mask))
@@ -96,18 +100,17 @@ class QConv2DSparse(tf.keras.layers.Layer):
96
100
  def get_config(self):
97
101
  cfg = super().get_config()
98
102
  cfg["conv_config"] = self.conv.get_config()
99
- cfg["bias_quantizer"] = self._bias_quant_cfg
103
+ cfg["bq_conf"] = self._bq_conf
100
104
  return cfg
101
105
 
102
106
  @classmethod
103
107
  def from_config(cls, config):
104
108
  conv_cfg = config.pop("conv_config")
105
- bias_quant_cfg = config.pop("bias_quantizer", None)
106
- layer = cls(**conv_cfg, bias_quantizer=bias_quant_cfg)
107
- return layer
109
+ bq_conf = config.pop("bq_conf", None)
110
+ return cls(**conv_cfg, bq_conf=bq_conf)
108
111
 
109
112
 
110
- class AveragePooling2DSparse(tf.keras.layers.Layer):
113
+ class AveragePooling2DSparse(keras.layers.Layer):
111
114
  def __init__(self, *pool_args, **pool_kwargs):
112
115
  super().__init__(name=pool_kwargs.get("name", None))
113
116
  self.avg_pool = AveragePooling2D(*pool_args, **pool_kwargs)
@@ -128,9 +131,9 @@ class AveragePooling2DSparse(tf.keras.layers.Layer):
128
131
  def from_config(cls, config):
129
132
  pool_cfg = config.pop("pool_config")
130
133
  return cls(**pool_cfg)
131
-
132
134
 
133
- class MaxPooling2DSparse(tf.keras.layers.Layer):
135
+
136
+ class MaxPooling2DSparse(keras.layers.Layer):
134
137
  def __init__(self, *pool_args, **pool_kwargs):
135
138
  super().__init__(name=pool_kwargs.get("name", None))
136
139
  self.max_pool = MaxPooling2D(*pool_args, **pool_kwargs)
@@ -150,4 +153,3 @@ class MaxPooling2DSparse(tf.keras.layers.Layer):
150
153
  def from_config(cls, config):
151
154
  pool_cfg = config.pop("pool_config")
152
155
  return cls(**pool_cfg)
153
-
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sparsepixels
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Efficient convolution for sparse data on FPGAs
5
5
  Home-page: https://github.com/hftsoi/sparse-pixels
6
6
  Author: Ho Fung Tsoi
@@ -8,13 +8,12 @@ Author-email: ho.fung.tsoi@cern.ch
8
8
  License: MIT
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: License :: OSI Approved :: MIT License
11
- Requires-Python: <3.12,>=3.10
11
+ Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
- Requires-Dist: tensorflow<2.15.0,>=2.14.0
15
- Requires-Dist: keras<2.15.0,>=2.14.0
16
- Requires-Dist: qkeras<0.10.0,>=0.9.0
17
- Requires-Dist: pyparsing<3.4.0,>=3.3.0
14
+ Requires-Dist: tensorflow
15
+ Requires-Dist: keras>=3.0
16
+ Requires-Dist: HGQ2
18
17
  Dynamic: license-file
19
18
 
20
19
  <p align="center">
@@ -35,39 +34,65 @@ Dynamic: license-file
35
34
  [![arXiv](https://img.shields.io/badge/arXiv-2512.06208-b31b1b.svg?style=flat-square)](https://arxiv.org/abs/2512.06208)
36
35
  [![PyPI - Version](https://img.shields.io/pypi/v/sparsepixels?color=orange&style=flat-square)](https://pypi.org/project/sparsepixels)
37
36
 
38
- > **Note:** we are actively working on making this usable soon, before integrating into hls4ml (first qkeras with keras2, and then HGQ with keras3), we are also working on a major upgrade with partial paralleliztion and streaming for sparse layers in HLS. stay tuned!!
37
+ > **Note:** We are actively working on hls4ml integration to auto-convert sparse models to HLS, along with a major upgrade with partial parallelization and streaming for sparse layers in HLS. Stay tuned!
39
38
 
40
39
  ## Installation
41
- With Python 3.10 or 3.11 (for now):
40
+
41
+ With Python >= 3.10:
42
+
42
43
  ```
43
44
  pip install sparsepixels
44
45
  ```
45
46
 
46
47
  ## Getting Started
47
- On the model training in Python, import sparse layers:
48
- ```
49
- from sparsepixels.layers import *
50
- ```
51
- Sparse input reduction:
52
- ```
53
- x_in = keras.Input(shape=(x_train.shape[1], x_train.shape[2], x_train.shape[3]), name='x_in')
54
- x, keep_mask = InputReduce(n_max_pixels=n_max_pixels, threshold=threshold, name='input_reduce')(x_in)
55
- ```
56
- Sparse convolution:
57
- ```
58
- x = QConv2DSparse(filters=1, kernel_size=7, use_bias=True, name='conv1', padding='same', strides=1,
59
- kernel_quantizer=quantizer, bias_quantizer=quantizer)([x, keep_mask])
60
- ```
61
- Sparse pooling:
48
+
49
+ Import sparse layers and quantization library (HGQ2):
50
+
51
+ ```python
52
+ import keras
53
+ from keras.layers import Flatten, Activation, ReLU
54
+ from hgq.layers import QConv2D, QDense
55
+ from hgq.config import QuantizerConfigScope, LayerConfigScope
56
+ from hgq.quantizer.config import QuantizerConfig
57
+ from sparsepixels.layers import InputReduce, QConv2DSparse, AveragePooling2DSparse
62
58
  ```
63
- x, keep_mask = AveragePooling2DSparse(4, name='pool1')([x, keep_mask])
59
+
60
+ Build an example sparse CNN within HGQ2 quantization scopes:
61
+
62
+ ```python
63
+ with (
64
+ QuantizerConfigScope(place='all', default_q_type='kbi', overflow_mode='SAT_SYM'),
65
+ QuantizerConfigScope(place='datalane', default_q_type='kif', overflow_mode='WRAP'),
66
+ LayerConfigScope(enable_ebops=False, enable_iq=False),
67
+ ):
68
+ x_in = keras.Input(shape=(x_train.shape[1], x_train.shape[2], x_train.shape[3]), name='x_in')
69
+
70
+ # Sparse input reduction: retain up to n_max_pixels active pixels
71
+ x, keep_mask = InputReduce(n_max_pixels=20, threshold=0.1, name='input_reduce')(x_in)
72
+
73
+ # Sparse convolution
74
+ x = QConv2DSparse(filters=3, kernel_size=3, name='conv1', padding='same', strides=1,
75
+ bq_conf=QuantizerConfig('default', 'bias'))([x, keep_mask])
76
+ x = ReLU(name='relu1')(x)
77
+
78
+ # Sparse pooling
79
+ x, keep_mask = AveragePooling2DSparse(2, name='pool1')([x, keep_mask])
80
+
81
+ x = Flatten(name='flatten')(x)
82
+ x = QDense(10, name='dense1', activation='relu')(x)
83
+ x = Activation('softmax', name='softmax')(x)
84
+
85
+ model = keras.Model(x_in, x)
64
86
  ```
87
+
65
88
  We are working on hls4ml integration that auto parses the sparse layers into HLS.
66
89
 
67
90
  ## Documentation
68
91
 
69
92
  ## Citation
93
+
70
94
  If you find this useful in your research, please consider citing:
95
+
71
96
  ```
72
97
  @article{Tsoi:2025nvg,
73
98
  author = "Tsoi, Ho Fung and Rankin, Dylan and Loncar, Vladimir and Harris, Philip",
@@ -79,3 +104,4 @@ If you find this useful in your research, please consider citing:
79
104
  year = "2025"
80
105
  }
81
106
  ```
107
+
@@ -11,4 +11,6 @@ sparsepixels.egg-info/SOURCES.txt
11
11
  sparsepixels.egg-info/dependency_links.txt
12
12
  sparsepixels.egg-info/requires.txt
13
13
  sparsepixels.egg-info/top_level.txt
14
- sparsepixels/img/logo.png
14
+ sparsepixels/img/logo.png
15
+ tests/__init__.py
16
+ tests/test_model.py
@@ -0,0 +1,3 @@
1
+ tensorflow
2
+ keras>=3.0
3
+ HGQ2
@@ -1,9 +1,8 @@
1
- datasets
2
1
  dist
3
2
  docs
4
3
  hls_proj
5
4
  hls_templates
6
5
  notebook
7
- plots
8
6
  sparsepixels
7
+ tests
9
8
  weights
File without changes
@@ -0,0 +1,58 @@
1
+ import keras
2
+ from keras.layers import Flatten, Activation, AveragePooling2D, ReLU
3
+ from hgq.layers import QConv2D, QDense
4
+ from hgq.config import QuantizerConfigScope, LayerConfigScope
5
+ from hgq.quantizer.config import QuantizerConfig
6
+ from sparsepixels.layers import InputReduce, QConv2DSparse, AveragePooling2DSparse
7
+
8
+
9
+ def build_cnn(is_sparse, n_max_pixels=None):
10
+ with (
11
+ QuantizerConfigScope(place='all', default_q_type='kbi', overflow_mode='SAT_SYM'),
12
+ QuantizerConfigScope(place='datalane', default_q_type='kif', overflow_mode='WRAP'),
13
+ LayerConfigScope(enable_ebops=False, enable_iq=False),
14
+ ):
15
+ x_in = keras.Input(shape=(32, 32, 1), name='x_in')
16
+ if is_sparse:
17
+ x, keep_mask = InputReduce(n_max_pixels=n_max_pixels, threshold=1, name='input_reduce')(x_in)
18
+ else:
19
+ x = x_in
20
+
21
+ if is_sparse:
22
+ x = QConv2DSparse(filters=1, kernel_size=7, name='conv1', padding='same', strides=1,
23
+ bq_conf=QuantizerConfig('default', 'bias'))([x, keep_mask])
24
+ x = ReLU(name='relu1')(x)
25
+ x, keep_mask = AveragePooling2DSparse(4, name='pool1')([x, keep_mask])
26
+
27
+ x = QConv2DSparse(filters=3, kernel_size=5, name='conv2', padding='same', strides=1,
28
+ bq_conf=QuantizerConfig('default', 'bias'))([x, keep_mask])
29
+ x = ReLU(name='relu2')(x)
30
+ x, keep_mask = AveragePooling2DSparse(2, name='pool2')([x, keep_mask])
31
+ else:
32
+ x = QConv2D(filters=1, kernel_size=7, name='conv1', padding='same', strides=1,
33
+ activation='relu')(x)
34
+ x = AveragePooling2D(4, name='pool1')(x)
35
+
36
+ x = QConv2D(filters=3, kernel_size=5, name='conv2', padding='same', strides=1,
37
+ activation='relu')(x)
38
+ x = AveragePooling2D(2, name='pool2')(x)
39
+
40
+ x = Flatten(name='flatten')(x)
41
+
42
+ x = QDense(36, name='dense1', activation='relu')(x)
43
+
44
+ x = QDense(10, name='dense2')(x)
45
+ x = Activation('softmax', name='softmax')(x)
46
+
47
+ return keras.Model(x_in, x)
48
+
49
+
50
+ def test_build_full_cnn():
51
+ cnn_full = build_cnn(is_sparse=False)
52
+ cnn_full.compile(optimizer=keras.optimizers.Adam(learning_rate=0.001), loss='categorical_crossentropy', metrics=['accuracy'])
53
+ cnn_full.summary()
54
+
55
+ def test_build_sparse_cnn():
56
+ cnn_sparse = build_cnn(is_sparse=True, n_max_pixels=20)
57
+ cnn_sparse.compile(optimizer=keras.optimizers.Adam(learning_rate=0.001), loss='categorical_crossentropy', metrics=['accuracy'])
58
+ cnn_sparse.summary()
@@ -1,62 +0,0 @@
1
- <p align="center">
2
- <img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/logo.png" width="300" />
3
- </p>
4
-
5
- <p align="center">
6
- <img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/sparsepixels.png" width="900"/>
7
- </p>
8
-
9
- <p align="center">
10
- <img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/cnn_standard.gif" width="400" />
11
- <img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/cnn_sparse.gif" width="400" />
12
- </p>
13
-
14
- # SparsePixels: Efficient convolution for sparse data on FPGAs
15
-
16
- [![arXiv](https://img.shields.io/badge/arXiv-2512.06208-b31b1b.svg?style=flat-square)](https://arxiv.org/abs/2512.06208)
17
- [![PyPI - Version](https://img.shields.io/pypi/v/sparsepixels?color=orange&style=flat-square)](https://pypi.org/project/sparsepixels)
18
-
19
- > **Note:** we are actively working on making this usable soon, before integrating into hls4ml (first qkeras with keras2, and then HGQ with keras3), we are also working on a major upgrade with partial paralleliztion and streaming for sparse layers in HLS. stay tuned!!
20
-
21
- ## Installation
22
- With Python 3.10 or 3.11 (for now):
23
- ```
24
- pip install sparsepixels
25
- ```
26
-
27
- ## Getting Started
28
- On the model training in Python, import sparse layers:
29
- ```
30
- from sparsepixels.layers import *
31
- ```
32
- Sparse input reduction:
33
- ```
34
- x_in = keras.Input(shape=(x_train.shape[1], x_train.shape[2], x_train.shape[3]), name='x_in')
35
- x, keep_mask = InputReduce(n_max_pixels=n_max_pixels, threshold=threshold, name='input_reduce')(x_in)
36
- ```
37
- Sparse convolution:
38
- ```
39
- x = QConv2DSparse(filters=1, kernel_size=7, use_bias=True, name='conv1', padding='same', strides=1,
40
- kernel_quantizer=quantizer, bias_quantizer=quantizer)([x, keep_mask])
41
- ```
42
- Sparse pooling:
43
- ```
44
- x, keep_mask = AveragePooling2DSparse(4, name='pool1')([x, keep_mask])
45
- ```
46
- We are working on hls4ml integration that auto parses the sparse layers into HLS.
47
-
48
- ## Documentation
49
-
50
- ## Citation
51
- If you find this useful in your research, please consider citing:
52
- ```
53
- @article{Tsoi:2025nvg,
54
- author = "Tsoi, Ho Fung and Rankin, Dylan and Loncar, Vladimir and Harris, Philip",
55
- title = "{SparsePixels: Efficient Convolution for Sparse Data on FPGAs}",
56
- eprint = "2512.06208",
57
- archivePrefix = "arXiv",
58
- primaryClass = "cs.AR",
59
- month = "12",
60
- year = "2025"
61
- }
62
- ```
@@ -1,4 +0,0 @@
1
- tensorflow<2.15.0,>=2.14.0
2
- keras<2.15.0,>=2.14.0
3
- qkeras<0.10.0,>=0.9.0
4
- pyparsing<3.4.0,>=3.3.0
File without changes
File without changes