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.
@@ -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
+ [![arXiv](https://img.shields.io/badge/arXiv-2512.06208-b31b1b.svg?style=flat-square)](https://arxiv.org/abs/2512.06208)
36
+ [![PyPI - Version](https://img.shields.io/pypi/v/sparsepixels?color=orange&style=flat-square)](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
+ [![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
+ 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.2.3
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]