Add the uniformity analysis to the WGSL spec#1571
Conversation
wgsl/index.bs
Outdated
|
|
||
| ### Uniformity analysis overview ### {#uniformity-overview} | ||
|
|
||
| In this section we specify an analysis that verifies that functions which are only safe to call in uniform control flow (barriers and derivatives) are only called in such a context. |
There was a problem hiding this comment.
"e.g." barriers and derivatives
wgsl/index.bs
Outdated
|
|
||
| Control-flow is said to be uniform at a point in the program if all invocations execute in lockstep at that program point. | ||
| An expression is said to be uniform if it is in uniform control-flow, and all invocations compute the same value for it. | ||
| The location of a local variable is said to be uniform, if all invocations have the same value in that variable at every program point where it is live. |
There was a problem hiding this comment.
Do you really mean "location" of a local variable? Why not just a variable?
There was a problem hiding this comment.
I'll try to replace it with just variable, I can't remember why I went with the more complicated phrase.
wgsl/index.bs
Outdated
| - If the analysis refuses a program, it provides a straightforward chain of implications that can be used to craft a good error message | ||
| </div> | ||
|
|
||
| The analysis analyzes each function in turn, verifying that there is a context where it is safe to call this function (otherwise it rejects the program), and computing metadata about the function to help analyze its callers in turn. This means that the call graph must first be built, and functions must be analyzed from the leaves (functions that call no function outside the standard library) upwards (toward the entry point), so that whenever we analyze a function we have already computed |
There was a problem hiding this comment.
Nit: there are a lot of parentheticals here. I think reading them is required to understand the paragraph, so they should be promoted to regular prose.
There was a problem hiding this comment.
thanks, will fix.
wgsl/index.bs
Outdated
| </div> | ||
|
|
||
| The analysis analyzes each function in turn, verifying that there is a context where it is safe to call this function (otherwise it rejects the program), and computing metadata about the function to help analyze its callers in turn. This means that the call graph must first be built, and functions must be analyzed from the leaves (functions that call no function outside the standard library) upwards (toward the entry point), so that whenever we analyze a function we have already computed | ||
| the metadata for all of its callees. There is no risk of being trapped in a cycle, as recurrence is forbidden in the language. |
There was a problem hiding this comment.
Why is there a newline here? (Throughout the rest of this PR, you seem to be putting paragraphs on single lines, except for this one specific place.)
There was a problem hiding this comment.
It might be valuable to use the term "topographical sort" to make this more clear
There was a problem hiding this comment.
The newline is just an accident. Explicitly using the term "topographical sort" is a good idea, I'll add it.
wgsl/index.bs
Outdated
| The first phase walks over the AST of the function, building a directed graph along the way. | ||
| The second phase explores that graph, resulting in either rejecting the program, or computing the constraints on calling this function. | ||
|
|
||
| Note: Each vertex in this graph corresponds either to the statement "The control-flow at a specific program point must be uniform" or to "The value of a specific expression must be uniform" or to "A specific variable must always hold a uniform value" or is one of the special vertices True and False. |
There was a problem hiding this comment.
What does "corresponds to" mean here? Does it mean that each vertex holds log2(5) bits of state? Or does each vertex actually just hold 1 bit of state, and the meaning of that one bit is different depending on what kind of AST node it corresponds to?
There was a problem hiding this comment.
Also, paraphrasing each of these 5 values, I see:
- Something must be uniform
- Something else must be uniform
- A third thing must hold a uniform value
- true
- false
So how does a vertex indicate the possibility of non-uniformity? None of the states (except for "false") seem to be able to describe that.
There was a problem hiding this comment.
"Corresponds to" means "here is how I think about the meaning of this, and why the rules look like they do". The vertices hold no information, they can be represented by a single integer, and edges are just pairs of integers.
Something having to be uniform is indicated by having a path from True.
Something that cannot be guaranteed to be uniform is indicated by having a path to False.
I will try to make it more explicit.
wgsl/index.bs
Outdated
|
|
||
| TODO: do we need an extra behavior for possibly infinite loops? I should check whether some form of reconvergence happens after something like `while(tid mod 2) {}`. | ||
|
|
||
| If the set of behaviors for a statement is a singleton, we take that to mean that reconvergence happened, and we pick the vertex corresponding to control-flow at the start of the statement as the vertex corresponding to control-flow at the exit of the statement, overriding the other rules below. |
There was a problem hiding this comment.
Should we be using words like "we" in a spec? Maybe instead of "we take that to mean that" we should instead say "this means that"
There was a problem hiding this comment.
good point, will fix.
wgsl/index.bs
Outdated
|
|
||
| ### Uniformity rules for function calls ### {#uniformity-function-calls} | ||
|
|
||
| The most important and complex rule is for function calls: |
There was a problem hiding this comment.
Should we really be ascribing importance to individual rules? It's certainly the most complex, but "important" seems like a value judgement.
There was a problem hiding this comment.
Right, I guess for a spec it might be inappropriate phrasing. I'll try to rephrase it.
wgsl/index.bs
Outdated
| <td> | ||
| <td class = "nowrap">*CF*, *s1* -> *CF1*, *B1*<br> | ||
| *CF1*, *s1* -> *CF2*, *B2* | ||
| <td>*B1* \ {Nothing} U *B2* |
There was a problem hiding this comment.
Can we use the actual Unicode characters for these operations?
- U+2229 INTERSECTION
- U+222A UNION
- U+2208 ELEMENT OF
- U+2209 NOT AN ELEMENT OF
- U+2216 SET MINUS
- U+2205 EMPTY SET
- U+2282 SUBSET OF
There was a problem hiding this comment.
Yes, I took the shortcut of staying with ascii, but I probably should replace all of these. Thanks for the link.
There was a problem hiding this comment.
E.g. ∩ -> ∩? Fancy!
There was a problem hiding this comment.
https://www.toptal.com/designers/htmlarrows/math/
∩ ∪ ⋂ ⋃ ∈ ∉ ∖ ∅ ⊂ ⊄ ⊆ ⊇ ⊅ ⊃
∩ ∪ ⋂ ⋃ ∈ ∉ ∖ ∅ ⊂ ⊄ ⊆ ⊇ ⊅ ⊃
wgsl/index.bs
Outdated
| <table class='data'> | ||
| <caption>Uniformity rules for statements</caption> | ||
| <thead> | ||
| <tr><th>Statement<th>New vertices<th>Precondition<th>Behaviors<th>Resulting control-flow vertex<th>New edges |
There was a problem hiding this comment.
for loops don't seem to be listed here. We should either
A) Describe the desugaring in a more robust way in the "for statement" section of the spec, and link to it here
B) Add a row in the table for the for loop.
There was a problem hiding this comment.
I got confused by the "new vertices" column heading of the chart, because the "resulting control-flow vertex" is a new vertex, but doesn't appear in the "new vertices" column.
There was a problem hiding this comment.
I'm having trouble understanding the "precondition" column. Above, you say
In the table below,
CF1, S -> CF2, Bmeans "run the analysis on S starting with control-flow CF1, apply the required changes to the graph, and name the resulting control-flow CF2, and the resulting set of behaviors B".
And these CF1, S -> CF2, B rules are inside the "precondition" column of the table. How can a precondition involve running an analysis and modifying a graph? I always thought a precondition was just a claim that either is true or untrue, and didn't modify state or anything.
There was a problem hiding this comment.
I did not list the rules for "for" loops, because they are supposed to desugar to "loop".
The "resulting control-flow vertex" may be a brand new vertex (in which case it will also be in the "new vertices column"), but it may instead be the result of the application of a rule on a sub-expression.
You are right that "precondition" is a bit of an abuse of language here, I should not have kept it from more conventional type systems. Maybe "recursive analyses" or something like that is a better name for this column.
| *CF1*, *s1* -> *CF2*, *B2* | ||
| <td>*B1* \ {Nothing} U *B2* | ||
| <td>*CF2* | ||
| <td> |
There was a problem hiding this comment.
How can there not be a new edge between s1 and s2 in s1; s2? I think I'm missing something fundamental here.
There was a problem hiding this comment.
Oops, there is a typo here: it should be *CF1*, *s2* -> *CF2*, *B2* on the second line, I accidentally repeated s1 instead of s2.
I guess it did not help with understanding, sorry. To further clarify things:
- s1 and s2 are statements, not vertices in the graph, so it makes no sense to have edges between them.
- the first line says that CF1 is the vertex corresponding to the control-flow at the exit of s1.
- the second line says to then use it as the control-flow at the entrance of s2, and name the vertex corresponding to the control-flow afterwards CF2
- so we don't need to explicitly create a new edge here, because the rules for
*CF1*, *s2* -> *CF2*, *B2*must already have made a path from CF2 to CF1, since we cannot have uniform control-flow after s2 if it was non-uniform before it.
There was a problem hiding this comment.
+1 The way I'm thinking of it is that each statement generates two nodes in the analysis graph, essentially a precondition and a post-condition. So the two lines express CF1===Post(s1) === Pre(s1) and so the path in the analysis graph is CF -> CF1 -> CF2
wgsl/index.bs
Outdated
| <tr><td class="nowrap">loop {*s1*} | ||
| <td>*CF'* | ||
| <td class="nowrap">*CF'*, *s1* -> *CF1*, *B1* | ||
| <td>*B1* \ {Break, Continue} U {Nothing} |
There was a problem hiding this comment.
Are you sure that loop should U {Nothing}? Control flow will always enter the loop, and it can't exit without a break or a return or a discard.
There was a problem hiding this comment.
You are correct, I took that part from the rules for while, which can exit whenever the condition is true.
The more precise behaviors here should be B1, removing Continue, and replacing Break by Nothing if it exists.
I'll make the change in the next commit.
|
Thanks a lot @litherum for all of these comments! They inform me of which parts need some clarification. |
|
Instead of "true" and "false" we might want to use "definitely uniform" and "possibly nonuniform" |
This is almost what I had in a previous version of the analysis. I picked True/False to make the analogy between edges and logical implication clearer, but I can easily reverse this and go back to these names. |
|
Before getting into the heart of the analysis:
Also, right now WebGPU is guaranteeing (i asked long ago) that there is no aliasing of storage locations across resource variables. That's probably implicitly assumed by the analysis. For infinite loops: I think the standard thing to say is that no semantics is defined for the program if it loops infinitely. Shaders are assumed to finish in finite time; in real life operating systems intervene to kill runaway processes, which pragmatically speaking is the same. I'd be interested to see if the analysis allows an interesting example such as @toji 's clustered-forward shading example: https://twitter.com/tojiro/status/1344409446802362372?lang=en and live page here |
wgsl/index.bs
Outdated
|
|
||
| Control-flow is said to be uniform at a point in the program if all invocations execute in lockstep at that program point. | ||
| An expression is said to be uniform if it is in uniform control-flow, and all invocations compute the same value for it. | ||
| A local variable is said to be uniform, if all invocations have the same value in that variable at every program point where it is live. |
There was a problem hiding this comment.
I think this is a little misleading, since it's not "same value" really, but rather the value is dependent on the same control flow in all invocations.
There was a problem hiding this comment.
Something must be said about what "all invocations" mean here. Is it "all" through the draw/dispatch? "all" in the current scope? Or even "all" as in all active at any point within the wave?
There was a problem hiding this comment.
I disagree with @jdashg's comment here. The way I use the term, a variable can be in uniform control flow and be non-uniform because it has different values across the scope of the uniformity. I would argue this definition because it is useful. If a control flow construct depends on a non-uniform variable under my definition, the control flow will be non-uniform. To express that in a definition where uniformity of a variable depends on the uniformity of its control flow, we would have to redefine a characteristic of variables to plug into that statement.
Furthermore, the uniformity of a uniform buffer concerns the persistence of its values across a broad scope. The same should apply to defining a variable as uniform.
alan-baker
left a comment
There was a problem hiding this comment.
From the 2021-03-30 meeting, I was asked to link to Vulkan's scoping definitions.
Vulkan defines invocation group as workgroup if that is a defined concept, otherwise it is the command. See https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#shaders-scope-command
For shaders without defined workgroups, this set of invocations forms an invocation group as defined in the SPIR-V specification.
SPIR-V only defines uniform control flow (i.e. divergence and reconvergence) in terms of invocation groups. See https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html#UniformControlFlow.
wgsl/index.bs
Outdated
| The following definitions are merely informative, trying to give an intuition for what the analysis in the next subsection is computing. | ||
| The analysis is what actually defines these concepts, and when a program is valid or break the uniformity rules. | ||
|
|
||
| Control-flow is said to be uniform at a point in the program if all invocations execute in lockstep at that program point. |
There was a problem hiding this comment.
Even informatively I would avoid saying lockstep execution occurs. Also, I think the invocations need scoped. For compute each workgroup defines uniformity. For other shaders I'd lean towards what SPIR-V defines as an invocation group (though this may prove to be to restrictive).
There was a problem hiding this comment.
For purposes of utility, I would define uniform control flow as being control flow that depends on values certain to be the same over the defined scope of uniformity. The programmer doesn't care if they are executed in lockstep or not. It's not strictly relevant to the restrictions this introduces. Rather than having to define how hardware works, let's define how you can write a program that trips it up.
wgsl/index.bs
Outdated
|
|
||
| <table class='data'> | ||
| <caption>Uniformity rules for expressions in lvalue positions</caption> | ||
| <thread> |
There was a problem hiding this comment.
| <thread> | |
| <thead> |
There was a problem hiding this comment.
thanks, fixed.
wgsl/index.bs
Outdated
| <td>*B1* \ {Nothing} U *B2* | ||
| <td>*CF2* | ||
| <td> | ||
| <tr><td class="nowrap">if (*e*) then *s1* else *s2* |
There was a problem hiding this comment.
I assume this chains for elseif, but it would be good to include.
|
First discussion in meeting of 2021-03-30 |
WGSL meeting minutes 2021-03-30
|
dneto0
left a comment
There was a problem hiding this comment.
I only partially reviewed this.
Sending this review now because it has actionable feedback, particularly on request for clarity of what's being modeled.
wgsl/index.bs
Outdated
| The following definitions are merely informative, trying to give an intuition for what the analysis in the next subsection is computing. | ||
| The analysis is what actually defines these concepts, and when a program is valid or break the uniformity rules. | ||
|
|
||
| Control-flow is said to be uniform at a point in the program if all invocations execute in lockstep at that program point. |
There was a problem hiding this comment.
Wording: "lockstep" is the wrong word here. Let's work on this as an editorial fix.
wgsl/index.bs
Outdated
| The analysis is what actually defines these concepts, and when a program is valid or break the uniformity rules. | ||
|
|
||
| Control-flow is said to be uniform at a point in the program if all invocations execute in lockstep at that program point. | ||
| An expression is said to be uniform if it is in uniform control-flow, and all invocations compute the same value for it. |
There was a problem hiding this comment.
This is stronger than I'm used to: I'm used to "all invocations compute the same value for it".
But maybe it amounts to the same thing?
Is there extra conservatism added because of requiring it to be in uniform control flow. Is it for simplicity of analysis/ diagnostics?
There was a problem hiding this comment.
It makes sense to me. How would you say "all invocations" without implying the uniform control flow?
There was a problem hiding this comment.
I think the only reason we need to define whether an expression is "uniform" is so we can place limits on what operations are in it. For this reason, we only care if it is in uniform control flow. These expressions might produce different results and still be in uniform control flow, that doesn't mean you can't use the uniform control limited operations in such expressions.
wgsl/index.bs
Outdated
|
|
||
| ### Uniformity analysis overview ### {#uniformity-overview} | ||
|
|
||
| In this section we specify an analysis that verifies that functions which are only safe to call in uniform control flow (barriers and derivatives) are only called in such a context. |
There was a problem hiding this comment.
I would call out that it's a static analysis: it is done only by inspecting the program source.
wgsl/index.bs
Outdated
| The first phase walks over the AST of the function, building a directed graph along the way. | ||
| The second phase explores that graph, resulting in either rejecting the program, or computing the constraints on calling this function. | ||
|
|
||
| Note: Each vertex in this graph corresponds either to the statement "The control-flow at a specific program point must be uniform" or to "The value of a specific expression must be uniform" or to "A specific variable must always hold a uniform value" or is one of the special vertices True and False. |
There was a problem hiding this comment.
Which agent has the obligation for the "must"? The analysis, the programmer, or the runtime behaviour of the implementation?
Is it provability?
The control flow at this point in the program is proven to be uniform?
Or
Semantics of the language require that this statement be executed in uniform control flow. (e.g. this is a control barrier)
wgsl/index.bs
Outdated
|
|
||
| In more detail, here is the algorithm for analyzing a function: | ||
|
|
||
| * Create some vertices called "True", "False", "CF_start", "CF_return" and if the function is non-void a vertex called "Value_return" |
There was a problem hiding this comment.
What is the intuition behind True and False.
Is it
- True = control flow is known to be uniform at this point
- False = control flow is not known to be uniform at this point.
That highlights the one-sided error of the analysis, which is important to make clear.
There was a problem hiding this comment.
Yes. I originally went for "mustBeUniform" "cannotBeGuaranteedToBeUniform", but found it too verbose, and think that True/False makes it clearer why edges can be interpreted as edges.
wgsl/index.bs
Outdated
| The second phase explores that graph, resulting in either rejecting the program, or computing the constraints on calling this function. | ||
|
|
||
| Note: Each vertex in this graph corresponds either to the statement "The control-flow at a specific program point must be uniform" or to "The value of a specific expression must be uniform" or to "A specific variable must always hold a uniform value" or is one of the special vertices True and False. | ||
| Each (directed) edge corresponds to an implication. |
There was a problem hiding this comment.
+1 to describing more what implication is supposed to capture.
kvark
left a comment
There was a problem hiding this comment.
Haven't finished my review yet, will do another pass after the first iteration.
wgsl/index.bs
Outdated
| The analysis is what actually defines these concepts, and when a program is valid or break the uniformity rules. | ||
|
|
||
| Control-flow is said to be uniform at a point in the program if all invocations execute in lockstep at that program point. | ||
| An expression is said to be uniform if it is in uniform control-flow, and all invocations compute the same value for it. |
There was a problem hiding this comment.
It makes sense to me. How would you say "all invocations" without implying the uniform control flow?
wgsl/index.bs
Outdated
| The following definitions are merely informative, trying to give an intuition for what the analysis in the next subsection is computing. | ||
| The analysis is what actually defines these concepts, and when a program is valid or break the uniformity rules. | ||
|
|
||
| Control-flow is said to be uniform at a point in the program if all invocations execute in lockstep at that program point. |
There was a problem hiding this comment.
we need to define the scope of this, i.e. what subset of invocations is considered for uniformity
There was a problem hiding this comment.
I agree completely that this needs to be defined. To do so, we'll have to introduce the concept of waves/subgroups/warps/wavefronts/whatever. Which I don't think is in the spec currently.
This brings up another concern of mine. Will having one scope for "uniform" that applies to buffers and another scope for "uniform" as it applies to variables and control flow be confusing? I think so.
wgsl/index.bs
Outdated
|
|
||
| Control-flow is said to be uniform at a point in the program if all invocations execute in lockstep at that program point. | ||
| An expression is said to be uniform if it is in uniform control-flow, and all invocations compute the same value for it. | ||
| A local variable is said to be uniform, if all invocations have the same value in that variable at every program point where it is live. |
There was a problem hiding this comment.
Something must be said about what "all invocations" mean here. Is it "all" through the draw/dispatch? "all" in the current scope? Or even "all" as in all active at any point within the wave?
wgsl/index.bs
Outdated
| <thead> | ||
| <tr><th>Statement<th>New vertices<th>Recursive analyses<th>Behaviors<th>Resulting control-flow vertex<th>New edges | ||
| </thead> | ||
| <tr><td class="nowrap">*s1*; *s2* |
There was a problem hiding this comment.
I don't get this part. What statement is this in the code?
There was a problem hiding this comment.
Just a sequence. E.g. two function calls {foo(); bar();}, this simply says that you should first analyze the first statement, then the second, using the control flow at the exit of the first one as the control flow at the entrance fo the second.
There was a problem hiding this comment.
Pedantically speaking, the grammar doesn't have this in the statements. Perhaps, we should move this rule out of the table?
wgsl/index.bs
Outdated
| <tr><td class="nowrap">if (*e*) then *s1* else *s2* | ||
| <td>*CFinside*, *CFend* | ||
| <td class="nowrap">*CF*, *e* -> *CF'*, *V*, *B*<br> | ||
| *CFinside*, *s1* -> *CF1*, *B1*<br> |
There was a problem hiding this comment.
Sorry, I'm confused here again. We are defining a new vertex "CFinside", but what is it?
This command is supposed to mean "run the analysis on S starting with control-flow CF1, apply the required changes to t...", so it assumes that CF1 is known and given. But here where we are using this, we aren't defining it in any way.
There was a problem hiding this comment.
CFinside is a freshly allocated node.
It is passed to the recursive analysis so that it can make edges from other nodes to it.
And from the last column you have "CFinside -> {CF', V}", which adds two edges starting from it.
In terms of implementation, a vertex/node is just an integer. So this entire rule would look something like this (in pseudo-C++ syntax):
unsigned CFinside = ++allocatedVerticesCounter;
unsigned CFend = ++allocatedVerticesCounter;
auto (CF', V, B) = analyzeExpr(CF, e);
auto (CF1, B1) = analyzeStmt(CFinside, s1);
auto (CF2, B2) = analyzeStmt(CFinside, s2);
behaviorSet bReturn = union(B, B1, B2);
edges[CFinside].push_back(CF');
edges[CFinside].push_back(V);
edges[CFend].push_back(CF1);
edges[CFend].push_back(CF2);
return (CFend, bReturn);
It is probably not perfect C++ (in particular I have not checked the syntax for creating/destructuring tuples, and we probably should expand the edges vector when we increment allocatedVerticesCounter, and we should not use an apostrophe in an identifier), but it should hopefully clarify the meaning of this table.
Did this answer your question? And if so, do you have any advice on how I should present it in the spec?
There was a problem hiding this comment.
No, I think we should have some wording here to help the reader, like "CFinside is a node corresponding to XXX", and same for all the nodes we are introducing.
There was a problem hiding this comment.
edges[CFinside].push_back(CF');
I agree this is necessary, but I can't figure out from the table where this edge comes from. I'd suggest stating it explicitly.
wgsl/index.bs
Outdated
| ### Uniformity rules for function calls ### {#uniformity-function-calls} | ||
|
|
||
| The most complex rule is for function calls: | ||
| - For each argument, apply the corresponding expression rule, with the control-flow at the exit of the previous argument (using the control-flow at the beginning of the function call for the first argument). Name the corresponding value vertices "arg_i" and the corresponding control-flow vertices "CF_i" |
There was a problem hiding this comment.
They're the nodes returned by the recursive calls to the analysis when analyzing the arguments.
They're intended to correspond to the uniformity requirement of the control-flow at the end of evaluating the corresponding argument.
|
Also, before I forget, we really need to figure out the exact scopes for uniformity (see #669 thread) before proceeding with the analysis. The design space includes some interesting options if the scope is at least the sub-group. |
|
I'm concerned about the following case: SPIR-V will not guarantee that the invocations are reconverged at the start of the subsequent loop iterations, so the barrier cannot be considered to be in uniform control flow. I don't think the analysis captures this. |
pow2clk
left a comment
There was a problem hiding this comment.
Sorry for the last minute review.
Loathe as I am to bog us down in renaming discussions, I fear that fact that "uniform" means a different scope for variables and buffers will be confusing.
I think the definitions of "uniform" for different nouns should be based on utility as I've described internally.
I think introducing the notion of a block would be helpful in simplifying the way to determine uniformity as it would allow simply saying that a variable is uniform if it is assigned in a basic block whose execution depends on uniform control flow and all its dependencies are uniform. We could do away with defining a uniform expression and just say that certain operations cannot be used within a basic block that is non-uniform.
wgsl/index.bs
Outdated
|
|
||
| Control-flow is said to be uniform at a point in the program if all invocations execute in lockstep at that program point. | ||
| An expression is said to be uniform if it is in uniform control-flow, and all invocations compute the same value for it. | ||
| A local variable is said to be uniform, if all invocations have the same value in that variable at every program point where it is live. |
There was a problem hiding this comment.
I disagree with @jdashg's comment here. The way I use the term, a variable can be in uniform control flow and be non-uniform because it has different values across the scope of the uniformity. I would argue this definition because it is useful. If a control flow construct depends on a non-uniform variable under my definition, the control flow will be non-uniform. To express that in a definition where uniformity of a variable depends on the uniformity of its control flow, we would have to redefine a characteristic of variables to plug into that statement.
Furthermore, the uniformity of a uniform buffer concerns the persistence of its values across a broad scope. The same should apply to defining a variable as uniform.
wgsl/index.bs
Outdated
| The following definitions are merely informative, trying to give an intuition for what the analysis in the next subsection is computing. | ||
| The analysis is what actually defines these concepts, and when a program is valid or break the uniformity rules. | ||
|
|
||
| Control-flow is said to be uniform at a point in the program if all invocations execute in lockstep at that program point. |
There was a problem hiding this comment.
I agree completely that this needs to be defined. To do so, we'll have to introduce the concept of waves/subgroups/warps/wavefronts/whatever. Which I don't think is in the spec currently.
This brings up another concern of mine. Will having one scope for "uniform" that applies to buffers and another scope for "uniform" as it applies to variables and control flow be confusing? I think so.
wgsl/index.bs
Outdated
| The following definitions are merely informative, trying to give an intuition for what the analysis in the next subsection is computing. | ||
| The analysis is what actually defines these concepts, and when a program is valid or break the uniformity rules. | ||
|
|
||
| Control-flow is said to be uniform at a point in the program if all invocations execute in lockstep at that program point. |
There was a problem hiding this comment.
For purposes of utility, I would define uniform control flow as being control flow that depends on values certain to be the same over the defined scope of uniformity. The programmer doesn't care if they are executed in lockstep or not. It's not strictly relevant to the restrictions this introduces. Rather than having to define how hardware works, let's define how you can write a program that trips it up.
wgsl/index.bs
Outdated
| The analysis is what actually defines these concepts, and when a program is valid or break the uniformity rules. | ||
|
|
||
| Control-flow is said to be uniform at a point in the program if all invocations execute in lockstep at that program point. | ||
| An expression is said to be uniform if it is in uniform control-flow, and all invocations compute the same value for it. |
There was a problem hiding this comment.
I think the only reason we need to define whether an expression is "uniform" is so we can place limits on what operations are in it. For this reason, we only care if it is in uniform control flow. These expressions might produce different results and still be in uniform control flow, that doesn't mean you can't use the uniform control limited operations in such expressions.
wgsl/index.bs
Outdated
| ### Uniformity rules for statements ### {#uniformity-statements} | ||
|
|
||
| ### Divergence and reconvergence TODO ### {#divergence-reconvergence} | ||
| In order to properly deal with reconvergence we must know when there is a single way for control to flow out of a statement. |
There was a problem hiding this comment.
This is the first mention of convergence or reconvergence. As I understand it, it is a synonym for uniformity and nonuniformity, but it's not explicitly said here. I happen to think that these terms are preferable due to the different meaning of uniform buffers, but regardless, they should either be defined or not used.
There was a problem hiding this comment.
oh this is going to be fun to specify! SPIR-V uses the notion of quad uniformity, while reconverting only at the subgroup scale.
There was a problem hiding this comment.
Point taken about terminology! We can bikeshed when the text comes into better focus. (We can use mangoes, bananas, grapes as needed.)
|
Sorry for not having answered all of these helpful comments more quickly. I am in the middle of it, I just wanted to rapidly answer the last one:
The analysis does capture this example (it was one the main examples that I used while designing the analysis!) are {Continue, Nothing}. Since this set has more than one element, there is no automatic reconvergence. In particular, the control-flow at the end of this statement (CFend in the if rule) has a path (indirectly) to non-uniform-expr. |
RobinMorisset
left a comment
There was a problem hiding this comment.
I'll answer the rest of the comments in the next hour or two.
wgsl/index.bs
Outdated
|
|
||
| At the same time, it computies metadata about the function to help analyze its callers in turn. This means that the call graph must first be built, and functions must be analyzed from the leaves upwards, i.e. from functions that call no function outside the standard library toward the entry point. This way, whenever a function is analyzed, the metadata for all of its callees has already been computed. There is no risk of being trapped in a cycle, as recurrence is forbidden in the language. | ||
|
|
||
| Note: another way of saying the same thing is that we do a topological sort of functions ordered by the "is a (possibly indirect) callee of" partial order, and analyze in that order. |
There was a problem hiding this comment.
We are debating allowing functions to be declared in any order. You are correct that if we do not allow calling a function before it is defined, then we can just use the program order.
wgsl/index.bs
Outdated
|
|
||
| In more detail, here is the algorithm for analyzing a function: | ||
|
|
||
| * Create some vertices called "True", "False", "CF_start", "CF_return" and if the function is non-void a vertex called "Value_return" |
There was a problem hiding this comment.
Yes. I originally went for "mustBeUniform" "cannotBeGuaranteedToBeUniform", but found it too verbose, and think that True/False makes it clearer why edges can be interpreted as edges.
wgsl/index.bs
Outdated
| ### Uniformity rules for statements ### {#uniformity-statements} | ||
|
|
||
| In order to properly deal with reconvergence we must know when there is a single way for control to flow out of a statement. | ||
| We achieve this by assigning to each statement a set of *behaviors* out of {Return, Break, Continue, Discard, Nothing}. |
There was a problem hiding this comment.
Nothing means that control can flow to the next statement.
I wanted to call it Fallthrough, but it conflicted with the actual Fallthrough statement. Maybe "Default" or "NextStatement" might be clearer?
The empty set would mean that control flow is trapped forever in the statement.
wgsl/index.bs
Outdated
| ### Uniformity restrictions TODO ### {#uniformity-restrictions} | ||
| TODO: do we need an extra behavior for possibly infinite loops? I should check whether some form of reconvergence happens after something like `while(tid mod 2) {}`. | ||
|
|
||
| If the set of behaviors for a statement has a single element, this means that we either did not diverge within the statement, or we reconverged, and we pick the vertex corresponding to control-flow at the start of the statement as the vertex corresponding to control-flow at the exit of the statement, overriding the other rules below. |
There was a problem hiding this comment.
Ok, I'll try to move this to after the table.
wgsl/index.bs
Outdated
| <thead> | ||
| <tr><th>Statement<th>New vertices<th>Recursive analyses<th>Behaviors<th>Resulting control-flow vertex<th>New edges | ||
| </thead> | ||
| <tr><td class="nowrap">*s1*; *s2* |
There was a problem hiding this comment.
Just a sequence. E.g. two function calls {foo(); bar();}, this simply says that you should first analyze the first statement, then the second, using the control flow at the exit of the first one as the control flow at the entrance fo the second.
wgsl/index.bs
Outdated
| <td>*B1* \ {Nothing} U *B2* | ||
| <td>*CF2* | ||
| <td> | ||
| <tr><td class="nowrap">if (*e*) then *s1* else *s2* |
WGSL meeting minutes 2021-04-06
|
wgsl/index.bs
Outdated
|
|
||
| At the same time, it computies metadata about the function to help analyze its callers in turn. This means that the call graph must first be built, and functions must be analyzed from the leaves upwards, i.e. from functions that call no function outside the standard library toward the entry point. This way, whenever a function is analyzed, the metadata for all of its callees has already been computed. There is no risk of being trapped in a cycle, as recurrence is forbidden in the language. | ||
|
|
||
| Note: another way of saying the same thing is that we do a topological sort of functions ordered by the "is a (possibly indirect) callee of" partial order, and analyze in that order. |
There was a problem hiding this comment.
IIRC, the current consensus is to have all the top-level declarations ordered.
We can therefore describe this as just analyzing the functions in the order they are written, and add a TODO to reconsider a topological sort once the ordering is relaxed (after MVP, if ever).
wgsl/index.bs
Outdated
| ### Uniformity rules for statements ### {#uniformity-statements} | ||
|
|
||
| ### Divergence and reconvergence TODO ### {#divergence-reconvergence} | ||
| In order to properly deal with reconvergence we must know when there is a single way for control to flow out of a statement. |
There was a problem hiding this comment.
oh this is going to be fun to specify! SPIR-V uses the notion of quad uniformity, while reconverting only at the subgroup scale.
wgsl/index.bs
Outdated
| <thead> | ||
| <tr><th>Statement<th>New vertices<th>Recursive analyses<th>Behaviors<th>Resulting control-flow vertex<th>New edges | ||
| </thead> | ||
| <tr><td class="nowrap">*s1*; *s2* |
There was a problem hiding this comment.
Pedantically speaking, the grammar doesn't have this in the statements. Perhaps, we should move this rule out of the table?
wgsl/index.bs
Outdated
| <tr><td class="nowrap">if (*e*) then *s1* else *s2* | ||
| <td>*CFinside*, *CFend* | ||
| <td class="nowrap">*CF*, *e* -> *CF'*, *V*, *B*<br> | ||
| *CFinside*, *s1* -> *CF1*, *B1*<br> |
There was a problem hiding this comment.
No, I think we should have some wording here to help the reader, like "CFinside is a node corresponding to XXX", and same for all the nodes we are introducing.
WGSL meeting minutes 2021-04-20
|
|
@alan-baker, @dneto0. I think I've changed everything that we discussed in today's call and in #1770; so it should be ready for you to review. Things that we decided to delay to some separate PRs:
|
alan-baker
left a comment
There was a problem hiding this comment.
Very close now, just some minor text update from my point of view. Thanks!
wgsl/index.bs
Outdated
| <td> | ||
| <td class="nowrap">*CF*, *CF* | ||
| <td> | ||
| <tr><td class="nowrap">reference to function-scope variable or parameter "x" |
There was a problem hiding this comment.
The spec distinguishes between variables and let-declarations, so to be precise we should name let-declarations here.
| <tr><td class="nowrap">reference to function-scope variable or parameter "x" | |
| <tr><td class="nowrap">reference to function-scope variable, let-declaration or formal parameter "x" |
Separately are these rules priority based? This rule conflicts with builtin value rules below since they may be function parameters (or members of a struct function parameter).
wgsl/index.bs
Outdated
| <td class="nowrap">*X* is the node corresponding to "x" | ||
| <td class="nowrap">*CF*, *Result* | ||
| <td class="nowrap">*Result* -> {*CF*, *X*} | ||
| <tr><td class="nowrap">reference to uniform built-in variable "x" |
There was a problem hiding this comment.
Nit: built-in variables are now referred to as built-in values.
wgsl/index.bs
Outdated
| <td> | ||
| <td class="nowrap">*CF*, *CF* | ||
| <td> | ||
| <tr><td class="nowrap">reference to non-uniform built-in variable "x" |
wgsl/index.bs
Outdated
| <td> | ||
| <td class="nowrap">*CF*, *CannotBeUniform* | ||
| <td> | ||
| <tr><td class="nowrap">reference to read-only global variable "x" |
There was a problem hiding this comment.
Nit: change global to module-scope
wgsl/index.bs
Outdated
| <td> | ||
| <td class="nowrap">*CF*, *CF* | ||
| <td> | ||
| <tr><td class="nowrap">reference to non-read-only global variable "x" |
…ion let-declarations
alan-baker
left a comment
There was a problem hiding this comment.
Great work, thanks for this contribution!
dneto0
left a comment
There was a problem hiding this comment.
Thanks!
I think we should land this version.
Discussing with @alan-baker and @RobinMorisset offline, we have queued up some further refinements, and reaction to decisions about what to analyze in unreachable code.
wgsl/index.bs
Outdated
| *Value_return* -> *V* | ||
| <tr><td class="nowrap">*e1* = *e2*; | ||
| <td> | ||
| <td class="nowrap">LValue: (*CF*, *e1*) => (*CF1*, *L1*)<br> |
There was a problem hiding this comment.
This has the LHS being evaluated before the RHS.
Current spec says the RHS is evaluated before the LHS.
We can fix this later.
There was a problem hiding this comment.
Ah, good catch, I kept expecting left-to-right evaluation. I'll fix it.
WGSL meeting minutes 2022-01-25
|
dneto0
left a comment
There was a problem hiding this comment.
Thanks for persevering. I think I understand it now and am happy to land this.
Thanks!
|
The merge conflict is a trivial one, based on the capitalization of the old section names. I can fix it for you if you like. |
kvark
left a comment
There was a problem hiding this comment.
I looked at it again, sorry about the wait!
I don't expect this to be perfect for landing. It's a hugely important and big piece of work that we require, and we should follow-up with patches. But I'd like to have at least the foundation of the algorithm to be solid, i.e. the terms defined and the nodes called something that's easy to understand. This way, we'll be in a better position to polish it.
So noted a few things. Please feel free to respond "let's deal with it later" on any of them!
Here are some high-level thoughts. While this algorithm is required, I have a sense that describing it in this form may be suboptimal. It's very hard to understand by reading it. For me, it's more or less a number of magic rules that I have to basically reinterpret in my head when reading. And this is complicated by the fact it's written in pseudo-code, so the rules of interpretation are also interpreted from the text...
So this reminds me of an article I made some time ago. What would be cleaner for me personally if this algorithm was written as actual code that I can run. Whatever language it is (statically typed are preferred, of course).
But I understand it's too much to ask at this stage, so we should proceed.
| In this section we specify an analysis that verifies that functions which are only safe to call in uniform control flow (e.g. barriers and derivatives) are only called in such a context. | ||
|
|
||
| <div class="note">Note: This analysis has the following desirable properties: | ||
| - Sound (meaning that it rejects every program that would break the uniformity requirements of builtins) |
There was a problem hiding this comment.
are we only talking about builtins here?
Considering this program:
fn foo() -> f32 {
return myTexture.sample(mySampler, 0.0).x;
}
fn bar() {
if (non_uniform) {
foo();
}
}
My interpretation is that we are analysing foo's requirement for being called in uniform control flow, and then we can reject the program based on the call sites of foo. And this is user-defined, not a builtin.
There was a problem hiding this comment.
You are correct. I wrote builtin here, because all constraints ultimately flow from some builtin (if there was no call to sample in foo, we could call foo anywhere). I am not sure how to phrase it more clearly.
wgsl/index.bs
Outdated
|
|
||
| In more detail, here is what we compute for each function: | ||
|
|
||
| * A tag about the control-flow in which the function can be called: one of {AlwaysMustBeUniform, MustBeUniformForControlFlowAfter} |
There was a problem hiding this comment.
is there a statement missing clarifying what a "tag" is in this context?
or at least, something like "for each node in the graph representing a function, we may attach the following tags"?
wgsl/index.bs
Outdated
| In more detail, here is what we compute for each function: | ||
|
|
||
| * A tag about the control-flow in which the function can be called: one of {AlwaysMustBeUniform, MustBeUniformForControlFlowAfter} | ||
| * A tag for each parameter of the function: one of {AlwaysMustBeUniform, MustBeUniformForControlFlowAfter, MustBeUniformForReturnValue, NoRestriction} |
There was a problem hiding this comment.
It sounds like exactly one tag needs to be attached to a node representing the function parameter. Would it ever be the case where we want MustBeUniformForControlFlowAfter together with MustBeUniformForReturnValue but not as strong as AlwaysMustBeUniform?
wgsl/index.bs
Outdated
|
|
||
| Here is the algorithm for computing this data for a given function: | ||
|
|
||
| * Create nodes called "MustBeUniform", "CannotBeUniform", "CF_start", "CF_return", and if the function is non-void a node called "Value_return" |
There was a problem hiding this comment.
the language seems to be marginally inconsistent in multiple dimensions:
- here we use "Name", where some other place we just do Name. Perhaps, we should just refer to them consistently as
Nameeverywhere? - some names use camel case, others use some variation of a snake case, e.g. "Value_return". Perhaps, we could pick one and stick with it? That would mean, for example,
ControlFlowStart,ControlFlowReturn,ValueReturn, etc
wgsl/index.bs
Outdated
|
|
||
| * Create nodes called "MustBeUniform", "CannotBeUniform", "CF_start", "CF_return", and if the function is non-void a node called "Value_return" | ||
| * Create one node for each parameter of the function which we'll call "arg_i" | ||
| * Walk over the syntax of the function, adding nodes and edges to the graph following the rules of the next section, using CF_start as the starting control-flow for the function's body. |
There was a problem hiding this comment.
nit: there is a few cases where the spec says "this is described below" of sorts, like here. It would be great to have an actual link to the relevant section instead
wgsl/index.bs
Outdated
| * Create some nodes called "MustBeUniform", "CannotBeUniform", "CF_start", "CF_return" and if the function is non-void a node called "Value_return" | ||
| * Create one node for each parameter of the function which we'll call "arg_i" | ||
| * Walk over the syntax of the function, adding nodes and edges to the graph following the rules of the next section. | ||
| * Look at which nodes are reachable from "MustBeUniform" |
There was a problem hiding this comment.
I expressed a similar concern in a comment, glad this is noticed. Let's have good names here, please.
"CannotBeUniform" -> "MayBeNonUniform" or "AssumedNonUniform"
"MustBeUniformForControlFlowAfter" -> "PropagatesUniformity" or something along the lines (edit: I see @Kangz suggested a very similar thing earlier)
RobinMorisset
left a comment
There was a problem hiding this comment.
Thanks @kvark for the comments. I've tried to rephrase things where you found them unclear, please tell me whether it helps.
I am not opposed to providing a reference implementation of the analysis implemented in an executable language, quite the opposite, but I don't think I can do that in the next few days, and we'd like to get this landed in the spec soon.
| In this section we specify an analysis that verifies that functions which are only safe to call in uniform control flow (e.g. barriers and derivatives) are only called in such a context. | ||
|
|
||
| <div class="note">Note: This analysis has the following desirable properties: | ||
| - Sound (meaning that it rejects every program that would break the uniformity requirements of builtins) |
There was a problem hiding this comment.
You are correct. I wrote builtin here, because all constraints ultimately flow from some builtin (if there was no call to sample in foo, we could call foo anywhere). I am not sure how to phrase it more clearly.
WGSL meeting minutes 2022-02-01
|
kvark
left a comment
There was a problem hiding this comment.
Thank you for patience in addressing all of those!
…ception (gpuweb#1571) This PR updates the existing createQuerySet test of query_types.spec.ts in order to expect exceptions rather than validation errors when the device features doesn't contain 'timestamp-query'.
This is spec text based on my previous uniformity analysis for WSL, you can see the previous version at:https://lists.w3.org/Archives/Public/public-gpu/2019Jul/0000.html (and the detailed version in the doc at the bottom of that email). The main difference is adding support for "discard", which somewhat complicates things, but still slots reasonably well into the overall framework.
The analysis currently does not support pointers (since it is a part of the spec that is still in flux). When we do add pointers, every read of a pointer will be considered non-uniform, and every taking of the adress of a variable will make that variable non-uniform (improvable later if it proves crippling, but it would be fairly complex).
It has a couple of other limitations:
There are still a couple of TODOs in the uniformity section of the spec:
I am very interested in getting feedback on this PR, both on the analysis itself and on its presentation. I've tried to explain it as clearly as I could, but I realize that I probably failed in quite a few places; I've just spent long enough working on this that I don't know which parts are hard to understand.
Preview | Diff