Repository navigation
Add joint palettization + sparsity for pre-sparsified weights - #139
Conversation
|
The CoreML path has no check for |
|
In some corner cases, _sparsity = 0 (say for a few layers they end up like this), does the export pipeline still work? Can we add one or two test cases for them? |
|
From the PR description only (not looked at the code yet):
Why does it say axis=0? Axis is configurable so it should work with whatever axis has been set, right?
Any reason to keep it private for now? I think we make it a public flag, if well tested, for the scenarios supported.
This should only happen in the per_group_channel case. For per_tensor, we should use all 2^n centroids.
I am bit confused about whether we need both the conditions to be true (using 2^n-1 centroids on non zero data + each group having the same number of 0s) for the
Whats the reason for not supporting some of these cases? Is it just something to keep this PR simple, and add those cases later, or is it something fundamental in these cases which won't work with combined sparsity? |
| @@ -0,0 +1,267 @@ | |||
| # Copyright 2026 Apple Inc. | |||
There was a problem hiding this comment.
Can you please add unit test(s) that takes a simple one layer model (e.g. one linear or one conv layer), and applies the following 4 configs:
- scalar + per_tensor
- scalar + per_group_channel
- vector + per_group_channel
- vector + per_tensor
and for each backend :Core AI and Core ML, verify the correct pattern of ops, that is the correct chaining / sequence of ops in the graph. Now this could be I guess done after the finalize call itself? Or we may need to convert and then check the pattern in the MLIR/MIL graphs.
(optional: for this unit test, we check that the outputs match between the prepared model and the aimodel/mlpackage outputs )
There was a problem hiding this comment.
We should also add another test (test_grouped_with_uneven_groups can be updated for this purpose) that checks that 2^n-1 centroids are used when expected and 2^n centroids are used when thats expected to happen.
There was a problem hiding this comment.
(BTW, if you find vector kmeans taking too much time, although for a single layer it should be fine, if you apply to mnist or resnet model it could take some time, then you can use max_iter=1, n_init=1 for unit testing purposes by using the VectorKMeansConfig introduced in #137).
|
Regarding per-group palletization + sparsity, lut_to_dense has to know which entries of the flat nonzero list belong to which group LUT. The CoreAI op has no per-element group information unlike CoreML. That's why same number of nonzero are needed. Workaround is to pad groups with fewer nonzero to have the same with the group with max nonzero by padding real zeroes and having one zero in the LUT used for them. However, this workaround reduces the savings in the sparse export format - maybe 5% depending on how sparsity is distributed (probably more if group size is smaller). Is my understanding correct? |
Thats correct and @u-simha is updating this PR to implement that "padding" (currently it doesn't do it). |
dbffbe2 to
5066211
Compare
aseemw
left a comment
There was a problem hiding this comment.
Looks most good.
-
Left a few comments on tests, I think some of the can be reduced / simplified.
-
Whats the reason to keep the flag as private ?
aseemw
left a comment
There was a problem hiding this comment.
I took a look again, the testing coverage is comprehensive and nice. Maybe it could be cleaned up a bit to make it more readable, but not a blocker at all.
Approving the PR. Thanks for covering all the different configurations!
Good to merge from my side once
- we make that a public flag
- add a short update to the documentation : to the
PalettizationSpecdoc string, explaining what this flag does more cleary.
5066211 to
51f4fad
Compare
PalettizationSpec(sparsity_detection_threshold=t) palettizes a weight jointly with its sparsity when at least a fraction t of it (and at least one element) is exactly zero; None (the default) turns detection off. The nonzero mask is detected on the first forward pass and kept fixed. K-means fits the palette on the nonzero weights only, and the fake-palettized weight is lut[indices] * mask, so the zeros stay exactly zero. CoreAI exports detected weights as lut_to_dense over the nonzero indices followed by sparse_to_dense, for per-tensor LUTs and for LUTs grouped along axis 0. A grouped LUT maps the g-th contiguous block of nonzero indices to group g, so every group must store the same number of elements: layers whose groups hold different numbers of nonzeros fit 2**n_bits - 1 centroids with a reserved zero LUT entry, and the export also stores some zeros of the smaller groups (which must look up exactly 0.0) until every group matches the fullest one. Balanced and per-tensor layers use all 2**n_bits centroids. CoreAI also supports per-channel scales and quantized LUTs (zero point of 0) with joint sparsity: the scale and the LUT dequantization are applied with constexpr_blockwise_shift_scale after sparse_to_dense so masked weights stay exactly zero. CoreAI also supports vector palettization (cluster_dim > 1) with joint sparsity, per tensor or grouped along axis 0. The stored values of each group are clustered in vectors of cluster_dim consecutive values, so lut_to_dense expands the indices (nnz // cluster_dim per group, with a [num_groups, 2**n_bits, cluster_dim] LUT) into the values that sparse_to_dense scatters. When a group's nonzero count is not a multiple of cluster_dim, its smallest nonzeros (at most cluster_dim - 1) are dropped from the mask once, where the mask is detected, so training and export see the same mask. Groups with fewer vectors than the fullest group also store some of their zeros, chosen at that point too; a stored zero can share a vector with a nonzero and come back as a small nonzero. Grouping along another axis and a quantized LUT are rejected with vector palettization. CoreML adds PRUNING to the compression types, giving constexpr_lut_to_sparse followed by constexpr_sparse_to_dense for any mask and group axis, and per-channel scales add a constexpr_blockwise_shift_scale. CoreML rejects vector palettization with joint sparsity, since constexpr_lut_to_sparse takes its mask on the grid of vectors, and it rejects lut_qspec, which coremltools cannot convert after its sparse LUT op. The Torch backend keeps the zeros. Export errors name the layer. The tests are split by what they exercise. test_joint_sparsity_palettization.py checks prepare() and finalize() of a small conv/conv/linear model without converting to a backend: detection and thresholds, centroid counts, padding, the sparse modules inserted for CoreAI (de-palettized by hand, including per-channel scales, quantized LUTs and vector palettization), the Torch backend, and each backend's rejections. test_joint_sparsity_palettization_e2e.py converts pre-sparsified MNIST (and ResNet-50, as slow tests) to CoreAI and CoreML and checks the op counts, the op order and the output match. Shared helpers are in tests/test_utils/joint_sparsity.py.
51f4fad to
0b1c45a
Compare
With axis-0 grouped LUTs, only the groups with fewer nonzeros than the fullest group are padded with stored zeros at export, and only they need a LUT entry that looks up exactly 0.0. The decision to reserve that entry (and fit 2**n_bits - 1 centroids) was made once per layer, so the fullest group also lost a centroid for a zero entry it never uses. Make it per group, so the fullest groups keep all 2**n_bits centroids for their nonzeros. Balanced layers and per-tensor layers are unchanged.
What:
PalettizationSpecdocstring anddocs/src/palettization/config.md, and add a changelog fragmentHow:
sparsity_detection_thresholdoption toPalettizationSpec(a float in [0, 1]).None(default) turns detection off. Otherwise, on the first forward pass, a weight with at least that fraction of exact zeros (and at least one zero) is treated as sparse; its nonzero mask is kept fixed afterwards. No pruning is applied.lut[indices] * mask, so the zeros stay exactly zero:2**n_bitscentroids.2**n_bits - 1centroids and reserve a zero LUT entry, so the zeros the CoreAI export stores for padding look up exactly0.0.cluster_dim = d > 1, per-tensor or grouped along axis 0), the stored values of each group are clustered in vectors ofdconsecutive values, so there arennz // dindices per group and the LUT is[num_groups, 2**n_bits, d]. If a group'snnz % d != 0, its smallestnnz % dnonzeros are zeroed out of the mask once, where the mask is detected, so training and export see the same mask. Groups with fewer vectors than the fullest group also store some of their zeros, chosen at that point too; a stored zero can share a vector with a nonzero and come back as a small nonzero.prepare_for_exportfor palettization:lut_to_denseon the nonzero indices, followed bysparse_to_dense(andconstexpr_blockwise_shift_scalefor a per-channel scale and/or a quantized LUT). Grouped LUTs are passed as[num_groups, num_clusters, 1];lut_to_densemaps theg-th contiguous block of the flat nonzero indices to groupg, so every group must store the same number of elements.PRUNINGto the compression types, givingconstexpr_lut_to_sparse->constexpr_sparse_to_dense(andconstexpr_blockwise_shift_scalefor a per-channel scale). CoreML recovers each element's group from the mask.prepare()andfinalize()on a small conv/conv/linear model (including a by-hand round trip of the stored indices), and tests for the padding helper, the centroid counts and the rejections.Supported configurations:
Palettization without sparsity:
cluster_dim > 1)lut_qspec)CoreMLExportError. For CoreAI there is no export guard, and per-channel scale + quantized LUT is tested.Joint sparsity + palettization of an already-sparse weight:
0.0, so every group stores the same number of elements; the LUT reserves an entry for0.0. Groups that already hold equal numbers of nonzeros pay nothing.lut_qspec)lut_qspec), so masked-out weights dequantize to exactly 0, and it is not supported together with vector palettization. Combined with a per-channel scale, the two are fused into one scale.cluster_dim > 1)constexpr_lut_to_sparsetakes its mask on the grid of vectors, so a vector is kept or dropped as a whole and cannot express nonzero values grouped into vectors. For CoreAI, per-tensor or grouped along axis 0 (grouping along another axis is rejected when the sparsity is detected); it works with a per-channel scale but not with a quantized LUT.Example config: