fuse_matmul_weight_bias: fix sub operand order and drop the const-on-the-left case - #2801
Open
LeSingh1 wants to merge 1 commit into
Open
fuse_matmul_weight_bias: fix sub operand order and drop the const-on-the-left case#2801LeSingh1 wants to merge 1 commit into
LeSingh1 wants to merge 1 commit into
Conversation
…the-left case
The pass rewrites matmul + add/sub into linear. Sweeping all 32 combinations of
(which matmul operand is const) x transpose_x x transpose_y x add/sub x (which
add/sub operand the matmul output is), 20 of them fuse to a program that
computes something different. Two independent causes.
1. sub is treated as commutative. The docstring says "add(x=%4, y=%3) is
equivalent # sub is similar", but it is not: the code only ever does
bias = -bias, with no is_first_input test. So bias - x@W becomes x@W - bias,
i.e. the output is exactly negated:
@mb.program(input_specs=[mb.TensorSpec(shape=(2, 3))])
def prog(x):
mm = mb.matmul(x=x, y=np.eye(3, dtype=np.float32))
return mb.sub(x=np.array([100., 200., 300.], np.float32), y=mm)
x = [[1,2,3],[4,5,6]]
before [[99, 198, 297], [96, 195, 294]]
after [[-99, -198, -297], [-96, -195, -294]]
fuse_linear_bias in the same file already handles this correctly by negating
the weight instead; do the same here.
2. When the const is the left matmul operand, w @ x is rewritten as
transpose(linear(transpose(x), w)) with the bias inside the linear. linear
adds its bias along its own last axis, which the outer transpose then moves
to axis -2 of the result, so the bias lands on the wrong axis:
mm = mb.matmul(x=np.eye(3, dtype=np.float32), y=x)
out = mb.add(x=mm, y=np.array([100., 200., 300.], np.float32))
x = arange(9).reshape(3, 3)
before [[100,201,302],[103,204,305],[106,207,308]]
after [[100,101,102],[203,204,205],[306,307,308]]
d_out is computed from the weight, but in this branch the output's last
dimension comes from the other operand, so the bias.shape[0] != d_out guard
only lets the fusion through when everything is square, which is exactly when
the shape check cannot notice. The bias cannot be expressed inside the linear
here, and adding it after the transpose would not fuse anything, so bail out.
After the change the same sweep fuses 16 of 32 and all 16 are numerically
identical to the original.
common::fuse_matmul_weight_bias is in the default pipeline.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
fuse_matmul_weight_biasrewritesmatmul + add/subintolinear. I swept all 32 combinations of (which matmul operand is const) ×transpose_x×transpose_y×add/sub× (which add/sub operand the matmul output is), evaluating the program before and after the pass. 20 of the 32 fuse into a program that computes something different. There are two independent causes.1.
subis treated as commutativeThe docstring says:
It is not similar —
subis not commutative. The code's entiresubhandling is:with no
is_first_inputtest anywhere. Sobias - x @ Wsilently becomesx @ W - bias:With
x = [[1,2,3],[4,5,6]]:[[99, 198, 297], [96, 195, 294]][[-99, -198, -297], [-96, -195, -294]]Exactly negated.
apply_pass_and_basic_checkraises nothing — the shapes and output names are unchanged.fuse_linear_bias, in the same file, already gets this right: it computesis_first_inputand negates the weight when the fused op is the second operand.2. Const as the left matmul operand puts the bias inside a transpose
w @ xis rewritten astranspose(linear(transpose(x), w))with the bias inside thelinear. Butlinearadds its bias along its own last axis, which the outer transpose then moves to axis −2 of the result, so the bias lands on the wrong axis:With
x = np.arange(9).reshape(3, 3):[[100,201,302],[103,204,305],[106,207,308]][[100,101,102],[203,204,205],[306,307,308]]The existing guard cannot catch this:
d_outis taken from the weight, but in this branch the output's last dimension comes from the other operand, sobias.shape[0] != d_outonly lets the fusion through when everything is square — exactly the case where the shape check cannot notice.All 16
const_is_x=Truecombinations are wrong, plus the 4sub-with-bias-first ones from cause 1.common::fuse_matmul_weight_biasis in the default pipeline (pass_pipeline.py).Fix
sub, test which operand the matmul output is.x @ w - biasstayslinear(x, w, -bias);bias - x @ wbecomeslinear(x, -w, bias), mirroringfuse_linear_bias.linearthere, and emitting it as a separateaddafter the transpose would turn 2 ops into 4 — not a fusion at all. This removes an "optimization" that was wrong in every one of its 16 configurations.After the change the same 32-case sweep fuses 16 and all 16 are numerically identical to the original.
Tests
In
TestFuseMatmulWeightBias(passes/tests/test_optimize_linear_passes.py):test_fuse_matmul_weight_bias_sub_with_matmul_second[transpose_x, transpose_y]— 4 cases; fuses and then compares the evaluated result against the original. Fails onmain.test_no_fuse_matmul_weight_bias_const_on_the_left[op_type, bias_first]— 4 cases; the program must be left alone. Fails onmain.The tests evaluate the programs with a small interpreter over the five op types involved (
matmul,linear,transpose,add,sub) so the numbers are checked without needing a backend.The existing
test_fuse_matmul_weight_biasis untouched and its fusion assertions still pass. I rantest_optimize_linear_passes.pyin full plustest_passes.py::TestFuseMatmulWeightBiasbefore and after; the pre-existing failure set is identical, 56 either way (this environment cannot load CoreML.framework, soassert_model_is_validfails there regardless).