Skip to content

Conversation

cowardsa
Copy link
Contributor

@cowardsa cowardsa commented Jul 3, 2025

Building on the boiler plate definition adding two operators

  • datapath.compress - compressor tree circuit
  • datapath.partial_product - partial product generation circuit

The key idea is to view datapath operators as generators of circuits that satisfy some contract, for example in the case of the datapath.compress summing it's results is equivalent to summing it's inputs. This allows us to defer implementing these critical circuits until later in the synthesis flow.

In a simple example, we can fold a*b+c using the datapath dialect to remove a carry-propagate adder:

%0 = comb.mul %a, %b : i4
%1 = comb.add %0, %c : i4

Which is equivalent to:

%0:4 = datapath.partial_product %a, %b : (i4, i4) -> (i4, i4, i4, i4)
%1:2 = datapath.compress %0#0, %0#1, %0#2, %0#3, %c : i4 [5 -> 2]
%2 = comb.add %1#0, %1#1 : i4

This is the first in a series of PRs to add datapath synthesis capabilities that will include the following (implemented but not merged):

  • Comb to Datapath pass
  • Datapath to Comb pass - for lowering datapath ops to comb gates
  • Datapath to SMT - for contract verification
  • Incorporating datapath passes into circt-synth
  • Replicate adds pass - to enable unsharing for better delay performance

let arguments = (ins Variadic<HWIntegerType>:$inputs);
let results = (outs Variadic<HWIntegerType>:$results);

let hasCustomAssemblyFormat = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use normal assembly format if possible for maintainability? I think functional-type is not that bad or you can also use custom-directives to print operand types nicely.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found it really valuable to see to make it easy to count how many rows were in my compressor - will revisit if custom directives can achieve this - using functional-type involved a lot of counting (if I recall)

PatternRewriter &rewriter) const override {
// Get operands of the AddOp
auto operands = compOp.getOperands();
SmallVector<Value, 8> processedCompressorResults;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you use SmallSetVector and replace llvm::is_contained with SmallSetVector::contains ? That should be as efficient as SmallVector + llvm::is_contained for small sizes and robust for large sets.

}

// Construct partial product array from two operands
def PartialProductOp : DatapathOp<"pp",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit: can we use more verbose name than pp such as partial_prod or partial_product?

The first step in a multiplication is to generate partial products, which
when summed, yield the product of the two operands. The partial
product operator does not specify an implementation, only that summing the
results will yield the product of the two operands.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you briefly describe the semantics of op here? Especially correspondence of number/bit position, column/row and operands.

