Reduce from an identity held in a register - #143
Conversation
| mlir::Value stored = copy_op.getSource(); | ||
| if (!stored.getDefiningOp<mlir::ktdf::ReadFromFifoOp>()) { | ||
| const auto buffer = llvm::dyn_cast<mlir::MemRefType>(stored.getType()); | ||
| if (!buffer || !buffer.hasStaticShape()) return mlir::failure(); |
There was a problem hiding this comment.
In these cases it would be nice to return rewriter.notifyMatchFailure(copy_op, "message") to tell our future selves why this pattern can't handle the scenario. This will also be printed during debugging.
There was a problem hiding this comment.
Done in 6b6d778 — both bail-outs now say why:
return rewriter.notifyMatchFailure(copy_op, "source is neither a fifo read nor a memref");
return rewriter.notifyMatchFailure(copy_op, "source has no static shape to load as one vector");
| .Case<mlir::arith::MinNumFOp>([&](mlir::arith::MinNumFOp op) { | ||
| // A plain min: there is no absolute-min operator for the | ||
| // abs-of-both shape that maxnumf has. | ||
| return lowerBinaryFOp( | ||
| op, op.getLhs(), op.getRhs(), rewriter, identity_map, | ||
| mlir::vectorchain::VectorChainBinaryOperator::min); | ||
| }) |
There was a problem hiding this comment.
This seems wrong because both MinimumFOp and MinNumFOp are lowered to VectorChainBinaryOperator::min, but their semantics are slightly different.
There was a problem hiding this comment.
You are right that they differ, and I could not settle it from the tree — so I have stated the assumption rather than fixed it, and I would like your call on which way to go.
The two differ only in which operand a NaN takes: minimumf propagates it, minnumf returns the non-NaN side. The unit has one minimum, so mapping both to it is exact only where neither operand is NaN. 6b6d778 says that in the case rather than leaving it implicit.
Two things made me stop short of restricting it:
maxnumfandmaximumfboth map tomaxalready, from [MapReductionPartials , LinalgLowering] Fix maxnumf→max, add absf+maxnumf→abs_max fusion #130. Restrictingminnumfalone would leave min and max inconsistent, and the same objection applies to the max pair.- I could not find the unit's NaN behaviour documented anywhere in the tree, so I would only be guessing which of the two arith ops the hardware actually implements.
If you know which it is, the clean fix is to lower only the matching op and reject the other at the frontend — for both min and max. Happy to do that here, or to drop the minnumf case again and leave it rejected until the max pair is sorted out too. Which would you prefer?
There was a problem hiding this comment.
I'm not sure either about what the hardware implements. I think the best thing to do is reject both the minnumf and maxnumf (except for abs_max) cases until this is sorted out. Currently MapReductionPartials already rejects both; we should update LinalgLowering.cpp to do this too. I.e. remove or comment out the MinNumF case and for MaxNumFOp reject if matchAbsMaxOperands returns false.
There was a problem hiding this comment.
If this triggers a deeper investigation, I'd like to bring up some more details.
IEEE-754 2008 used to have minNum and maxNum, which were removed in 2019 in favor of minimumNumber, maximumNumber, minimum and maximum. The arith.minnumf and arith.maxnumf operations reference the 2008 operations, which are no longer in the standard.
The old operations are not associative, and leave many details up to the implementation. One of those is how signed zero is handled. Another issue is with NaN propagation, which is not just "does it go through" but also "should signalling NaN be treated differently from quiet NaN". I remember reading that every platform did something different.
Note that there is an RFC for adding the new ops to arith, but since we're not gonna bump I presume, that point is kinda moot. Still, I wouldn't use arith.maxnumf/arith.minnumf for anything new, unless we actually fit that envelope.
If it turns out that our hardware has a specific implementation that we might want the front-end to exploit, we would need a spyreop intrinsic.
There was a problem hiding this comment.
Done in 219e078, both together as you suggested:
- The
MinNumFOpcases are gone from both element-wise blocks, andMinNumFOpis out of the frontend allow-list — so minnumf is now turned away up front rather than accepted and then failing below. MaxNumFOpkeeps the abs-of-both path andnotifyMatchFailurees whenmatchAbsMaxOperandsreturns false, so abs_max still lowers and plain maxnumf does not.
You were right that the other two paths already did this: the reduction chain in lowerReductionGenericOp only accepts maxnumf as part of the three-op absf/absf/maxnumf body, and MapReductionPartials only gives a neutral element for that pattern. So this is the element-wise path catching up rather than a new position.
One test had to move. linalg-maxnumf-lowering.mlir pinned maxnumf alone → binary_operator max, which is now failed to run operation lowerings. It keeps the two abs_max cases; the plain one is a negative test in linalg-maxnumf-plain-rejected.mlir. Suite is 191 passed, 4 expected failures.
There was a problem hiding this comment.
That is very useful, thank you — and it settles the direction rather than just the detail. I had been treating this as "which of the two does the hardware do", when the real answer is that the 2008 operations do not pin down enough to be worth mapping at all.
219e078 turns both away: minnumf at the frontend, and maxnumf everywhere except the absf-of-both shape that selects abs_max. The comment on the remaining case says why in your terms — withdrawn in 2019, NaN and signed-zero handling left to the implementation — so the next person does not have to rediscover it.
Agreed on not reaching for these for anything new. If the unit turns out to have a behaviour worth exposing, a spyreop intrinsic is the honest way to say so, since it can carry the exact semantics instead of borrowing an operation that no longer has any.
…the IR A template whose instructions are written for either width needs the caller to say which one, and the callers that say it hardcode a precision per pattern -- so a pattern covering both widths has to be written twice. ktdf.with_precision(op, params) reads the element type the op accumulates in and adds the precision to the pattern's own parameter dictionary, leaving whatever else that dictionary carries alone. It fails for a type no template names, so the compute is left as it is rather than lowered at the wrong width. Signed-off-by: Swagath Venkataramani <vswagath1989@gmail.com>
The copy lowering stored its source straight into the destination, which is right
for a fifo read -- that is already a value -- and wrong for a buffer, where the
store is handed a memref:
error: 'agen.vector_store' op operand #0 must be vector of any type values
or Dataflow custom vector type, but got 'memref<1x32xf32>'
A buffer source is loaded first now. Nothing emits that form yet; the pass in the
commit below does.
Signed-off-by: Swagath Venkataramani <vswagath1989@gmail.com>
A reduction's accumulator was filled with its identity in place. Where a device turns such a fill into an instruction's immediate, the width of that field bounds what the identity may be: enough for an f16 negative infinity and not for an f32 one, which failed late and said only that an immediate was out of range. Giving a register its value up front has no such bound, and reaching it needs the write to be invariant and clear of the loop. So the identity goes to a buffer of its own and the accumulator is copied from it. That buffer is written once and never read back, where the accumulator never was invariant -- the reduction writes it too, and the combiner reads a subview of it. Hoisting after this pass then lifts the fill out of the loop, which is what lets the value be given to the register rather than carried in an instruction. This applies at every width, so an f16 reduction now spends a register on an identity that would have fitted an immediate. Making it conditional on the value fitting would avoid that, at the cost of two ways of doing one thing. Signed-off-by: Swagath Venkataramani <vswagath1989@gmail.com>
The frontend legality check accepts minnumf alongside minimumf, maxnumf and
maximumf, but only minnumf had no lowering. A body using it got through the check
and died further down saying only
error: unsupported operation type in linalg.generic body
It lowers to a plain min. There is no absolute-min operator, so unlike maxnumf it
has no second shape to select for an abs-of-both pattern.
Signed-off-by: Swagath Venkataramani <vswagath1989@gmail.com>
Neither arith.cmpf nor arith.select had a lowering, and neither was in the frontend's allow-list, so a comparison could not be expressed at all. Both dialect ops they map to already existed, along with the lowerings below them. The compare's result carries the operands' type rather than a boolean: it is what a selection takes as its condition, and the i1 form of that op is its separate mask operand. Only the ordered predicates map -- an unordered one asks about NaN, which the compare does not answer. What this reaches depends on the shape. A select whose two sides are the values compared fuses with the compare into a single min or max below here, and works end to end. A select over anything else -- picking between two values the comparison did not name -- still leaves the compare with no reader further down, so it is expressible but not yet compilable. Signed-off-by: Swagath Venkataramani <vswagath1989@gmail.com>
- A copy that cannot be lowered says why through notifyMatchFailure rather than failing silently, so the reason shows up when debugging. - ktdf.with_precision names its two values entries in the header, as the constraints beside it do. - compareOperatorFor was inserted between LowerLinalgGenericPattern's own doc comment and the struct, leaving that comment on the wrong thing. - The minnumf case says what it assumes: the unit has one minimum and minnumf differs from minimumf only in which operand a NaN takes, so mapping both to it is exact only where neither is NaN. maxnumf and maximumf already map that way, so restricting one without the other would leave the two inconsistent. Signed-off-by: Swagath Venkataramani <vswagath1989@gmail.com>
maxnumf and minnumf are the 754-2008 operations, withdrawn in 2019. They leave NaN handling and signed zero to the implementation, and what this unit does with either is not written down -- so mapping them to a plain max or min was a guess. minimumf and maximumf say what they mean and are lowered instead. So minnumf goes back to being rejected, at the frontend rather than below it, and maxnumf lowers only in the abs-of-both shape that selects abs_max. The reduction path and MapReductionPartials already turned both away, so this is the elementwise path catching up rather than a new position. linalg-maxnumf-lowering keeps the two abs-max cases; the plain one moves to linalg-maxnumf-plain-rejected, which pins it being turned away. Signed-off-by: Swagath Venkataramani <vswagath1989@gmail.com>
Rebasing onto main brings torch-spyre#144, which gives getFlattenedVectorType a ShapedType and an execution unit in place of the resource kinds, and threads that unit through lowerBinaryFOp. The compare and the selection take it the same way. The dialects submodule moves with it: the pin main records carries the header the ResourceKinds view moved to, which the commit before it does not have. Signed-off-by: Swagath Venkataramani <vswagath1989@gmail.com>
219e078 to
aaec52d
Compare
An f32 reduction could not be compiled. Three changes, each standing on its own.
1. An identity held in a register
A reduction's accumulator was filled with its identity in place. Where a device
turns such a fill into an instruction's immediate, the width of that field bounds
what the identity may be. An f16 negative infinity fits; an f32 one does not.
Giving a register its value up front has no such bound, and reaching it needs the
write to be invariant and clear of the loop. Neither held for the accumulator: the
reduction writes it as well as the fill, and the combiner reads a subview of it, so
nothing that hoists invariant writes would touch it.
So the identity goes to a buffer of its own, written once and never read back, and
the accumulator is copied from it:
Hoisting after the pass lifts that fill clear of the loop, which is what lets the
value be given to the register rather than carried in an instruction. The
accumulator is then copied from it once per reduction, which is what the identity
is for -- a register given its value up front is set once, and a reduction needs to
start from it again for every statistic.
2. A copy whose source is a buffer
memref.copywas lowered by storing its source straight into the destination.That is right for a fifo read, which is already a value, and wrong for a buffer:
A buffer source is loaded first now. The pass above is the first thing to emit that
form.
3. A pattern that takes its precision from the IR
A template written for either width needs the caller to say which one, and the
callers that say it hardcode a precision per pattern -- so a pattern covering both
widths had to be written twice.
ktdf.with_precision(op, params)reads the element type the op accumulates in andadds the precision to the pattern's own parameter dictionary, leaving the rest of
that dictionary alone. It fails for a type no template names, so a compute is left
as it is rather than lowered at the wrong width.
4. arith.minnumf had no lowering
The frontend legality check accepts
minnumfalongsideminimumf,maxnumfandmaximumf. Onlyminnumfhad no lowering case, so a body using it passed the checkand died further down with nothing to go on:
It lowers to a plain
min. There is no absolute-min operator, so unlikemaxnumfit has no second shape to select for an abs-of-both pattern.
5. A compare and a selection had no lowering
arith.cmpfandarith.selectwere in neither the allow-list nor the lowering, so acomparison could not be expressed at all. Both dialect ops they map to already
existed, and so did the lowerings below them.
The compare's result carries the operands' type rather than a boolean -- that is what
a selection takes as its condition, and the
i1form of the op is its separate maskoperand. Only the ordered predicates map; an unordered one asks about NaN, which the
compare does not answer.
What this reaches depends on the shape:
A select whose two sides are the values compared fuses with the compare into a single
min or max further down. A select over anything else leaves the compare with no reader
there, so it stops with
Dangling non-compute op has no use. Closing that is a changebelow this repo, not in it.
Where things are
lib/Dialect/KTDF/Transforms/MapReductionPartials.cpplib/Pipeline.cpplib/Conversion/backend/ScheduleIRToDFIR/KTDFLowToDFIR/OperationLowerings.cpplib/Transforms/ApplyDevicePatterns.cppktdf.with_precisionlib/Conversion/backend/ScheduleIRToDFIR/KTDFLowToDFIR/LinalgLowering.cppminnumflowers to a min; a compare and a selection lowerlib/Conversion/frontend/KTIRToScheduleIR/KTIRLegalityCheck.cpptest/Transforms/MapReductionPartials/*test/dataflow-scheduler-opt/emit-dfir-pipeline.mlirTesting
llvm-lit dataflow-scheduler/test: 191 passed, 4 expected failures, 2 unsupported.Each of the first three commits was built and run on its own and is green at that
point, so the branch bisects.
linalg-minnumf-lowering.mlirpins the new case. The kernel that could not becompiled before it was also checked end to end downstream, which is where the error
above came from.
The downstream consumer covers this end to end. Its reduction kernels -- sum, min,
max and a new absolute-max -- now pin the identity as a register's value rather than
as an immediate, two of the four in f32, which is the case that could not be
compiled before. Its suite passes, 28 of 28.
No test here pins the register's value, because that is visible in the generated
program rather than in the IR this repo's tests check.