sparsepixels 0.2.3__tar.gz → 0.3.1__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.3.1/PKG-INFO +205 -0
- sparsepixels-0.3.1/README.md +186 -0
- {sparsepixels-0.2.3 → sparsepixels-0.3.1}/setup.cfg +2 -1
- sparsepixels-0.3.1/sparsepixels/layers.py +325 -0
- sparsepixels-0.3.1/sparsepixels/utils.py +496 -0
- sparsepixels-0.3.1/sparsepixels.egg-info/PKG-INFO +205 -0
- {sparsepixels-0.2.3 → sparsepixels-0.3.1}/sparsepixels.egg-info/SOURCES.txt +1 -0
- {sparsepixels-0.2.3 → sparsepixels-0.3.1}/sparsepixels.egg-info/requires.txt +1 -0
- sparsepixels-0.3.1/tests/test_model.py +93 -0
- sparsepixels-0.2.3/PKG-INFO +0 -138
- sparsepixels-0.2.3/README.md +0 -120
- sparsepixels-0.2.3/sparsepixels/layers.py +0 -174
- sparsepixels-0.2.3/sparsepixels.egg-info/PKG-INFO +0 -138
- sparsepixels-0.2.3/tests/test_model.py +0 -57
- {sparsepixels-0.2.3 → sparsepixels-0.3.1}/LICENSE +0 -0
- {sparsepixels-0.2.3 → sparsepixels-0.3.1}/notebook/utils.py +0 -0
- {sparsepixels-0.2.3 → sparsepixels-0.3.1}/pyproject.toml +0 -0
- {sparsepixels-0.2.3 → sparsepixels-0.3.1}/setup.py +0 -0
- {sparsepixels-0.2.3 → sparsepixels-0.3.1}/sparsepixels/__init__.py +0 -0
- {sparsepixels-0.2.3 → sparsepixels-0.3.1}/sparsepixels/img/logo.png +0 -0
- {sparsepixels-0.2.3 → sparsepixels-0.3.1}/sparsepixels.egg-info/dependency_links.txt +0 -0
- {sparsepixels-0.2.3 → sparsepixels-0.3.1}/sparsepixels.egg-info/top_level.txt +0 -0
- {sparsepixels-0.2.3 → sparsepixels-0.3.1}/tests/__init__.py +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sparsepixels
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Efficient convolution for sparse data on FPGAs
|
|
5
|
+
Home-page: https://github.com/hftsoi/sparse-pixels
|
|
6
|
+
Author: Ho Fung Tsoi
|
|
7
|
+
Author-email: ho.fung.tsoi@cern.ch
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: tensorflow
|
|
15
|
+
Requires-Dist: keras>=3.0
|
|
16
|
+
Requires-Dist: HGQ2>=0.1.8
|
|
17
|
+
Requires-Dist: matplotlib
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/logo.png" width="300" />
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/sparsepixels.png" width="900"/>
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/cnn_standard.gif" width="400" />
|
|
30
|
+
<img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/cnn_sparse.gif" width="400" />
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
# SparsePixels: Efficient convolution for sparse data on FPGAs
|
|
34
|
+
|
|
35
|
+
[](https://arxiv.org/abs/2512.06208)
|
|
36
|
+
[](https://pypi.org/project/sparsepixels)
|
|
37
|
+
|
|
38
|
+
SparsePixels is a Keras 3 library to build, train, and deploy sparse convolutional neural networks on FPGAs. In many detectors, especially in high-energy physics experiments, the images are almost empty: only a handful of pixels carry a signal (the hits), yet a standard CNN still spends compute on every pixel. A sparse CNN convolves only over the active pixels, so its cost scales with the number of hits rather than the image size, which is what makes low-latency, real-time inference (for example in a trigger) feasible on an FPGA. This library builds quantization-aware (via [HGQ2](https://github.com/calad0i/HGQ2)) sparse CNNs in which the pixel budget and the activity threshold can be learned from data, with a hardware-aware penalty that drives the budget toward the fewest pixels the task tolerates. Trained models convert to FPGA firmware through the [hls4ml](https://github.com/fastmachinelearning/hls4ml) integration, with control over the parallelization of the sparse layers to trade latency against resource usage.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
With Python >= 3.10:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
pip install sparsepixels
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Getting Started
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import keras
|
|
52
|
+
from keras.layers import Flatten, Activation
|
|
53
|
+
from hgq.layers import QDense
|
|
54
|
+
from hgq.config import QuantizerConfigScope, LayerConfigScope
|
|
55
|
+
from hgq.quantizer.config import QuantizerConfig
|
|
56
|
+
from sparsepixels.layers import InputReduce, QConv2DSparse, AveragePooling2DSparse, MaxPooling2DSparse
|
|
57
|
+
from sparsepixels.utils import (
|
|
58
|
+
active_pixels_vs_threshold, plot_reduced_examples, cosine_lr,
|
|
59
|
+
SparseTrainingMonitor, plot_history, print_quantization, plot_quantization,
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
First, study the data to pick a threshold and an initial pixel budget `n`: how many pixels stay
|
|
64
|
+
active as the threshold rises, and what a candidate `(n, threshold)` keeps on a few images.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
active_pixels_vs_threshold(x_train)
|
|
68
|
+
plot_reduced_examples(x_train, n=20, threshold=0.1, n_examples=4)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Build an example sparse CNN within HGQ2 quantization scopes. A custom input quantizer config with
|
|
72
|
+
higher initial fractional bits (`f0=8`) prevents the default (`f0=2`) from zeroing out sparse signals
|
|
73
|
+
in early training epochs. `InputReduce` keeps the first `n` active pixels (first channel above
|
|
74
|
+
`threshold`); by default `n` and `threshold` are trainable hyperparameters, and a penalty of weight `beta_n`
|
|
75
|
+
nudges the budget smaller, trading a little accuracy for lower FPGA latency and resources.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
iq_conf = QuantizerConfig(place='datalane', q_type='kif', i0=4, f0=8, overflow_mode='WRAP')
|
|
79
|
+
|
|
80
|
+
with (
|
|
81
|
+
QuantizerConfigScope(place='all', default_q_type='kbi', overflow_mode='SAT_SYM'),
|
|
82
|
+
QuantizerConfigScope(place='datalane', default_q_type='kif', overflow_mode='WRAP'),
|
|
83
|
+
LayerConfigScope(enable_ebops=True, enable_iq=True, beta0=1e-5),
|
|
84
|
+
):
|
|
85
|
+
x_in = keras.Input(shape=(28, 28, 1), name='x_in')
|
|
86
|
+
|
|
87
|
+
# Sparse input reduction
|
|
88
|
+
x, keep_mask = InputReduce(
|
|
89
|
+
n=30, # initial pixel budget
|
|
90
|
+
threshold=0.1, # initial activity threshold
|
|
91
|
+
beta_n=1e-5, # weight of the pixel budget penalty
|
|
92
|
+
learn_n=True, # trainable pixel budget
|
|
93
|
+
learn_threshold=True, # trainable threshold
|
|
94
|
+
name='input_reduce',
|
|
95
|
+
)(x_in)
|
|
96
|
+
|
|
97
|
+
# Sparse convolution
|
|
98
|
+
x = QConv2DSparse(filters=3, kernel_size=3, name='conv1', padding='same', strides=1,
|
|
99
|
+
activation='relu', iq_conf=iq_conf)([x, keep_mask])
|
|
100
|
+
|
|
101
|
+
# Sparse pooling
|
|
102
|
+
x, keep_mask = AveragePooling2DSparse(2, name='pool1')([x, keep_mask])
|
|
103
|
+
|
|
104
|
+
x = Flatten(name='flatten')(x)
|
|
105
|
+
x = QDense(10, name='dense1', activation='relu', iq_conf=iq_conf)(x)
|
|
106
|
+
x = Activation('softmax', name='softmax')(x)
|
|
107
|
+
|
|
108
|
+
model = keras.Model(x_in, x)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Train the model with `SparseTrainingMonitor`. It records the loss breakdown, the learned
|
|
112
|
+
budget/threshold and the EBOPS each epoch, and it corrects the EBOPS (a proxy for the quantized
|
|
113
|
+
hardware cost) to the sparse compute automatically, so no extra setup is needed. The
|
|
114
|
+
only sparse-specific choices are the cosine-decayed learning rate and `restore_best_weights`, which
|
|
115
|
+
keep the learned budget from over-compressing near the end of training.
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
early_stop = keras.callbacks.EarlyStopping(monitor='val_accuracy', mode='max', patience=20, restore_best_weights=True)
|
|
119
|
+
model.compile(
|
|
120
|
+
optimizer=keras.optimizers.Adam(cosine_lr(1e-3, epochs=100, steps_per_epoch=len(x_train) // 128)),
|
|
121
|
+
loss='categorical_crossentropy', metrics=['accuracy'],
|
|
122
|
+
)
|
|
123
|
+
history = model.fit(x_train, y_train, validation_data=(x_val, y_val),
|
|
124
|
+
epochs=100, batch_size=128, callbacks=[early_stop, SparseTrainingMonitor()])
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
After training, plot the diagnostics and read out the learned sparsity to deploy. `plot_history`
|
|
128
|
+
shows the loss breakdown, the learned budget/threshold and the EBOPS in one figure.
|
|
129
|
+
`print_quantization` / `plot_quantization` summarize the per-layer bit-widths. The values to
|
|
130
|
+
deploy are `layer.n_max_pixels` and `layer.threshold` (hls4ml converter will auto-parse these from the model).
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
plot_history(history, early_stopping=early_stop) # loss breakdown, budget, threshold, EBOPS
|
|
134
|
+
print_quantization(model) # per-layer bit-width distribution and EBOPS
|
|
135
|
+
plot_quantization(model)
|
|
136
|
+
|
|
137
|
+
ir = model.get_layer('input_reduce')
|
|
138
|
+
print(f"n_max_pixels={ir.n_max_pixels}, threshold={ir.threshold:.3f}")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Converting a trained model to HLS with hls4ml
|
|
142
|
+
|
|
143
|
+
> **Note:** A [PR](https://github.com/fastmachinelearning/hls4ml/pull/1468) adding `sparsepixels` support to the official [hls4ml](https://github.com/fastmachinelearning/hls4ml) repo has been submitted but is not yet merged. In the meantime you can install hls4ml from the PR branch on this fork to try the converter:
|
|
144
|
+
>
|
|
145
|
+
> ```bash
|
|
146
|
+
> pip install "git+https://github.com/hftsoi/hls4ml.git@sparsepixels"
|
|
147
|
+
> ```
|
|
148
|
+
|
|
149
|
+
Once installed, pull a config from the trained model, optionally set the per-layer parallelization
|
|
150
|
+
knobs, and convert:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
import hls4ml
|
|
154
|
+
|
|
155
|
+
hls_config = hls4ml.utils.config_from_keras_model(model, granularity='name')
|
|
156
|
+
hls_config.setdefault('Model', {})['PipelineStyle'] = 'dataflow' # "#pragma HLS DATAFLOW"
|
|
157
|
+
|
|
158
|
+
# Optional per-layer parallelization knobs. Omit them for the fully-parallel, lowest-latency default.
|
|
159
|
+
# input_reduce Variant : 'tree' (default, lowest latency) or 'stream' (fewer resources)
|
|
160
|
+
# conv* PixelParallelFactor : active pixels in parallel (<= n_max_pixels)
|
|
161
|
+
# FiltParallelFactor : filters in parallel (<= that conv's filters)
|
|
162
|
+
# pool* PixelParallelFactor : active pixels in parallel
|
|
163
|
+
# ChanParallelFactor : channels in parallel
|
|
164
|
+
# flatten ParallelFactor : scatter positions in parallel (<= out_height * out_width)
|
|
165
|
+
n_sparse = model.get_layer('input_reduce').n_max_pixels
|
|
166
|
+
knobs = {
|
|
167
|
+
'input_reduce': {'Variant': 'stream'},
|
|
168
|
+
'conv1': {'PixelParallelFactor': n_sparse, 'FiltParallelFactor': 3},
|
|
169
|
+
'pool1': {'PixelParallelFactor': n_sparse, 'ChanParallelFactor': 3},
|
|
170
|
+
'flatten': {'ParallelFactor': 14 * 14}, # this model pools 28*28 -> 14*14
|
|
171
|
+
}
|
|
172
|
+
for name, cfg in knobs.items():
|
|
173
|
+
hls_config['LayerName'].setdefault(name, {}).update(cfg)
|
|
174
|
+
|
|
175
|
+
hls_model = hls4ml.converters.convert_from_keras_model(
|
|
176
|
+
model,
|
|
177
|
+
hls_config=hls_config,
|
|
178
|
+
output_dir='hls_proj/my_sparse_cnn',
|
|
179
|
+
backend='Vitis',
|
|
180
|
+
io_type='io_parallel',
|
|
181
|
+
)
|
|
182
|
+
hls_model.write()
|
|
183
|
+
hls_model.compile()
|
|
184
|
+
y_hls = hls_model.predict(x_test)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Documentation
|
|
188
|
+
|
|
189
|
+
Coming soon!
|
|
190
|
+
|
|
191
|
+
## Citation
|
|
192
|
+
|
|
193
|
+
If you find this useful in your research, please consider citing:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
@article{Tsoi:2025nvg,
|
|
197
|
+
author = "Tsoi, Ho Fung and Rankin, Dylan and Loncar, Vladimir and Harris, Philip",
|
|
198
|
+
title = "{SparsePixels: Efficient Convolution for Sparse Data on FPGAs}",
|
|
199
|
+
eprint = "2512.06208",
|
|
200
|
+
archivePrefix = "arXiv",
|
|
201
|
+
primaryClass = "cs.AR",
|
|
202
|
+
month = "12",
|
|
203
|
+
year = "2025"
|
|
204
|
+
}
|
|
205
|
+
```
|
|
@@ -0,0 +1,186 @@
|
|
|
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
|
+
SparsePixels is a Keras 3 library to build, train, and deploy sparse convolutional neural networks on FPGAs. In many detectors, especially in high-energy physics experiments, the images are almost empty: only a handful of pixels carry a signal (the hits), yet a standard CNN still spends compute on every pixel. A sparse CNN convolves only over the active pixels, so its cost scales with the number of hits rather than the image size, which is what makes low-latency, real-time inference (for example in a trigger) feasible on an FPGA. This library builds quantization-aware (via [HGQ2](https://github.com/calad0i/HGQ2)) sparse CNNs in which the pixel budget and the activity threshold can be learned from data, with a hardware-aware penalty that drives the budget toward the fewest pixels the task tolerates. Trained models convert to FPGA firmware through the [hls4ml](https://github.com/fastmachinelearning/hls4ml) integration, with control over the parallelization of the sparse layers to trade latency against resource usage.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
With Python >= 3.10:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
pip install sparsepixels
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Getting Started
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import keras
|
|
33
|
+
from keras.layers import Flatten, Activation
|
|
34
|
+
from hgq.layers import QDense
|
|
35
|
+
from hgq.config import QuantizerConfigScope, LayerConfigScope
|
|
36
|
+
from hgq.quantizer.config import QuantizerConfig
|
|
37
|
+
from sparsepixels.layers import InputReduce, QConv2DSparse, AveragePooling2DSparse, MaxPooling2DSparse
|
|
38
|
+
from sparsepixels.utils import (
|
|
39
|
+
active_pixels_vs_threshold, plot_reduced_examples, cosine_lr,
|
|
40
|
+
SparseTrainingMonitor, plot_history, print_quantization, plot_quantization,
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
First, study the data to pick a threshold and an initial pixel budget `n`: how many pixels stay
|
|
45
|
+
active as the threshold rises, and what a candidate `(n, threshold)` keeps on a few images.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
active_pixels_vs_threshold(x_train)
|
|
49
|
+
plot_reduced_examples(x_train, n=20, threshold=0.1, n_examples=4)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Build an example sparse CNN within HGQ2 quantization scopes. A custom input quantizer config with
|
|
53
|
+
higher initial fractional bits (`f0=8`) prevents the default (`f0=2`) from zeroing out sparse signals
|
|
54
|
+
in early training epochs. `InputReduce` keeps the first `n` active pixels (first channel above
|
|
55
|
+
`threshold`); by default `n` and `threshold` are trainable hyperparameters, and a penalty of weight `beta_n`
|
|
56
|
+
nudges the budget smaller, trading a little accuracy for lower FPGA latency and resources.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
iq_conf = QuantizerConfig(place='datalane', q_type='kif', i0=4, f0=8, overflow_mode='WRAP')
|
|
60
|
+
|
|
61
|
+
with (
|
|
62
|
+
QuantizerConfigScope(place='all', default_q_type='kbi', overflow_mode='SAT_SYM'),
|
|
63
|
+
QuantizerConfigScope(place='datalane', default_q_type='kif', overflow_mode='WRAP'),
|
|
64
|
+
LayerConfigScope(enable_ebops=True, enable_iq=True, beta0=1e-5),
|
|
65
|
+
):
|
|
66
|
+
x_in = keras.Input(shape=(28, 28, 1), name='x_in')
|
|
67
|
+
|
|
68
|
+
# Sparse input reduction
|
|
69
|
+
x, keep_mask = InputReduce(
|
|
70
|
+
n=30, # initial pixel budget
|
|
71
|
+
threshold=0.1, # initial activity threshold
|
|
72
|
+
beta_n=1e-5, # weight of the pixel budget penalty
|
|
73
|
+
learn_n=True, # trainable pixel budget
|
|
74
|
+
learn_threshold=True, # trainable threshold
|
|
75
|
+
name='input_reduce',
|
|
76
|
+
)(x_in)
|
|
77
|
+
|
|
78
|
+
# Sparse convolution
|
|
79
|
+
x = QConv2DSparse(filters=3, kernel_size=3, name='conv1', padding='same', strides=1,
|
|
80
|
+
activation='relu', iq_conf=iq_conf)([x, keep_mask])
|
|
81
|
+
|
|
82
|
+
# Sparse pooling
|
|
83
|
+
x, keep_mask = AveragePooling2DSparse(2, name='pool1')([x, keep_mask])
|
|
84
|
+
|
|
85
|
+
x = Flatten(name='flatten')(x)
|
|
86
|
+
x = QDense(10, name='dense1', activation='relu', iq_conf=iq_conf)(x)
|
|
87
|
+
x = Activation('softmax', name='softmax')(x)
|
|
88
|
+
|
|
89
|
+
model = keras.Model(x_in, x)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Train the model with `SparseTrainingMonitor`. It records the loss breakdown, the learned
|
|
93
|
+
budget/threshold and the EBOPS each epoch, and it corrects the EBOPS (a proxy for the quantized
|
|
94
|
+
hardware cost) to the sparse compute automatically, so no extra setup is needed. The
|
|
95
|
+
only sparse-specific choices are the cosine-decayed learning rate and `restore_best_weights`, which
|
|
96
|
+
keep the learned budget from over-compressing near the end of training.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
early_stop = keras.callbacks.EarlyStopping(monitor='val_accuracy', mode='max', patience=20, restore_best_weights=True)
|
|
100
|
+
model.compile(
|
|
101
|
+
optimizer=keras.optimizers.Adam(cosine_lr(1e-3, epochs=100, steps_per_epoch=len(x_train) // 128)),
|
|
102
|
+
loss='categorical_crossentropy', metrics=['accuracy'],
|
|
103
|
+
)
|
|
104
|
+
history = model.fit(x_train, y_train, validation_data=(x_val, y_val),
|
|
105
|
+
epochs=100, batch_size=128, callbacks=[early_stop, SparseTrainingMonitor()])
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
After training, plot the diagnostics and read out the learned sparsity to deploy. `plot_history`
|
|
109
|
+
shows the loss breakdown, the learned budget/threshold and the EBOPS in one figure.
|
|
110
|
+
`print_quantization` / `plot_quantization` summarize the per-layer bit-widths. The values to
|
|
111
|
+
deploy are `layer.n_max_pixels` and `layer.threshold` (hls4ml converter will auto-parse these from the model).
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
plot_history(history, early_stopping=early_stop) # loss breakdown, budget, threshold, EBOPS
|
|
115
|
+
print_quantization(model) # per-layer bit-width distribution and EBOPS
|
|
116
|
+
plot_quantization(model)
|
|
117
|
+
|
|
118
|
+
ir = model.get_layer('input_reduce')
|
|
119
|
+
print(f"n_max_pixels={ir.n_max_pixels}, threshold={ir.threshold:.3f}")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Converting a trained model to HLS with hls4ml
|
|
123
|
+
|
|
124
|
+
> **Note:** A [PR](https://github.com/fastmachinelearning/hls4ml/pull/1468) adding `sparsepixels` support to the official [hls4ml](https://github.com/fastmachinelearning/hls4ml) repo has been submitted but is not yet merged. In the meantime you can install hls4ml from the PR branch on this fork to try the converter:
|
|
125
|
+
>
|
|
126
|
+
> ```bash
|
|
127
|
+
> pip install "git+https://github.com/hftsoi/hls4ml.git@sparsepixels"
|
|
128
|
+
> ```
|
|
129
|
+
|
|
130
|
+
Once installed, pull a config from the trained model, optionally set the per-layer parallelization
|
|
131
|
+
knobs, and convert:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
import hls4ml
|
|
135
|
+
|
|
136
|
+
hls_config = hls4ml.utils.config_from_keras_model(model, granularity='name')
|
|
137
|
+
hls_config.setdefault('Model', {})['PipelineStyle'] = 'dataflow' # "#pragma HLS DATAFLOW"
|
|
138
|
+
|
|
139
|
+
# Optional per-layer parallelization knobs. Omit them for the fully-parallel, lowest-latency default.
|
|
140
|
+
# input_reduce Variant : 'tree' (default, lowest latency) or 'stream' (fewer resources)
|
|
141
|
+
# conv* PixelParallelFactor : active pixels in parallel (<= n_max_pixels)
|
|
142
|
+
# FiltParallelFactor : filters in parallel (<= that conv's filters)
|
|
143
|
+
# pool* PixelParallelFactor : active pixels in parallel
|
|
144
|
+
# ChanParallelFactor : channels in parallel
|
|
145
|
+
# flatten ParallelFactor : scatter positions in parallel (<= out_height * out_width)
|
|
146
|
+
n_sparse = model.get_layer('input_reduce').n_max_pixels
|
|
147
|
+
knobs = {
|
|
148
|
+
'input_reduce': {'Variant': 'stream'},
|
|
149
|
+
'conv1': {'PixelParallelFactor': n_sparse, 'FiltParallelFactor': 3},
|
|
150
|
+
'pool1': {'PixelParallelFactor': n_sparse, 'ChanParallelFactor': 3},
|
|
151
|
+
'flatten': {'ParallelFactor': 14 * 14}, # this model pools 28*28 -> 14*14
|
|
152
|
+
}
|
|
153
|
+
for name, cfg in knobs.items():
|
|
154
|
+
hls_config['LayerName'].setdefault(name, {}).update(cfg)
|
|
155
|
+
|
|
156
|
+
hls_model = hls4ml.converters.convert_from_keras_model(
|
|
157
|
+
model,
|
|
158
|
+
hls_config=hls_config,
|
|
159
|
+
output_dir='hls_proj/my_sparse_cnn',
|
|
160
|
+
backend='Vitis',
|
|
161
|
+
io_type='io_parallel',
|
|
162
|
+
)
|
|
163
|
+
hls_model.write()
|
|
164
|
+
hls_model.compile()
|
|
165
|
+
y_hls = hls_model.predict(x_test)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Documentation
|
|
169
|
+
|
|
170
|
+
Coming soon!
|
|
171
|
+
|
|
172
|
+
## Citation
|
|
173
|
+
|
|
174
|
+
If you find this useful in your research, please consider citing:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
@article{Tsoi:2025nvg,
|
|
178
|
+
author = "Tsoi, Ho Fung and Rankin, Dylan and Loncar, Vladimir and Harris, Philip",
|
|
179
|
+
title = "{SparsePixels: Efficient Convolution for Sparse Data on FPGAs}",
|
|
180
|
+
eprint = "2512.06208",
|
|
181
|
+
archivePrefix = "arXiv",
|
|
182
|
+
primaryClass = "cs.AR",
|
|
183
|
+
month = "12",
|
|
184
|
+
year = "2025"
|
|
185
|
+
}
|
|
186
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[metadata]
|
|
2
2
|
name = sparsepixels
|
|
3
|
-
version = 0.
|
|
3
|
+
version = 0.3.1
|
|
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
|
|
@@ -20,6 +20,7 @@ install_requires =
|
|
|
20
20
|
tensorflow
|
|
21
21
|
keras>=3.0
|
|
22
22
|
HGQ2>=0.1.8
|
|
23
|
+
matplotlib
|
|
23
24
|
include_package_data = True
|
|
24
25
|
|
|
25
26
|
[options.package_data]
|