// pp(concat(0,a), concat(0,b)) -> reduce number of results
for (Value operand : operands) {
// If the extracted bits are all known, then return the result.
auto knownBits = comb::computeKnownBits(operand);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not blocking but computeKnownBits is very expensive API (#4690) to use in canonicalizer so we might need to revisit this later.

@cowardsa cowardsa marked this pull request as ready for review July 3, 2025 14:13
@cowardsa
Copy link
Contributor Author

cowardsa commented Jul 3, 2025

Have now updated the datapath operator formatting to avoid custom assembly formats

@@ -23,5 +23,85 @@ include "circt/Dialect/HW/HWTypes.td"
class DatapathOp<string mnemonic, list<Trait> traits = []> :
Op<DatapathDialect, mnemonic, traits>;

//===----------------------------------------------------------------------===//

// Compress an array of bitvectors to a smaller set of bitvectors (at least 2).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: why not add this to the description (thus also the documentation on the website) instead?


// Compress an array of bitvectors to a smaller set of bitvectors (at least 2).
def CompressOp : DatapathOp<"compress",
[Pure, SameTypeOperands,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super-nit: this indentation looks a bit weird

let results = (outs Variadic<HWIntegerType>:$results);

let assemblyFormat =
"$inputs attr-dict `:` custom<CompressFormat>(type($inputs), type($results))";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

80 cols?

];
}

// Construct partial product array from two operands
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: comment seems redundant with the summary and could thus be removed


// Construct partial product array from two operands
def PartialProductOp : DatapathOp<"partial_product",
[Pure, SameTypeOperands,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: weird indentation


// let hasCustomAssemblyFormat = true;
let assemblyFormat =
"$multiplicand `,` $multiplier attr-dict `:` functional-type(operands, results)";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

80 cols?

Comment on lines 21 to 28
if (getNumOperands() < 3)
return emitOpError("Requires 3 or more arguments - otherwise use add");

if (getNumResults() >= getNumOperands())
return emitOpError("Must reduce the number of operands by at least 1");

if (getNumResults() < 2)
return emitOpError("Must produce at least 2 results");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the consensus is that these error strings shouldn't start with an uppercase letter (because it's not the start of a sentence and not even the start of the whole error message)


Example:
```mlir
%0:2 = datapath.compress %a, %b, %c : 3 x i16 -> (i16, i16)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since all operands and result types are always the same, why is this not just

Suggested change
%0:2 = datapath.compress %a, %b, %c : 3 x i16 -> (i16, i16)
%0:2 = datapath.compress %a, %b, %c : i16

It would remove alll the redundant information and the entire custom parser and printer could go away. It would also be more consistent with how things are done in other CIRCT dialects (e.g. most comb ops).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My ideal format would be
%0:2 = datapath.compress %a, %b, %c : 3 x i16

Reasoning - when these compressors become large - it is valuable to be able to quickly read off how many rows the compressor is summing - without this annotation you need to count the number of arguments

Problems: have had great difficulties understanding how assemblyFormat works... So if anyone is able to help achieve the above that would be super helpful?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd argue that you can easily use your editor/IDE (e.g., vim) to give you this information by counting the number of % minus 1 in that line instead of annotating these things in the IR.

If you really want this format, you can significantly reduce what the custom directive is doing. It should just print num-operands x and in the parser it should parse a number and x and just throw it away as it is redundant anyway. The operands and type should be part of the assemblyFormat not the directive and with the right traits (the ones you already added should be enough) it will automatically infer the other operands and all result types.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maerhart - do you have an example of how to print type($inputs[0[) as the comb example just uses the result - which in this case we have variadic inputs and outputs?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry! This is trickier than I thought. One way I have seen people work around this is to define one extra operand or result outside of the variadic and use that as the anchor for the type but that leads to other annoyances, so I'd probably rather avoid that and pay the cost of the custom parser/printer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's also no way to get the number of results from the %0:2 = prefix (which I guess is the reason you kept the (i16, i16)). So I guess there are two options:

  1. Custom parser/printer for something like %0:2 = datapath.compress %a, %b, %c : 2 x i16
  2. Use the ODS functional-type

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok how about the following that just involves one custom-directive and prints the type only once - I accept there is replication in the 2 but given the additional code required to get %0:2 this is perhaps more maintainable?

%0:2 = datapath.compress %a, %b, %c : i16 [3 -> 2]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to there regression tests for the errors, we also have at least one test per operation (often in a file called basic.mlir) where circt-opt is only invoked with the round-trip option.

}
};

struct FoldAddIntoCompress : public OpRewritePattern<comb::AddOp> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it often helps readability a lot to add a typical example of an application of a canonicalization to the docsting of the pattern struct.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have added comments of basic examples and where we may have mutliple folds e.g. in Compress constant fold - have mimicked the CombFolds commenting style

@cowardsa
Copy link
Contributor Author

cowardsa commented Jul 4, 2025

Believe I've addressed all the comments above now - please let me know if any further concerns @maerhart or @uenoku?

Copy link
Member

@uenoku uenoku left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM other than several nits 👍


// Only fold if we have constructed a larger compressor than what was
// already there
if (!(shouldFold))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!(shouldFold))
if (!shouldFold)

// CHECK-NEXT: datapath.partial_product %a, %b : (i3, i3) -> (i3, i3, i3)
%0:3 = datapath.partial_product %a, %b : (i3, i3) -> (i3, i3, i3)
hw.output %0#0, %0#1, %0#2 : i3, i3, i3
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: insert new line

Suggested change
}
}

@@ -0,0 +1,15 @@
// RUN: circt-opt %s | circt-opt | FileCheck %s
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: You ca ncheck with -verify-roundtrip

Suggested change
// RUN: circt-opt %s | circt-opt | FileCheck %s
// RUN: circt-opt %s -verify-roundtrip| FileCheck %s

Comment on lines 191 to 194
auto newCompressOp = rewriter.create<CompressOp>(
op.getLoc(), inputs.drop_back(), op.getNumResults());

rewriter.replaceOp(op, newCompressOp.getResults());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work?

Suggested change
auto newCompressOp = rewriter.create<CompressOp>(
op.getLoc(), inputs.drop_back(), op.getNumResults());
rewriter.replaceOp(op, newCompressOp.getResults());
rewriter.replaceOpWithNewOp<CompressOp>(
op, inputs.drop_back(), op.getNumResults());

Comment on lines 249 to 250
while (newResults.size() < op.getNumResults())
newResults.push_back(zero);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
while (newResults.size() < op.getNumResults())
newResults.push_back(zero);
newResults.append(op.getNumResults() - newResults.size(), zero);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this - was looking for a neater way to do this!

//===----------------------------------------------------------------------===//
// Partial Product Operation
//===----------------------------------------------------------------------===//
struct ConstantFoldPartialProduct : public OpRewritePattern<PartialProductOp> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This canonicalization pattern looks like width narrowing. MLIR has folder API for constant folding so can we rename this pattern to avoid confusion?

@cowardsa
Copy link
Contributor Author

cowardsa commented Jul 7, 2025

Have now addressed the nits and still awaiting commit access approval so will need someone else to merge for me if satisfied please?

@uenoku uenoku merged commit f26f1ed into llvm:main Jul 7, 2025
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants