See https://www.tensorflow.org/xla/operation_semantics#concatenate for the operator semantics
Details
Diff Detail
- Repository
- rG LLVM Github Monorepo
Event Timeline
Nice work!
mlir/include/mlir/Dialect/SparseTensor/IR/SparseTensorOps.td | ||
---|---|---|
177–179 | Maybe we can simply say "The output tensor and all input tensors must be of the same rank."? | |
179–180 | How about this? The concatenation happens on the specified dimension. The result dimension size is the sum of all the input dimension sizes, while all the other dimensions should have the same size in the input and output tensors. | |
mlir/lib/Dialect/SparseTensor/Transforms/SparseTensorConversion.cpp | ||
147 | Please add documentation for this routine, similar to the surrounding routines. | |
155 | Please add an empty line after the routine, similar to the surrounding routines. | |
181–182 | s/use/using/ | |
204 | NIT: s/sum/Sum/ s/sizes/sizes./ | |
626–627 | Why don't we just do this simplification? Similar simplification can be done for genDenseX. | |
662–663 | I think these two lines describe something that is done inside bodyBuilder, isn't it? | |
669 | s/finish/Finish/ | |
672 | s/free/Free/ | |
681–687 | s/iterate/iterates/ I would move this comment out as a document for the whole function. | |
1106 | Unintentional change? | |
1301 | Please add a period to the end. | |
1390–1391 | Maybe we can just delete this comment line, otherwise, | |
1404 | What is this comment for? | |
1435 | Please add a period at the end. | |
mlir/test/Integration/Dialect/SparseTensor/CPU/concatenate.mlir | ||
26 | Very comprehensive testing! | |
31 | s/Concat/Concats/ There are a few similar places below. | |
45 | What is "mix types"? Do you mean "mix sparse and dense matrix"? |
mlir/lib/Dialect/SparseTensor/Transforms/SparseTensorConversion.cpp | ||
---|---|---|
626–627 | Because all other functions in the file now only accept an Operation *, I would need to change them all to make it work. So I decided that maybe it is better to split them into two CLs. |
mlir/test/Integration/Dialect/SparseTensor/CPU/concatenate.mlir | ||
---|---|---|
26 | I am not sure about it. Isn't it always good to have more test cases? I was trying to test not only sparse/dense but also different sparse encodings. (Although the codegen for different types of sparse tensors is the same, the runtime library is implemented differently) |
mlir/include/mlir/Dialect/SparseTensor/IR/SparseTensorOps.td | ||
---|---|---|
177–179 | You should add a new trait to verify this requirement. For an example of how to do so, see def SameOperandsAndResultShape in OpBase.td, template class SameOperandsAndResultShape in include/mlir/IR/OpDefinition.h, and OpTrait::impl::verifySameOperandsAndResultShape in lib/IR/Operation.cpp. The trait you'll define is basically the same, just differing on the specified $dimension This is important not just for catching errors in user programs, but the same trait will be desired for the analogous dense-tensor op (if it doesn't already exist) | |
mlir/lib/Dialect/SparseTensor/Transforms/SparseTensorConversion.cpp | ||
148 | spelling | |
156 | This wasn't in the original sizesFromPtr function, since generally we only need to construct an array of the dynamic sizes (since static sizes are already noted in the types). So why the change of the semantics for sizesFromPtr? And is that change strictly necessary? I would much rather have the implementation of concatSizesFromInputs be adjusted to deal with static dimensions directly, since there are many other users of sizesFromPtr | |
181 | I think it would be cleaner to float this conditional out of the loop. The for(i=0;i<dim;++i) and for(i=dim+1;i<rank:++i) loops would have the same loop body, but... If you combine what I said about sizeFromPtr only constructing the array of dynamic sizes, with what I said about never introducing runtime type checks, then the before/after "loops" are really quite trivial. If the output dimension is dynamic, then store one of the input dimensions (just pick any one, since we know they must all be static and equal); otherwise do nothing. It's only for the concatenated dimension that this function actually needs to do any work. | |
184–185 | We should statically verify that all input tensors already have a compatible shape. The sparse_tensor dialect intentionally avoids all situations where we would be forced to introduce a dynamic/runtime check due to dynamic sizes. (That is, we are allowed to lose information by converting static dimensions into dynamic dimensions, because such information-loss can be performed at compile-time; but we may only gain information about dynamic shapes for the mere purpose of threading that information through to places that need an SSA-value but are entirely parametric in the actual numeric quantity, since any other usage could result in run-time type errors or difficult-to-diagnose performance issues.) Therefore, for all dimensions other than the one being concatenated: all inputs must be static and equal, since if any of them are dynamic then that would require introducing assertions that they match; and the output must be either static-and-equal or else be dynamic (since we're allowed to forget the static-and-equal knowledge after the fact). Whereas for the dimension being concatenated: if all inputs are static, then the output will be either static and equal to the sum, or be dynamic; whereas if any inputs are dynamic, then the output must also be dynamic (though we are permitted to compute the runtime summation-value for the purposes of threading through). | |
300–313 | +1 for factoring this out :) |
mlir/lib/Dialect/SparseTensor/Transforms/SparseTensorConversion.cpp | ||
---|---|---|
425 | Please document this function | |
427 | Avoid meaningless variable names like ret. Instead, use the same names we use elsewhere for this sort of value: namely ind or ivs (which mean different things). In this case you want ivs. (In situations where you have multiple ind things or multiple ivs things, then they should be qualified to indicate what their meaning is in that context; e.g., srcInd, dstInd, targetInd, etc) | |
432 | Style-wise, you should swap the order of the conjuncts: since the primary thing in question is whether the current dimension is the concatDim, and only after that do we care to avoid the AddIOp when there is no offset. | |
439 | please document this function | |
444 | This variable should be named ivs same as everywhere else in the code. | |
445 | Ditto, re order of conjuncts. | |
445–447 | Style guide says no braces here | |
453 | Please retain the documentation that was here before | |
460–471 | Is this really worth factoring out, rather than simply inlining? Is this even called anywhere? | |
626–627 | +1 for just doing this, and +1 for splitting it out into a separate CL. Is that CL uploaded for review? | |
715 | (1) This should be moved to be an actual ConcatenateOp::verify method, by doing let hasVerifier = 1; in the td file where the ConcatenateOp is defined. That way it is guaranteed to be called in the right places at the right time. (2) Most of the logic here should be factored out into a trait, as mentioned earlier. | |
716 | This can be made auto; since the cast op fixes the type, and since it does so explicitly so there's no legibility benefit to repeating the type name again | |
718 | should be unsigned. (I'm a huge advocate of using auto wherever possible, but doing so here only serves to obfuscate things rather than improving legibility) | |
739 | No. As mentioned before, we intentionally avoid ever introducing dynamic checks | |
755–758 | There's no need for the isLegal variable. Instead you should just directly assert(allEqual && "...") here. And once you refactor things to do a proper verifier, you'd just return the LogicalResult directly. |
mlir/lib/Dialect/SparseTensor/Transforms/SparseTensorConversion.cpp | ||
---|---|---|
156 | It is actually in the original sizeFromPtr function (sizes.push_back(constantIndex(builder, loc, shape[i]));). I simply split the original function into two functions to avoid code repetition |
Address some comments from Wren
mlir/lib/Dialect/SparseTensor/Transforms/SparseTensorConversion.cpp | ||
---|---|---|
460–471 | Yes, it is previously used in sparse=>dense conversion, which 1st, use a pointer of COO to load the index vector and 2nd, insert the scalar into dense tensor. When concatenating a dense tensor to a dense tensor, we do not need to convert COO to index vector and we can directly insert the scalar into dense tensor. By factoring it out, we can reuse the common part (inserting scalar into dense vector using index vector). | |
716 | Good piece of advice! Will follow it in the future! | |
739 | Okay! |
Address styling issues
mlir/lib/Dialect/SparseTensor/Transforms/SparseTensorConversion.cpp | ||
---|---|---|
626–627 | No, I will submit it after current CL is accepted to avoid conflicts. |
mlir/lib/Dialect/SparseTensor/Transforms/SparseTensorConversion.cpp | ||
---|---|---|
156 | Aha, the diff just got totally messed up so it was hard to see | |
460–471 | In this CL, the only callsite I see for a function named insertScalarIntoDenseTensor is on line 1395, however that is actually a call to the function defined on lines 456–461. Prior to this CL the only call to a function of this name is on original line 770, which calls the function this CL defines on lines 466–474. However, since the function defined on lines 466–474 is only two lines long and is only ever called from one place, I don't see the value of defining it as a new function rather than inlining it at that the callsite on original line 770. |
mlir/lib/Dialect/SparseTensor/Transforms/SparseTensorConversion.cpp | ||
---|---|---|
181 | Since I was mistaken about sizesFromPtr storing the static dimensions, the before/after loops aren't quite as I described in the above comment. The code actually becomes even simpler, since they'd just always push the constant index from one of the inputs, without needing to care whether the output is static or dynamic |
mlir/include/mlir/Dialect/SparseTensor/IR/SparseTensorOps.td | ||
---|---|---|
177–179 | Fwiw, so long as you add the let hasVerifier = 1; to this op definition, and rephrase the verifyConcatShape function into the ConcatenateOp::verify method. I'd be fine with you doing all the trait stuff in a separate CL, to help break things up. |
mlir/include/mlir/Dialect/SparseTensor/IR/SparseTensorOps.td | ||
---|---|---|
177–179 | I decided to add a verifier instead of a trait to verify the shape. Because
|
mlir/lib/Dialect/SparseTensor/Transforms/SparseTensorConversion.cpp | ||
---|---|---|
181 | Done! It indeed makes the code much simpler! |
mlir/lib/Dialect/SparseTensor/IR/SparseTensorDialect.cpp | ||
---|---|---|
378 ↗ | (On Diff #447733) | spelling |
383 ↗ | (On Diff #447733) | This is incorrect. If any of the inputs are dynamically sized in this dimension, then the output must also be dynamically sized! Consider the simple example of concatenating some tensor<?xi64> with tensor<1x64>. We cannot say the output has whatever size the user wants, because the size they pick could be both too large and too small. If the tensor that gets passed as the first argument actually has size 5, then the output must have size exactly equal to 6. What would we do if the user assumed the output had size 2? or 17? Moreover, this concatenate could be called multiple times (e.g., because it's in a function), and every single time it's called it can be called with different sized inputs, so there's no fixed size that's going to be correct for the output. If MLIR had a more sophisticated type system then the ? runtime variable could be given some name like ?n. In which case we could say that the output has size ?m with the extra constraint that ?m == ?n + 1; this will always be true regardless of what runtime value ?n happens to take, and even if it takes several different values. Alas, MLIR cannot handle such sophisticated details; so in practice those variable names get erased. The output must still have dynamic size ?, since the output size must still be equal to ? + 1 but that expression has no static/fixed value. |
391–399 ↗ | (On Diff #447733) | No, again. As I said before, if any (non-concatDim) input dimension has a dynamic size then the verifier must fail. For clarity of exposition, let's pretend again that the ? runtime variables are given names. Now, consider the case of concatenating tensor<?mx1xi64> with tensor<?nx2xi64> along the second dimension. In order for that to be well-typed, we must require that ?m == ?n; otherwise how would we handle a ragged concatenation? But there's no way to guarantee that constraint at compile-time, thus the verifier must reject this concatenation as ill-formed. The situation doesn't change when mixing static with dynamic. Concatenating tensor<?mx1xi64> with tensor<3x2xi64> on the second dimension would still require the constraint ?m == 3, but that still cannot be guaranteed at compile-time. |
mlir/lib/Dialect/SparseTensor/Transforms/SparseTensorConversion.cpp | ||
---|---|---|
218 | please rename this variable to clarify which type this is supposed to be exactly | |
233 | Shouldn't call .size() method repeatedly in the condition; cf., the style guide. | |
234 | please use a more meaningful name like srcSize or srcSz. Then the comment is unnecessary since the variable name is self-documenting Also, this declaration ought to be combined with the conditional that assigns to it; i.e., Value srcSize = encSrc ? sizeFromPtrAtDim(...) : linalg::createOrFoldDimOp(...);. Since there's no clarity gained from separating it out | |
242 | I think the generated code would be easier to follow if the order of these two arguments was swapped. (Since currently this generates the summation in the reverse order from the input tensors) | |
245–248 | It'd be clearer to do this first (i.e., if (shape[dim] != ShapedType::kDynamicSize) { sizes[dim] = constantIndex(builder, loc, shape[dim]); return; }) since this case is very short whereas the case for dynamic sizes is long. | |
425 | This would be better named loadIndices | |
428 | Would be better named offsetDim, since this is the dimension where the offset is applied (regardless of why the caller wants to apply that offset). | |
456 | And this would be better named storeIndices | |
458 | ditto re renaming to offsetDim |
Address comments from Wren
mlir/lib/Dialect/SparseTensor/IR/SparseTensorDialect.cpp | ||
---|---|---|
383 ↗ | (On Diff #447733) | Make sense. |
391–399 ↗ | (On Diff #447733) | What about concating tensor<?x?xi64> and tensor<?x?xi64>, wouldn't it be too strict? We can explicitly say that programmers should guarantee the shaping rules when using dynamic-sized tensor, otherwise it is undefined behavior. Similar to |
mlir/lib/Dialect/SparseTensor/Transforms/SparseTensorConversion.cpp | ||
233 | Good to know! |
mlir/lib/Dialect/SparseTensor/IR/SparseTensorDialect.cpp | ||
---|---|---|
391–399 ↗ | (On Diff #447733) | It is indeed strict, but as I said before that's the only way to keep from introducing runtime assertions. I don't see anything in memref::CollapseShapeOp that suggests any differently. In fact, that op is a bit stricter than what I've said before, since they also prohibit having an output dimension be dynamic when the corresponding input dimension is fixed. Actually I think that's a very good decision on their part, since it means that any intentional loss of information must be made explicit via some casting op, which in turn simplifies doing further analysis/lowering. Personally I think our concat op should do the same thing. If you want to argue that it's essential we support runtime-polymorphic concat ops, then you'll need to convince Aart that it's worth relaxing the restriction that we never introduce runtime assertions. If you do convince him it's worth it, then the right way to introduce such assertions is via the Shape dialect, since that's what it was designed for. Of course, setting up the proper interop between the SparseTensor dialect and the Shape dialect would be a whole project in and of itself; thus, that would be done in a series of CLs separate from this one anyways. |
mlir/lib/Dialect/SparseTensor/IR/SparseTensorDialect.cpp | ||
---|---|---|
391–399 ↗ | (On Diff #447733) | I did not intend to argue that we should introduce runtime assertions. I was trying to argue that we should accept the instruction if we can NOT statically prove it is WRONG. Similar to this
https://mlir.llvm.org/docs/Dialects/MemRef/#memrefexpand_shape-mlirmemrefexpandshapeop But I am okay with the strict rules too, because I currently do not know how the instruction will be used by the user in practice. |
mlir/include/mlir/Dialect/SparseTensor/IR/SparseTensorOps.td | ||
---|---|---|
167 | nit: we do not use the //=== header for individual ops, only for classes of ops (three in this file, general ops, memory ops, semi ring ops). Since it is in the regular ops section, no need for L166-168 | |
175 | Concatenates, i.e. use the active "s" form in the summary, as done elsewhere | |
177 | The resulting dimension (add "ing") | |
179 | Also, add constraint that, for all src in inputs :: dest-rank == src-rank? | |
182 | empty line after example | |
mlir/lib/Dialect/SparseTensor/IR/SparseTensorDialect.cpp | ||
371 ↗ | (On Diff #448157) | We did not verify that ranks are the same? This can go out of bounds! |
mlir/lib/Dialect/SparseTensor/Transforms/SparseTensorConversion.cpp | ||
64 | hmm, this worries me; this utility has been there for a very long time, and I am not sure breaking it open like this is a good idea; do you have some more details on when this goes wrong? | |
300–313 | I like the refactoring too, but just for future revisions, it is often better to break the revision up into two parts, once that does the preparing refactoring in existing code, and then one that adds the new functionality, like this one; right now, we have a lot of moving parts to keep track off, making careful review a bit harder | |
mlir/test/Dialect/SparseTensor/roundtrip.mlir | ||
308 | maybe break this part onto separate, aligned lines | |
mlir/test/Integration/Dialect/SparseTensor/CPU/concatenate.mlir | ||
26 | I am okay with keeping more test cases, as long as runtime is not excessive, good to be exhaustive! | |
33 | can we break and align parameters for readability of the file? | |
46 | Here and below, period at end of comment |
For future reference, I would probably have broken up this revision into three parts
(1) add concat op to IR, with roundtrip, verifier, and invalid test (note, latter seems missing here)
(2) the util refactoring in existing code
(3) the actual concat conversion, with CHECK and integration test
nit: we do not use the //=== header for individual ops, only for classes of ops (three in this file, general ops, memory ops, semi ring ops). Since it is in the regular ops section, no need for L166-168