-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generator Enhancements #1523
Generator Enhancements #1523
Conversation
No comments? |
Haven't looked closely at the code yet, but I have a few half-formed thoughts on some parts of the design. Sorry if this comes out a little incoherent - I am sleep-deprived and have a cold.
|
One legitimate reason to keep the distinction is that GeneratorParams will often have useful default values that you want to keep (so you only want to specify some of them), while Inputs must always be specified. This is likely to be even more pervasive going forward (as we use this approach to write more-flexible, more-reusable Generator libraries); we don't want to end up with (say) a Blur component that requires recapitulation of a half-dozen extra arguments.
Yeah, it's suboptimal; if C++ had a named-parameter-with-default-values mode that would be the thing to use here, but, alas, it doesn't.
The intent is that you'd only use this form when the values are compile-time constants. (I kinda like having this as an optional syntax as IMHO it reinforces the idea that certain params are "compile-time" vs runtime.)
IMHO the double-call-operator idiom is rare for a good reason; it looks weird and jars the reader. I'd prefer to avoid it.
The actual code already accepts "this"; this documentation is out of date (from an earlier rev of the code) and I neglected to update. Willfix.
This is true, and I can't think of a reason offhand to prevent such an approach from working (other than "that's not how I'd code it", which is a terrible reason). Assuming we can't think of a reason this should be prevented, we should document that it's supported and add tests to verify it work. That said, I think that the explicit schedule() method approach makes for more-readable code and should be the approach that we document as recommended.
Agreed...
...Disagree strongly. Checking in machine-generated code is pretty much always a bad idea.
Agreed 100%, adding documentation is critical and is missing from this PR. (I'd prefer to add it in a subsequent PR, however.)
Disagree: IMHO one of the desirable properties of Stubs is to add a separation of interface vs implementation to a Generator that's intended as a reusable component; if the Stub doesn't have enough information in its API + Documentation to be useful, that's a failure of this design. |
I also haven't looked at the code yet, just read through the description. Can you declare a Func Input that produces a Tuple the same way you can declare Output tuples? The template method of passing generator params to a make method will not work for floats (can't be template parameters) or anything other than integral types I believe. That will be annoying, might be better to just enable syntax like:
Maybe that already works? In the examples, the outer generator (AwesomeFilter) schedules the last step of the inner generator (RgbToYCbCr) by vectorizing it. In my experience, the inner generator often wants to do something interesting in the schedule that the outer component shouldn't have to know about. For example, if the inner generator is a demosaic, then it is quite likely that it will want it's output to be scheduled with at least the following:
This is not something that is easy to communicate in the design. If this is left up to the calling generator, then I don't see any way to get this other than to specify in the documentation of the generator that it wants a particular property (e.g. unrolling in x and y by 2 to simplify some selects). In my code, I use the lambda strategy mentioned above, and I let the inner code (not a generator, just a function) do the "loop" scheduling (some splits, vectorizing, unrolling, etc.) and have the outer code do the "locality" scheduling. However, the split isn't perfect. Some splits are relevant to the inner code, and some splits are relevant to the outer code. |
Not at present, but that could be made to happen with modest work.
Correct; we support this via a hacky std::ratio workaround, but as it turns out, float GeneratorParams are almost non-existent (I count exactly two instances inside of Google, both of which could be implemented other ways.)
Yes, via the standard ctor.
Fair enough -- I don't think we're going to be able to achieve perfect insulation here. I think these cases are going to need to be handled case-by-case via documentation. |
I think not having the prototype available at coding-time when calling a generator is problematic. We need to figure out something reasonable for when someone clicks on one of these calls and wants to jump to its declaration to check the docs for it, or if someone expects their IDE to give them hints while typing out a call to it. Checking in the stubs violates the common rule against checking in build artifacts, but I think the alternative is worse. This issue is already a common complaint when calling AOT-compiled Halide pipelines, but it's less of a big deal because typically the person calling the pipeline is also the person who wrote it, so they know what the arguments are based on the Params and Generator Params. Regarding GeneratorParams in the call syntax - I think we should take a look at some generators we have and see if the GeneratorParams are more or less likely to be left at their default values relative to the params. Something like a Type always needs to be set. If they're no more or less likely, there's no reason to separate them from params in the call syntax. If they're much less likely to be anything other than the default, then it makes more sense to put them in a struct. Another option is to put them in the signature in order after all the params, with default values. People writing generators would order the generator params by how likely it is that they'll be non-default, which lets callers specify as many of them as they like:
It's also a little weird that Type generator params need to be set at all when they describe the type of input Funcs. What if |
We are planning to automatically generate outputs in addition to the stub, Access to the stub prototypes is basically the same as with stub generators Doing automatic inference of types from input Funcs is near the top of the -Z- On Fri, Sep 30, 2016 at 10:37 AM, Andrew Adams notifications@github.com
|
# Conflicts: # src/Generator.cpp
From offline discussion: it was suggested that Stubs should accept Inputs (aka Params) in struct form, similar to how Generator/ScheduleParams are handled; this allows for nicer symmetry, while still allowing inline “calls-style” declarations via C++11 aggregate initialization syntax. Take a look and see what you think.
From offline discussion: it was suggested that Stubs should accept Inputs (aka Params) in struct form, similar to how Generator/ScheduleParams are handled; this allows for nicer code symmetry, while still allowing inline “call-style” declarations via C++11 aggregate initialization syntax. PTAL. |
Gentle ping for comments on 77a0e97 |
Added revisions to add type-and-dim inference for Func, and size inference for Array (and updated long PR comment description to reflect this). PTAL, I think this is close enough to consider landing. |
std::pair<int64_t, int64_t> rational_approximation(double d) { | ||
if (std::isnan(d)) return {0, 0}; | ||
if (!std::isfinite(d)) return {(d < 0) ? -1 : 1, 0}; | ||
// TODO: fix this abomination to something more intelligent |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a fun topic:
https://www.youtube.com/watch?v=CaasbfdJdJg
I'll open a small PR with my solution. It's overkill.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ouch, yeah, I overlooked that one. Frankly, at this point I'm tempted to say we should just pull the templated "constructors" entirely; I'm not sure they're worthwhile now that we've added type/dim/size inference. Anyone else have opinions?
Fancier method for finding a good rational
Halide::Var x, y, c; | ||
|
||
template<typename Type> | ||
Image<Type> MakeImage(int extra) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: make_image here and in the other tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
|
||
// We statically know the types we want, so the templated construction method | ||
// is most convenient. | ||
auto gen = StubTest::make<>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused by the comment - no template arguments are provided
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the comment is stale. Willfix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
// This generator defaults intermediate_level to "undefined", | ||
// so we *must* specify something for it (else we'll crater at | ||
// Halide compile time). We'll use this: | ||
sp.intermediate_level = LoopLevel(gen.f, Var("y")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Passing in the variable name by string seems like an anti-pattern. Is there another way to do this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now: no, not really. The alternative which we've discussed is to put the public-facing Vars into the Stub as part of the contract, e.g.
sp.intermediate_level = LoopLevel(gen.f, gen.y);
but since we currently guarantee/require that Vars always match by name, I haven't done so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updating this: as it turns out, the addition of type/dim/size inference to Stubs makes it hard to infer the output Vars... To infer the output Vars for the Stub, we have to call generate() so that all the Output values are valid. But if any Input<> or Output<> values have unspecified values, we can't do that... and we can't just guess at a hopefully-valid value since there may be constraints in the code we can't predict.
I'm pushing a change with a revision to use Func::args() instead:
sp.intermediate_level = LoopLevel(gen.f, gen.f.args().at(1));
(Note that I'm using args().at() rather than args()[] since the former guarantees exception/halt if entry isn't found, rather than returning a default as with operator[])
After an offline discussion with zalman@, we'd like to hold off on trying to gather the used-by-output-vars into the Stub, mainly because (1) it's hard, and (2) we'd like to actually try out the idiom above in zalman@'s code to see if it feels good enough.
AFAIK this is the last nontrivial stumbling block; LMK your thoughts.
// Use a little variadic macro hacking to allow two or three arguments. | ||
// This is suboptimal, but allows us more flexibility to mutate registration in | ||
// the future with less impact on existing code. | ||
#define _HALIDE_REGISTER_GENERATOR2(GEN_CLASS_NAME, GEN_REGISTRY_NAME) \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're going to use macro magic, couldn't we also encapsulate the "auto register_me = " part?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The embarrassing answer: I can't figure out a way to guarantee a unique name for the variable when multiple registrations occur in the same source file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Scratch that comment: an approach just occurred to me and I like it, so I've pushed it out there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
string-pasting the class name looks good. __COUNTER__
is also handy for unique names
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had to google __COUNTER__
-- looks like an MS extension? Is it widely supported?
|
||
} // namespace | ||
|
||
namespace StubNS1 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why must it go in a namespace?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Requiring good hygiene up front: I submit that if you are going to use a Stub class, putting it in the global namespace is ~always the wrong thing to do. Thus we require it to be elsewhere.
I agree that we should remove them, though I'm sad to not be able to use On Tue, Oct 18, 2016 at 12:15 PM, Steven Johnson notifications@github.com
|
I pulled & applied your fraction thingy for now: offline conversations with zalman@ indicated he had plans for use of the templated constructors, at least for now, so I'm inclined to keep with the option to remove later if they don't prove useful. |
static const std::map<std::string, LoopLevel> halide_looplevel_enum_map{ | ||
{"root", LoopLevel::root()}, | ||
{"undefined", get_halide_undefined_looplevel()}, | ||
{"inline", LoopLevel()}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are ScheduleParam<LoopLevel>
default values specified by string instead of by actual LoopLevel objects? This is one part that felt a little weird to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The textual values specified here can only be used in build rules (e.g. Makefiles); since there is no unique textual representation for a Func, there can't be a unique textual representation for an arbitrary LoopLevel. That said: these three possibilties are "special" and arguably worth providing a way to specify at build time.
General comment, now that I've finally read through most of this: I like the direction this is going. Andrew and Dillon have much more practical experience using this stuff lately, so their specific concerns are clearly relevant, but at a high level, I like it! |
I believe clang and gcc also support it. On Tue, Oct 18, 2016 at 5:23 PM, Steven Johnson notifications@github.com
|
re: COUNTER, that level of preprocessor-fu makes me a little nervous; I'm inclined to avoid it unless/until we have clear evidence that the current approach is not good enough. |
— add tests — special-case non-finite numbers and negative numbers to get predictable results
This allows us to remove lots of Halide:: noise from generated code
@@ -907,16 +912,18 @@ $(FILTERS_DIR)/pyramid.a: $(BIN_DIR)/pyramid.generator | |||
@-mkdir -p $(TMP_DIR) | |||
cd $(TMP_DIR); $(CURDIR)/$< -f pyramid -o $(CURDIR)/$(FILTERS_DIR) target=$(HL_TARGET) levels=10 | |||
|
|||
MDTEST_GEN_ARGS=input.type=uint8 input.dim=3 output.type=float32,float32 output.dim=3 input_not_nod.type=uint8 input_not_nod.dim=3 input_nod.dim=3 input_not.type=uint8 array_input.size=2 array_i8.size=2 array_i16.size=2 array_i32.size=2 array_h.size=2 array_outputs.size=2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"MDTEST" does not feel like a productive abbreviation here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
} | ||
|
||
/** Emit spaces according to the current indentation level */ | ||
std::string ind(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the savings of three characters here is worth the loss of clarity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
} | ||
}; | ||
|
||
class GIOBase { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we have a short comment explaining the purpose of this class? (Or perhaps the whole class hierarchy around this.)
@@ -533,14 +1370,26 @@ class NamesInterface { | |||
static inline Type UInt(int bits, int lanes = 1) { return Halide::UInt(bits, lanes); } | |||
}; | |||
|
|||
class JITGeneratorContext : public GeneratorContext { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the Generator context classes get used as part of the API, so they should have Doxygen comments.
Modulo a few requests for comments and discussion re: updating the description to reflect recent changes, LGTM. |
# Conflicts: # test/CMakeLists.txt
— remove Generator::context() — allow Stubs to accept a context by either pointer or ref
Revised & expanded version of the PR comment above added to the wiki at https://github.com/halide/Halide/wiki/Generator-Enhancements |
Updated (rewrote) doxygen comment for Generator. I have an LGTM from zalman@, so if there are no objections, I'm going to land this once the travis checks pass again. |
This PR introduces some enhancements to the Generator infrastructure; the goals are
Note that none of these changes break existing Generators (all existing Generators should work as-is); it is not the intent of this PR to force existing Generators to migrate to this style anytime in the near future.
Replacing
Param<X>
withInput<X>
(andImageParam
withInput<Func>
)Param<>
will continue to exist, but Generators can now use a newclass,
Input<>
, instead. For scalar types, these can be considered essentiallyidentical to
Param<>
, but have a different name for reasons of code clarity,as we'll see later.
Similarly,
ImageParam
will continue to exist, but Generators caninstead use a
Input<Func>
. This is (essentially) like anImageParam
, withthe main difference being that it may (or may not) not be backed by an actual
buffer, and thus has no defined extents.
It is an error for a Generator to declare both
Input<>
andParam<>
orImageParam
(i.e.: if you useInput<>
you may not use the previous syntax).Note that
Input<>
is intended only for use with Generator, and is notintended for use in other Halide code; in particular, it is not intended to
replace
Param<>
, except for inside Generators.Example:
becomes
You can optionally make the type and/or dimensions of
Input<Func>
unspecified, in which case the value is simply inferred from the actual Funcs passed to them. Of course, if you specify an explicit Type or Dimension, we still require the inputFunc
to match, or a compilation error results.When a Generator using
Input<Func>
is compiled directly (e.g., using GenGen), theInput<Func>
must be concretely specified; if Type and/or Dimensions are unspecified, you can specify them using implicit GeneratorParams with names derived from the Input or Output. (In the example above, input has an implicit GeneratorParam named "input.type" and an implicit GeneratorParam named "input.dim".)Explicitly Declaring Outputs
Currently, all of a Generator's inputs can be determined by introspecting its
members, but information about its outputs must be determined by calling its
build()
method and examining the return value (which may be aFunc
or aPipeline
).With this change, a Generator can, instead, explicitly declare its output(s) as
member variables, and provide a
generate()
method instead of abuild()
method.(These are equivalent aside from the fact that
generate()
does not return avalue.)
Example:
becomes
As with
Input<Func>
, you can optionally make the type and/or dimensions of anOutput<Func>
unspecified; any unspecified types must be resolved via an implicit GeneratorParam in order to use top-level compilation.Note that
Output<>
is intended only for use with Generator, and is notintended for use in other Halide code.
The Generator infrastructure will verify (after calling
generate()
) that alloutputs are defined, and have definitions that match the declaration.
You can specify an output that returns a Tuple by specifying a list of Types:
A Generator can define multiple outputs (which is quietly implemented as a
Pipeline
under the hood):We also allow you to specify Output for any scalar type (except for Handle
types); this is merely syntactic sugar on top of a zero-dimensional Func, but
can be quite handy, especially when used with multiple outputs:
Note that it is an error to define both a
build()
andgenerate()
method.Array Inputs and Outputs
You can also use the new syntax to declare an array of
Input
orOutput
, byusing an array type as the type parameter:
You can also lee array size unspecified, in which case it will be inferred from the input vector, or (optionally) explicitly specified via a resize() method:
An Array Input/Output with unspecified size must be resolved to a concrete size for toplevel compilation; there are now implicit GeneratorParam<size_t> that allow to to set this, based on the name ("pyramid.size" in the example above).
Note that both Input and Output arrays support a limited subset of the methods from
std::vector<>
:operator[]
size()
begin()
end()
resize()
Separating Scheduling from Building
A Generator can now split the existing
build()
method into two methods:Such a Generator must move all scheduling code for intermediate
Func
intothe
schedule()
method. Note that this means that schedulableFunc
,Var
,etc will need to be stored as member variables of the Generator. (Since
Output<>
are required to be declared as member variables, these are simpleenough, but intermediate
Func
that need scheduling may require motion.)Example:
becomes
Note that the output
Func
doesn't have a scheduling directive forcompute_at()
orstore_at()
in either case: it is either implicitlycompute_root()
(when being compiled directly into a filter), or explicitlyscheduled by its caller (when being used as a subcomponent, as we'll see later).
Even if the intermediate Halide code doesn't have any scheduling necessary (e.g.
it's all inline), you should still provide an empty
schedule()
method to makethis fact obvious and clear.
Example:
becomes
Converting
GeneratorParam
intoScheduleParam
where necessaryGeneratorParam
is now augmented by the newScheduleParam
type. Allgenerator params that are intended to be used by the
schedule()
method shouldbe declared as
ScheduleParam
rather thanGeneratorParam
. This has twopurposes:
information between arbitrary Generators (as we'll see later).
future Autoscheduler work.
Note that there are common
GeneratorParam
conventions that already act asScheduleParam
(most notably,vectorize
andparallelize
); this merelyformalizes the previous convention.
GeneratorParam
andScheduleParam
will continue to live inside a singlenamespace (i.e., it is an error to declare a
GeneratorParam
andScheduleParam
with the same name).While a
GeneratorParam
can be used from anywhere inside a Generator (eitherthe
generate()
orschedule()
method), aScheduleParam
should be accessedonly within the
schedule()
method. (We'd like to make this a compile-timeerror in the future.)
Note that while
GeneratorParam
will continue to be serializable to and fromstrings (just as GeneratorParams are), some
ScheduleParam
values are notserializable, as they may reference runtime-only Halide structures (most
notably,
LoopLevel
, which cannot be reliably specified by name in the generalcase). Attempting to set such a
ScheduleParam
from GenGen will cause acompile-time error.
Example:
becomes
Note that
ScheduleParam
can have other interesting values too, most notablyLoopLevel
:Note that
ScheduleParam<LoopLevel>
can default to "root", "inline", or"undefined"; all other values (e.g. Func-and-Var) must be specified in actual
code. (It is explicitly not possible to specify LoopLevel(Func, Var) by name,
e.g. "func.var"; although Halide uses such a convention internally, it is not
currently possible to guarantee unique Func names across an arbitrary set of Generators.)
Note that it is an error to use an undefined LoopLevel for scheduling.
Generator Stubs
Let's start with an example of usage, then work backwards to explain what's
going on. Say we have an RGB-to-YCbCr component we want to re-use:
GenGen now can produce a "
Func
-like" stub class around a generator, which (by convention)is emitted in a file with the extension ".stub.h". It looks something like:
Note that this is a "header-only" class; all methods are inlined (or
template-multilinked, etc) so there is no associated .cpp to incorporate. Also
note that this is a "by-value", internally-handled-based class, like most other
types in Halide (e.g.
Func
,Expr
, etc).We'd consume this downstream like so:
It's worth pointing out that all inputs to the subcomponent must be explicitly
provided when the subcomponent is created (as arguments to its ctor); the caller
is responsible for providing these. (There is no concept of automatic input
forwarding from the caller to a subcomponent.)
What if
RgbToYCbCr
has array inputs or outputs? For instance:In that case, the generated
RgbToYCbCrMulti
class requires vector-of-Func (orvector-of-Expr) for inputs, and provides vector-of-Func as output members:
What if
RgbToYCbCr
has multiple outputs? For instance:In that case, the generated
RgbToYCbCrMulti
class has all outputs as structmembers, with names that match the declared names in the Generator:
Note that scalar outputs are still represented as (zero-dimensional) functions,
for consistency. (Also note that "output" isn't a magic name; it just happens to
be the name of the first output of this Generator.)
Note also that the first output is always represented both in an "is-a"
relationship and a "has-a" relationship: RgbToYCbCrMulti overloads the necessary
operators so that accessing it as a Func is the same as accessing its "output"
field, i.e.:
This is (admittedly) redundant, but is deliberate: it allows convenience for the
most common case (a single output), but also orthogonality in the multi-output
case.
The consumer might use this like so:
What if there were
GeneratorParam
we wanted to set inRgbToYCbCr
, toconfigure code generation? In that case, we'd pass a value for the optional
generator_params
field when calling its constructorThis would produce a different (generated) definition of
GeneratorParams
, with a field for eachGeneratorParam
, initializedto the proper default:
We could then fill this in manually:
Alternately, if we know the types at C++ compilation time, we can use a templated
construction method that is terser:
What if there are
ScheduleParam
inRgbToYCbCr
?In that case, the generated stub code would have a different declaration for
ScheduleParams
:And we might call it like so: