Skip to content

Commit 9568d68

Browse files
CodeKitty-maxclaude
andcommitted
Fix dropout lowering, reject unwritten reads, speed up input quantization
- Quantizer: aten.dropout/dropout_ and alias share their input's qparams. Without this, a Linear after an eval-mode Dropout (MobileNetV2/V3 classifiers) observed its own input and the delegate read a tensor that no operation wrote. - Serializer: collapse the q -> dq -> q -> dq chain that a decomposed identity op leaves behind, and reject it if the qparams differ. - serialize_graph checks that every operation reads only graph inputs, constants or tensors written earlier. - Delegate: quantize float inputs with a two-op round-half-to-even instead of nearbyintf, which is a software routine on Xtensa, and write the NHWC destination sequentially. On MobileNetV3-Small 224 the input conversion drops from about 93 ms to 33 ms. All e2e programs give the same results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 2636e89 commit 9568d68

4 files changed

Lines changed: 108 additions & 4 deletions

File tree

‎backends/espressif/espnn_runtime/src/espnn_backend.cpp‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ namespace {
5050

5151
DelegateStats g_stats{};
5252

53+
constexpr float kHalfEvenRoundingBias = 12582912.0f;
54+
55+
float round_half_even_below_2p22(float value) {
56+
return (value + kHalfEvenRoundingBias) - kHalfEvenRoundingBias;
57+
}
58+
5359
int8_t quantize(float value, float inverse_scale, int32_t zero_point) {
5460
const float scaled = value * inverse_scale;
5561
if (!std::isfinite(scaled))
@@ -58,7 +64,8 @@ int8_t quantize(float value, float inverse_scale, int32_t zero_point) {
5864
return 127;
5965
if (scaled < -1.0e6f)
6066
return -128;
61-
int32_t q = static_cast<int32_t>(std::nearbyintf(scaled)) + zero_point;
67+
int32_t q =
68+
static_cast<int32_t>(round_half_even_below_2p22(scaled)) + zero_point;
6269
if (q < -128)
6370
q = -128;
6471
if (q > 127)
@@ -98,8 +105,8 @@ bool load_input(
98105
destination[i] = quantize(source[i], inverse, info.zero_point);
99106
return true;
100107
}
101-
for (size_t c = 0; c < channels; ++c) {
102-
for (size_t p = 0; p < plane; ++p) {
108+
for (size_t p = 0; p < plane; ++p) {
109+
for (size_t c = 0; c < channels; ++c) {
103110
destination[p * channels + c] =
104111
quantize(source[c * plane + p], inverse, info.zero_point);
105112
}

‎backends/espressif/quantizer/espnn_quantizer.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,9 @@ def annotate_unary_and_softmax(
301301
torch.ops.aten.upsample_bilinear2d.default,
302302
torch.ops.aten.constant_pad_nd.default,
303303
torch.ops.aten.pad.default,
304+
torch.ops.aten.dropout.default,
305+
torch.ops.aten.dropout_.default,
306+
torch.ops.aten.alias.default,
304307
)
305308

306309
SPLIT_TARGETS = (

‎backends/espressif/serialization/xts3.py‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -611,8 +611,23 @@ def _get_or_create_int8( # noqa: C901
611611
quantized_node, float(node.args[1]), int(node.args[2])
612612
)
613613
return self._constant_int8(node)
614+
source = quantized_node.args[0]
615+
if getattr(
616+
source, "op", None
617+
) == "call_function" and "dequantize_per_tensor" in str(source.target):
618+
# q(dq(x)) is left behind where an identity op (such as an
619+
# eval-mode dropout) was decomposed away.
620+
if (float(source.args[1]), int(source.args[2])) != (
621+
float(node.args[1]),
622+
int(node.args[2]),
623+
):
624+
raise ValueError(
625+
f"{quantized_node.name} requantizes {source.name}; "
626+
"back-to-back requantization is not supported"
627+
)
628+
return self._get_or_create_int8(source, filter_layout)
614629
return self._new_int8_activation(
615-
quantized_node.args[0],
630+
source,
616631
float(node.args[1]),
617632
int(node.args[2]),
618633
)
@@ -1917,9 +1932,35 @@ def check_runtime_limits(graph: XTS3Graph) -> None:
19171932
raise ValueError(f"int32 constant {tensor.tensor_id} is not 4-byte aligned")
19181933

19191934

1935+
def check_dataflow(graph: XTS3Graph) -> None:
1936+
"""
1937+
Raise ValueError when an operation or graph output reads an activation
1938+
that is neither a graph input nor written by an earlier operation. The
1939+
device would read whatever its arena holds there.
1940+
"""
1941+
ready = set(graph.inputs)
1942+
ready.update(
1943+
tensor.tensor_id
1944+
for tensor in graph.tensors
1945+
if tensor.flags & TENSOR_FLAG_CONSTANT
1946+
)
1947+
for index, operation in enumerate(graph.operations):
1948+
for tensor_id in operation.inputs:
1949+
if tensor_id >= 0 and tensor_id not in ready:
1950+
raise ValueError(
1951+
f"Operation {index} ({XTS3OpCode(operation.opcode).name}) "
1952+
f"reads tensor {tensor_id}, which nothing writes before it"
1953+
)
1954+
ready.update(operation.outputs)
1955+
for tensor_id in graph.outputs:
1956+
if tensor_id not in ready:
1957+
raise ValueError(f"Graph output {tensor_id} is never written")
1958+
1959+
19201960
def serialize_graph(graph: XTS3Graph) -> bytes:
19211961
"""Serialize to the XTS3 wire format; constants start 16-byte aligned."""
19221962
check_runtime_limits(graph)
1963+
check_dataflow(graph)
19231964
tensor_data = b"".join(_serialize_tensor(tensor) for tensor in graph.tensors)
19241965
parameter_data = bytearray()
19251966
operation_data = bytearray()

‎backends/espressif/test/test_contracts.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@
3434
EspNNQuantizer,
3535
fused_activation_kind,
3636
)
37+
from executorch.backends.espressif.reference import ProgramEmulator # noqa: E402
3738
from executorch.backends.espressif.serialization.xts3 import ( # noqa: E402
3839
build_graph,
40+
check_dataflow,
3941
check_runtime_limits,
4042
MAX_GRAPH_IO,
4143
MAX_GRAPH_OPERATIONS,
@@ -171,3 +173,54 @@ def test_runtime_limits_reject_misaligned_int32_constants():
171173
]
172174
with pytest.raises(ValueError, match="4-byte aligned"):
173175
check_runtime_limits(dataclasses.replace(graph, tensors=tensors))
176+
177+
178+
class DropoutClassifier(nn.Module):
179+
"""MobileNetV3's classifier: Linear, Hardswish, in-place Dropout, Linear."""
180+
181+
def __init__(self):
182+
super().__init__()
183+
self.head = nn.Sequential(
184+
nn.Linear(16, 32),
185+
nn.Hardswish(inplace=True),
186+
nn.Dropout(0.2, inplace=True),
187+
nn.Linear(32, 10),
188+
)
189+
190+
def forward(self, x):
191+
return self.head(x)
192+
193+
194+
def test_dropout_between_quantized_ops_reads_the_producer():
195+
converted, program = quantize(DropoutClassifier(), [(1, 16)])
196+
graph = build_graph(program)
197+
check_dataflow(graph)
198+
last = graph.operations[-1]
199+
assert last.opcode == XTS3OpCode.LINEAR
200+
assert last.inputs[0] in graph.operations[-2].outputs
201+
202+
edge, report = to_espnn_edge(program, allow_cpu_fallback=False)
203+
assert report["partitions"] == 1
204+
x = torch.randn(1, 16)
205+
expected = converted(x)
206+
actual = ProgramEmulator(edge.exported_program())(x).to(torch.float32)
207+
assert torch.allclose(actual, expected, atol=0.2)
208+
209+
210+
def test_dataflow_rejects_reading_an_unwritten_activation():
211+
graph = _small_graph()
212+
unwritten = dataclasses.replace(
213+
graph.tensors[graph.operations[-1].inputs[0]],
214+
tensor_id=len(graph.tensors),
215+
)
216+
last = dataclasses.replace(
217+
graph.operations[-1],
218+
inputs=[unwritten.tensor_id, *graph.operations[-1].inputs[1:]],
219+
)
220+
broken = dataclasses.replace(
221+
graph,
222+
tensors=[*graph.tensors, unwritten],
223+
operations=[*graph.operations[:-1], last],
224+
)
225+
with pytest.raises(ValueError, match="nothing writes"):
226+
check_dataflow(broken)

0 commit comments

Comments
 (0)