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.
- {sparsepixels-0.1.1/sparsepixels.egg-info → sparsepixels-0.2.0}/PKG-INFO +50 -24
- sparsepixels-0.2.0/README.md +89 -0
- {sparsepixels-0.1.1 → sparsepixels-0.2.0}/setup.cfg +5 -6
- {sparsepixels-0.1.1 → sparsepixels-0.2.0}/sparsepixels/layers.py +57 -55
- {sparsepixels-0.1.1 → sparsepixels-0.2.0/sparsepixels.egg-info}/PKG-INFO +50 -24
- {sparsepixels-0.1.1 → sparsepixels-0.2.0}/sparsepixels.egg-info/SOURCES.txt +3 -1
- sparsepixels-0.2.0/sparsepixels.egg-info/requires.txt +3 -0
- {sparsepixels-0.1.1 → sparsepixels-0.2.0}/sparsepixels.egg-info/top_level.txt +1 -2
- sparsepixels-0.2.0/tests/__init__.py +0 -0
- sparsepixels-0.2.0/tests/test_model.py +58 -0
- sparsepixels-0.1.1/README.md +0 -62
- sparsepixels-0.1.1/sparsepixels.egg-info/requires.txt +0 -4
- {sparsepixels-0.1.1 → sparsepixels-0.2.0}/LICENSE +0 -0
- {sparsepixels-0.1.1 → sparsepixels-0.2.0}/notebook/utils.py +0 -0
- {sparsepixels-0.1.1 → sparsepixels-0.2.0}/pyproject.toml +0 -0
- {sparsepixels-0.1.1 → sparsepixels-0.2.0}/setup.py +0 -0
- {sparsepixels-0.1.1 → sparsepixels-0.2.0}/sparsepixels/__init__.py +0 -0
- {sparsepixels-0.1.1 → sparsepixels-0.2.0}/sparsepixels/img/logo.png +0 -0
- {sparsepixels-0.1.1 → sparsepixels-0.2.0}/sparsepixels.egg-info/dependency_links.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sparsepixels
|
|
3
|
-
Version: 0.
|
|
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:
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: tensorflow
|
|
15
|
-
Requires-Dist: keras
|
|
16
|
-
Requires-Dist:
|
|
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
|
[](https://arxiv.org/abs/2512.06208)
|
|
36
35
|
[](https://pypi.org/project/sparsepixels)
|
|
37
36
|
|
|
38
|
-
> **Note:**
|
|
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
|
-
|
|
40
|
+
|
|
41
|
+
With Python >= 3.10:
|
|
42
|
+
|
|
42
43
|
```
|
|
43
44
|
pip install sparsepixels
|
|
44
45
|
```
|
|
45
46
|
|
|
46
47
|
## Getting Started
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
+
[](https://arxiv.org/abs/2512.06208)
|
|
17
|
+
[](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.
|
|
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
|
|
18
|
+
python_requires = >=3.10
|
|
19
19
|
install_requires =
|
|
20
|
-
tensorflow
|
|
21
|
-
keras>=
|
|
22
|
-
|
|
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
|
|
2
|
-
from
|
|
3
|
-
from
|
|
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
|
-
|
|
8
|
+
|
|
9
|
+
class InputReduce(keras.layers.Layer):
|
|
6
10
|
def __init__(self, n_max_pixels, threshold, **kwargs):
|
|
7
|
-
super(
|
|
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 =
|
|
13
|
-
h =
|
|
14
|
-
w =
|
|
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 =
|
|
22
|
+
active_flag = ops.cast(inputs[..., 0] > self.threshold, "int32")
|
|
27
23
|
else:
|
|
28
|
-
active_flag =
|
|
24
|
+
active_flag = ops.cast(inputs[..., 0] != 0, "int32")
|
|
29
25
|
|
|
30
|
-
active_flag_flat =
|
|
31
|
-
active_count =
|
|
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 =
|
|
34
|
-
|
|
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(
|
|
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
|
-
|
|
46
|
+
|
|
47
|
+
class RemoveDilatedPixels(keras.layers.Layer):
|
|
49
48
|
def __init__(self, **kwargs):
|
|
50
|
-
super(
|
|
49
|
+
super().__init__(**kwargs)
|
|
51
50
|
|
|
52
51
|
def call(self, inputs):
|
|
53
52
|
x, mask = inputs
|
|
54
|
-
mask =
|
|
55
|
-
|
|
56
|
-
return removed
|
|
53
|
+
mask = ops.cast(mask, x.dtype)
|
|
54
|
+
return x * mask
|
|
57
55
|
|
|
58
56
|
def get_config(self):
|
|
59
|
-
|
|
60
|
-
return config
|
|
57
|
+
return super().get_config()
|
|
61
58
|
|
|
62
59
|
|
|
63
|
-
class QConv2DSparse(
|
|
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.
|
|
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.
|
|
86
|
-
if self.
|
|
87
|
-
b = self.
|
|
88
|
-
b =
|
|
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 =
|
|
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["
|
|
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
|
-
|
|
106
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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:
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: tensorflow
|
|
15
|
-
Requires-Dist: keras
|
|
16
|
-
Requires-Dist:
|
|
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
|
[](https://arxiv.org/abs/2512.06208)
|
|
36
35
|
[](https://pypi.org/project/sparsepixels)
|
|
37
36
|
|
|
38
|
-
> **Note:**
|
|
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
|
-
|
|
40
|
+
|
|
41
|
+
With Python >= 3.10:
|
|
42
|
+
|
|
42
43
|
```
|
|
43
44
|
pip install sparsepixels
|
|
44
45
|
```
|
|
45
46
|
|
|
46
47
|
## Getting Started
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
+
|
|
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()
|
sparsepixels-0.1.1/README.md
DELETED
|
@@ -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
|
-
[](https://arxiv.org/abs/2512.06208)
|
|
17
|
-
[](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
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|