Skip to content

Per-channel int8 quantized linear (#24397) - #24397

Merged
meta-codesync[bot] merged 2 commits into
pytorch:mainfrom
pssrawat:export-D126929702
Nov 10, 2026
Merged

meta-codesync[bot] merged 2 commits into
pytorch:mainfrom
pssrawat:export-D126929702

Conversation

@pssrawat

@pssrawat pssrawat commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

cortex_m::quantized_linear supports only per-tensor weights: it calls arm_fully_connected_s8 with a single requantization multiplier and shift. This adds per-channel linear weights.

With CortexMQuantizer.set_node_target (previous diff), set_node_target(torch.ops.aten.linear.default, INT8_PER_CHANNEL_CONFIG) gives per-channel int8 linear weights; without it, linear layers keep per_tensor_config, so existing exports are unchanged. CortexMLinearCheck rejects per-channel weights that are not quantized per output feature (axis 0, or unset as the convolution check allows), and the lowering checks that it gets one weight scale per output feature, since the kernel indexes its multipliers by output feature.

Per-channel linear weights lower to a new cortex_m::quantized_linear_per_channel. It matches quantized_linear but takes int32 multiplier and shift tensors, as the convolution ops do, and calls arm_fully_connected_per_channel_s8. CMSIS-NN takes int32 arrays, so tensors avoid any runtime conversion. int[] lists would also cost about 20 bytes per output feature in the program, where a tensor costs 4. The two C++ entry points share one implementation that takes the CMSIS-NN call; quantized_linear is unchanged. The Python reference requantizes each output feature with its own parameters.

This diff was authored with Claude Code.

Differential Revision: D126929702

@pssrawat
pssrawat requested a review from rascani as a code owner November 1, 2026 04:00
@pytorch-bot

pytorch-bot Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/24397

Note: Links to docs will display an error until the docs builds have been completed.

✅ No Failures

As of commit bd2a5a5 with merge base dd38e81 (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Nov 1, 2026
@meta-codesync

meta-codesync Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

@pssrawat has exported this pull request. If you are a Meta employee, you can view the originating Diff in D126929702.

@meta-codesync meta-codesync Bot changed the title Per-channel int8 quantized linear Per-channel int8 quantized linear (#24397) Nov 2, 2026
pssrawat added a commit to pssrawat/executorch that referenced this pull request Nov 2, 2026
Summary:
Pull Request resolved: pytorch#24397

`cortex_m::quantized_linear` supports only per-tensor weights: it calls `arm_fully_connected_s8` with a single requantization multiplier and shift. This adds per-channel linear weights.

With `CortexMQuantizer.set_node_target` (previous diff), `set_node_target(torch.ops.aten.linear.default, INT8_PER_CHANNEL_CONFIG)` gives per-channel int8 linear weights; without it, linear layers keep `per_tensor_config`, so existing exports are unchanged. `CortexMLinearCheck` rejects per-channel weights that are not quantized per output feature (axis 0, or unset as the convolution check allows), and the lowering checks that it gets one weight scale per output feature, since the kernel indexes its multipliers by output feature.

Per-channel linear weights lower to a new `cortex_m::quantized_linear_per_channel`. It matches `quantized_linear` but takes int32 multiplier and shift tensors, as the convolution ops do, and calls `arm_fully_connected_per_channel_s8`. CMSIS-NN takes int32 arrays, so tensors avoid any runtime conversion. `int[]` lists would also cost about 20 bytes per output feature in the program, where a tensor costs 4. The two C++ entry points share one implementation that takes the CMSIS-NN call; `quantized_linear` is unchanged. The Python reference requantizes each output feature with its own parameters.

This diff was authored with Claude Code.

Differential Revision: D126929702
@pssrawat pssrawat added the release notes: none Do not include this in the release notes label Nov 2, 2026
pssrawat added a commit to pssrawat/executorch that referenced this pull request Nov 3, 2026
Summary:

`cortex_m::quantized_linear` supports only per-tensor weights: it calls `arm_fully_connected_s8` with a single requantization multiplier and shift. This adds per-channel linear weights.

With `CortexMQuantizer.set_node_target` (previous diff), `set_node_target(torch.ops.aten.linear.default, INT8_PER_CHANNEL_CONFIG)` gives per-channel int8 linear weights; without it, linear layers keep `per_tensor_config`, so existing exports are unchanged. `CortexMLinearCheck` rejects per-channel weights that are not quantized per output feature (axis 0, or unset as the convolution check allows), and the lowering checks that it gets one weight scale per output feature, since the kernel indexes its multipliers by output feature.

Per-channel linear weights lower to a new `cortex_m::quantized_linear_per_channel`. It matches `quantized_linear` but takes int32 multiplier and shift tensors, as the convolution ops do, and calls `arm_fully_connected_per_channel_s8`. CMSIS-NN takes int32 arrays, so tensors avoid any runtime conversion. `int[]` lists would also cost about 20 bytes per output feature in the program, where a tensor costs 4. The two C++ entry points share one implementation that takes the CMSIS-NN call; `quantized_linear` is unchanged. The Python reference requantizes each output feature with its own parameters.

This diff was authored with Claude Code.

Differential Revision: D126929702

@AdrianLundell AdrianLundell left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall. a few small nits. For easier reviewing I would appreciate to keep different PR:s separate, otherwise I have to sort out non relevant changes myself

namespace {
// Runs `fully_connected`, a CMSIS-NN fully connected call bound to its
// requantization parameters, on the op's tensors.
template <typename FullyConnected>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Templating this adds too much complexity for too little benefit for my taste.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

Comment thread backends/cortex_m/ops/operators.py Outdated
else:
output = torch.stack(
[
requantize_cmsis(output_reshaped[..., channel], int(mult), int(shift))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe simpler to add per-channel support to requantize_cmsis?

]
super().__init__(quantizers)

def set_node_target(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is from another PR

@rascani

rascani commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Looks good overall. a few small nits. For easier reviewing I would appreciate to keep different PR:s separate, otherwise I have to sort out non relevant changes myself

This is happens when internal diff stacks are exported one by one. It needs the earlier diffs so there's a functional stack.

@pssrawat - when working with stacks internally, you can use ghexport to get a corresponding PR stack. I can provide instructions if you'd like to try that.

pssrawat added a commit to pssrawat/executorch that referenced this pull request Nov 4, 2026
Summary:

`cortex_m::quantized_linear` supports only per-tensor weights: it calls `arm_fully_connected_s8` with a single requantization multiplier and shift. This adds per-channel linear weights.

With `CortexMQuantizer.set_node_target` (previous diff), `set_node_target(torch.ops.aten.linear.default, INT8_PER_CHANNEL_CONFIG)` gives per-channel int8 linear weights; without it, linear layers keep `per_tensor_config`, so existing exports are unchanged. `CortexMLinearCheck` rejects per-channel weights that are not quantized per output feature (axis 0, or unset as the convolution check allows), and the lowering checks that it gets one weight scale per output feature, since the kernel indexes its multipliers by output feature.

Per-channel linear weights lower to a new `cortex_m::quantized_linear_per_channel`. It matches `quantized_linear` but takes int32 multiplier and shift tensors, as the convolution ops do, and calls `arm_fully_connected_per_channel_s8`. CMSIS-NN takes int32 arrays, so tensors avoid any runtime conversion. `int[]` lists would also cost about 20 bytes per output feature in the program, where a tensor costs 4. The two C++ entry points share one implementation that takes the CMSIS-NN call; `quantized_linear` is unchanged. The Python reference requantizes each output feature with its own parameters.

This diff was authored with Claude Code.

Differential Revision: D126929702
pssrawat added a commit to pssrawat/executorch that referenced this pull request Nov 4, 2026
Summary:

`cortex_m::quantized_linear` supports only per-tensor weights: it calls `arm_fully_connected_s8` with a single requantization multiplier and shift. This adds per-channel linear weights.

With `CortexMQuantizer.set_node_target` (previous diff), `set_node_target(torch.ops.aten.linear.default, INT8_PER_CHANNEL_CONFIG)` gives per-channel int8 linear weights; without it, linear layers keep `per_tensor_config`, so existing exports are unchanged. `CortexMLinearCheck` rejects per-channel weights that are not quantized per output feature (axis 0, or unset as the convolution check allows), and the lowering checks that it gets one weight scale per output feature, since the kernel indexes its multipliers by output feature.

Per-channel linear weights lower to a new `cortex_m::quantized_linear_per_channel`. It matches `quantized_linear` but takes int32 multiplier and shift tensors, as the convolution ops do, and calls `arm_fully_connected_per_channel_s8`. CMSIS-NN takes int32 arrays, so tensors avoid any runtime conversion. `int[]` lists would also cost about 20 bytes per output feature in the program, where a tensor costs 4. The two C++ entry points share one implementation that takes the CMSIS-NN call; `quantized_linear` is unchanged. The Python reference requantizes each output feature with its own parameters.

This diff was authored with Claude Code.

Differential Revision: D126929702
pssrawat added a commit to pssrawat/executorch that referenced this pull request Nov 4, 2026
Summary:

`cortex_m::quantized_linear` supports only per-tensor weights: it calls `arm_fully_connected_s8` with a single requantization multiplier and shift. This adds per-channel linear weights.

With `CortexMQuantizer.set_node_target` (previous diff), `set_node_target(torch.ops.aten.linear.default, INT8_PER_CHANNEL_CONFIG)` gives per-channel int8 linear weights; without it, linear layers keep `per_tensor_config`, so existing exports are unchanged. `CortexMLinearCheck` rejects per-channel weights that are not quantized per output feature (axis 0, or unset as the convolution check allows), and the lowering checks that it gets one weight scale per output feature, since the kernel indexes its multipliers by output feature.

Per-channel linear weights lower to a new `cortex_m::quantized_linear_per_channel`. It matches `quantized_linear` but takes int32 multiplier and shift tensors, as the convolution ops do, and calls `arm_fully_connected_per_channel_s8`. CMSIS-NN takes int32 arrays, so tensors avoid any runtime conversion. `int[]` lists would also cost about 20 bytes per output feature in the program, where a tensor costs 4. The two C++ entry points share one implementation that takes the CMSIS-NN call; `quantized_linear` is unchanged. The Python reference requantizes each output feature with its own parameters.

This diff was authored with Claude Code.

Differential Revision: D126929702
@pssrawat

pssrawat commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

This is happens when internal diff stacks are exported one by one. It needs the earlier diffs so there's a functional stack.

@pssrawat - when working with stacks internally, you can use ghexport to get a corresponding PR stack. I can provide instructions if you'd like to try that.

Addressed the nits. The changes from bottom-stack no longer present.

cmsis_nn_dims output_dims = {batches, 1, 1, out_feat};

arm_cmsis_nn_status status = arm_fully_connected_s8(
const arm_cmsis_nn_status status = arm_fully_connected_wrapper_s8(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this might link both per_channel and per_tensor kernels even if just one is used, could you verify if this is the case and fix that in that case?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, confirmed. arm_fully_connected_wrapper_s8 references both arm_fully_connected_s8 and arm_fully_connected_per_channel_s8. Even with --gc-sections, a binary that only uses per-tensor linear still linked the per-channel kernel.
I've removed the wrapper. quantized_linear_impl now takes the kernel as a template argument, and each op passes its own kernel.

Summary:

`cortex_m::quantized_linear` supports only per-tensor weights: it calls `arm_fully_connected_s8` with a single requantization multiplier and shift. This adds per-channel linear weights.

With `CortexMQuantizer.set_node_target` (previous diff), `set_node_target(torch.ops.aten.linear.default, INT8_PER_CHANNEL_CONFIG)` gives per-channel int8 linear weights; without it, linear layers keep `per_tensor_config`, so existing exports are unchanged. `CortexMLinearCheck` rejects per-channel weights that are not quantized per output feature (axis 0, or unset as the convolution check allows), and the lowering checks that it gets one weight scale per output feature, since the kernel indexes its multipliers by output feature.

Per-channel linear weights lower to a new `cortex_m::quantized_linear_per_channel`. It matches `quantized_linear` but takes int32 multiplier and shift tensors, as the convolution ops do, and calls `arm_fully_connected_per_channel_s8`. CMSIS-NN takes int32 arrays, so tensors avoid any runtime conversion. `int[]` lists would also cost about 20 bytes per output feature in the program, where a tensor costs 4. The two C++ entry points share one implementation that takes the CMSIS-NN call; `quantized_linear` is unchanged. The Python reference requantizes each output feature with its own parameters.

This diff was authored with Claude Code.

Differential Revision: D126929702
// cppcheck-suppress unusedFunction
Tensor& quantized_linear_out(
namespace {
template <typename QuantParams, typename FullyConnectedFn>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me I would prefer just copying the function twice over templating it but this is fine too.

@rascani rascani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review automatically exported from Phabricator review in Meta.

@meta-codesync
meta-codesync Bot merged commit a168321 into pytorch:main Nov 10, 2026
204 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. meta-exported release notes: none Do not include this in the release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants