This is an archive of the discontinued LLVM Phabricator instance.

[mlir][sparse] Add sparse_tensor.concatenate operator
AbandonedPublic

Authored by Peiming on Jul 21 2022, 10:04 AM.

Diff Detail

Event Timeline

Peiming created this revision.Jul 21 2022, 10:04 AM
Herald added a project: Restricted Project. · View Herald Transcript
Peiming requested review of this revision.Jul 21 2022, 10:04 AM
Peiming edited the summary of this revision. (Show Details)Jul 21 2022, 10:05 AM
Peiming added reviewers: bixia, wrengr, yinying-lisa-li.
Peiming updated this revision to Diff 446568.Jul 21 2022, 10:31 AM

Remove repeated code

bixia added a comment.Jul 21 2022, 5:34 PM

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/
Please add a period at the end of the sentence, that is, s/fine/fine./.

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/
s/loop/loop./

672

s/free/Free/
s/iterator/iterator./

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,
s/1st./First/

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!
Though, I am not sure what we really want to test all these 16 combination (SS, SD) x (S, D) x (0, 1) x (NP, P), plus a test with dynamic shape.
I would test these four cases (SS, SD) x (S, D), for which I would make sure some use dimension 0 , dimension 1, NonPermute, Permute, and a dynamic shape case.

31

s/Concat/Concats/

There are a few similar places below.

45

What is "mix types"? Do you mean "mix sparse and dense matrix"?

Peiming marked an inline comment as done.Jul 22 2022, 9:09 AM
Peiming added inline comments.
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.

Peiming updated this revision to Diff 446876.Jul 22 2022, 9:39 AM
Peiming marked an inline comment as done.

Address comments from bixia

Peiming updated this revision to Diff 446879.Jul 22 2022, 9:45 AM

Fix grammar issue in comments

Peiming added inline comments.Jul 22 2022, 9:49 AM
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)

Peiming marked 15 inline comments as done.Jul 22 2022, 9:52 AM
Peiming marked 2 inline comments as done.
wrengr added inline comments.Jul 25 2022, 1:13 PM
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 :)

wrengr added inline comments.Jul 25 2022, 1:57 PM
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.

Peiming added inline comments.Jul 25 2022, 3:06 PM
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

Peiming updated this revision to Diff 447495.Jul 25 2022, 3:19 PM
Peiming marked 3 inline comments as done.

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!

Peiming updated this revision to Diff 447496.Jul 25 2022, 3:35 PM
Peiming marked 9 inline comments as done.

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.

wrengr added inline comments.Jul 25 2022, 3:49 PM
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.

wrengr added inline comments.Jul 25 2022, 3:55 PM
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

Peiming updated this revision to Diff 447506.Jul 25 2022, 3:59 PM

Add verifier to concatenate operator

wrengr added inline comments.Jul 25 2022, 4:02 PM
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.

Peiming marked an inline comment as done.Jul 25 2022, 4:03 PM
Peiming added inline comments.
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

  1. There is no existing operator (that I am aware of) has the same verification rules.
  2. I do not think there will be many operators in the future that gonna to have the same verification rules
Peiming marked an inline comment as done.Jul 25 2022, 4:03 PM
Peiming updated this revision to Diff 447514.Jul 25 2022, 4:42 PM
Peiming marked an inline comment as done.

code simplification

Peiming marked 8 inline comments as done.Jul 25 2022, 4:43 PM
Peiming added inline comments.
mlir/lib/Dialect/SparseTensor/Transforms/SparseTensorConversion.cpp
181

Done!

It indeed makes the code much simpler!

Peiming updated this revision to Diff 447518.Jul 25 2022, 4:48 PM
Peiming marked an inline comment as done.

revert unintended change

Peiming updated this revision to Diff 447733.Jul 26 2022, 9:02 AM

fix formatting issue

wrengr requested changes to this revision.Jul 27 2022, 12:14 PM
wrengr added inline comments.
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.

This revision now requires changes to proceed.Jul 27 2022, 12:14 PM
wrengr added inline comments.Jul 27 2022, 12:53 PM
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

Peiming updated this revision to Diff 448157.Jul 27 2022, 2:10 PM
Peiming marked 12 inline comments as done.

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
https://mlir.llvm.org/docs/Dialects/MemRef/#memrefcollapse_shape-mlirmemrefcollapseshapeop

mlir/lib/Dialect/SparseTensor/Transforms/SparseTensorConversion.cpp
233

Good to know!

wrengr added inline comments.Jul 28 2022, 6:59 PM
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.

Peiming added inline comments.Jul 29 2022, 1:54 PM
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

If an op can be statically proven to be invalid (e.g, an expansion from memref<10xf32> to memref<2x6xf32>), it is rejected by the verifier. If it cannot statically be proven invalid (e.g., the full example above; it is unclear whether the first source dimension is divisible by 5), the op is accepted by the verifier. However, if the op is in fact invalid at runtime, the behavior is undefined.

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.

aartbik added inline comments.Aug 2 2022, 1:14 PM
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
(note, the part inside the CHECK often breaks the 80-column, that is okay, but the part in the mlir code can be formatted a bit better in general

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

Peiming abandoned this revision.Aug 4 2022, 2:10 PM
Peiming marked 7 inline comments as done.