-
Notifications
You must be signed in to change notification settings - Fork 4
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
5 WESL Proposals #20
Open
ncthbrt
wants to merge
11
commits into
wgsl-tooling-wg:main
Choose a base branch
from
ncthbrt:load-proposal
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,726
−544
Open
5 WESL Proposals #20
Changes from 7 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
20735c5
Finished initial doc for alternative import specification.
ncthbrt 9d467dd
Added module proposal
ncthbrt cebdb85
Added a draft of module interfaces. However the type checking is not …
ncthbrt 596b494
Update ModulesInterfaces.md to add section on type checking roles
ncthbrt 8b3bbd0
Updated proposal to reuse aliases for module aliases
ncthbrt 10966a2
Added Include specification
ncthbrt 2936a75
Started working on specification for generic modules
ncthbrt 849d7f8
Finished first draft of GenericModules
ncthbrt 44c2913
Delete GenericFunctions.md
ncthbrt f5565a1
Added original wgsl syntax.bnf file
ncthbrt 37ec96c
Added attributes to load statement
ncthbrt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
# Generics Modules for WGSL | ||
|
||
# Summary | ||
|
||
We propose adding a mechanism to allow generic programming using WESL modules as an extension to the language. | ||
|
||
## Assumptions | ||
|
||
Assumes that [`load`](./Imports.md), [Modules](./Modules.md) and [Module Interfaces](./ModulesInterfaces.md) have already been implemented. | ||
|
||
# Motivation | ||
|
||
Generic programming is useful for WESL, particularly for libraries. | ||
|
||
With generic modules, operations like `Reduce` or `PrefixSum` wouldn't need to be manually rewritten for each combination of element type and binary operation. | ||
|
||
Larger projects like GPU accelerated particle systems could also benefit from generic modules. This is because the | ||
encapsulation of types, functions and other globals in modules provides a means of structuring libraries in a way that is | ||
readily user extensible, reusable, and maintainable. | ||
|
||
|
||
# Guide-level explanation | ||
|
||
Generic modules and signatures are declared using the `mod` keyword and like generic types and built-in functions, use angle brackets (`<>`) to denote the generic parameters. Parameters are permitted to be other modules. | ||
|
||
An insantiation of a generic module can be declared inline when used, added to the current namespace using [`include`](./Include.md) (if implemented) or aliased to give the module a concrete name. | ||
|
||
In addition to generic modules, this proposal also requires that [module signatures](./ModulesInterfaces.md) support generic parameters. Module signatures may be used to constrain the type of a generic argument. | ||
Generic module signatures in generic type constraints may additionally use `_` as a "hole" in arguments to indicate that the user doesn't care about the type of a particular generic parameter. | ||
|
||
Below is an annotated example of how generic modules may be used in practise. This has been translated from the | ||
[StoneBerry WebGPU Repository](https://github.com/stoneberry-webgpu/) into the proposed WESL format | ||
|
||
```rescript | ||
// Module signature that simply exposes the single type T. Could perhaps later be sugared to elide the module in follow-up | ||
// work | ||
mod sig Type { | ||
type T; | ||
} | ||
|
||
// Module signature that exposes a single constant `value`. Could perhaps later be sugared to elide the module in follow-up work | ||
mod sig Const<Type: Type> { | ||
const value: Type::T; | ||
} | ||
|
||
// Abstract representation of a binary operation. | ||
mod sig BinaryOp<OpElem: Type, LoadElem: Type> { | ||
// This is a common pattern to allow transfer of type information from generic input to output module | ||
type LoadElem : LoadElem::T; | ||
type OpElem : OpElem::T; | ||
fn identityOp() -> OpElem; | ||
fn loadOp(a: LoadElem::T) -> OpElem; | ||
fn binaryOp(a: OpElem, b: OpElem) -> OpElem; | ||
} | ||
|
||
// In future, modules representing numbers, vectors, matrices and other built in types | ||
// would be part of the standard library. But lets define some common operations for now | ||
mod sig Number { | ||
type T; | ||
|
||
fn add(a: T, b: T) -> T; | ||
fn identity() -> T; | ||
} | ||
|
||
mod Sum<N: Number> { | ||
struct T { | ||
sum: N::T; | ||
} | ||
} | ||
|
||
|
||
mod SumBinaryOp<N: Number> -> BinaryOp<Sum<N>, Sum<N>> { | ||
alias OpElem = Sum<N>::T; | ||
alias LoadElem = Sum<N>::T; | ||
|
||
fn identityOp() -> OpElem { | ||
return OpElem(); | ||
} | ||
|
||
fn loadOp(a: LoadElem) -> OpElem { | ||
return OpElem(a.sum); | ||
} | ||
|
||
fn binaryOp(a: OpElem, b: OpElem) -> OpElem { | ||
return OpElem(N::add(a.sum, b.sum)); | ||
} | ||
} | ||
|
||
mod F32 { | ||
alias T = f32; | ||
|
||
fn add(a: T, b: T) -> T { | ||
return a + b; | ||
} | ||
|
||
fn identity() -> T { | ||
return 0.0; | ||
} | ||
} | ||
|
||
mod U32 { | ||
alias T = u32; | ||
} | ||
|
||
// Here we don't care about the exact generic mod values | ||
// passed to BinaryOp as we can extract the underlying types from the module | ||
// members | ||
mod ReduceWorkgroup<Op: BinaryOp<_, _>, WorkSize: Const<U32>, Threads: Const<U32>> { | ||
var <workgroup> work: array<Op::OpElem::T, WorkSize::value>; | ||
fn reduceWorkgroup(localId: u32) { | ||
let workDex = localId << 1u; | ||
for (var step = 1u; step < Threads::value; step <<= 1u) { | ||
workgroupBarrier(); | ||
if localId % step == 0u { | ||
work[workDex] = Op::binaryOp(work[workDex], work[workDex + step]); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Same here | ||
mod ReduceBuffer<Op: BinaryOp<_, _>, BlockArea: Const<U32>, WorkSize: Const<U32>, Threads: Const<U32>> { | ||
// Including brings the module members into the namespace | ||
include ReduceWorkgroup<Op, WorkSize, Threads>; | ||
|
||
alias Input = Op::LoadElem::T; | ||
alias Output = Op::OpElem::T; | ||
|
||
struct Uniforms { | ||
sourceOffset: u32, // offset in Input elements to start reading in the source | ||
resultOffset: u32, // offset in Output elements to start writing in the results | ||
} | ||
|
||
@group(0) @binding(0) var<uniform> u: Uniforms; | ||
@group(0) @binding(1) var<storage, read> src: array<Input>; | ||
@group(0) @binding(2) var<storage, read_write> out: array<Output>; | ||
@group(0) @binding(11) var<storage, read_write> debug: array<f32>; // buffer to hold debug values | ||
|
||
override workgroupThreads = 4u; | ||
|
||
var <workgroup> work: array<Output, workgroupThreads>; | ||
|
||
// reduce a buffer of values to a single value, returned as the last element of the out array | ||
// | ||
// each dispatch does two reductions: | ||
// . each invocation reduces from a src buffer to the workgroup buffer | ||
// . one invocation per workgroup reduces from the workgroup buffer to the out buffer | ||
// the driver issues multiple dispatches until the output is 1 element long | ||
// (subsequent passes uses the output of the previous pass as the src) | ||
// the same output buffer can be used as input and output in subsequent passes | ||
// . start and end indices in the uniforms indicate input and output positions in the buffer | ||
// | ||
@compute | ||
@workgroup_size(workgroupThreads, 1, 1) | ||
fn main( | ||
@builtin(global_invocation_id) grid: vec3<u32>, // coords in the global compute grid | ||
@builtin(local_invocation_index) localIndex: u32, // index inside the this workgroup | ||
@builtin(num_workgroups) numWorkgroups: vec3<u32>, // number of workgroups in this dispatch | ||
@builtin(workgroup_id) workgroupId: vec3<u32> // workgroup id in the dispatch | ||
) { | ||
reduceBufferToWork(grid.xy, localIndex); | ||
let outDex = workgroupId.x + u.resultOffset; | ||
reduceWorkgroup(localIndex); | ||
if localIndex == 0u { | ||
out[outDex] = work[0]; | ||
} | ||
} | ||
|
||
fn reduceBufferToWork(grid: vec2<u32>, localId: u32) { | ||
var values = fetchSrcBuffer(grid.x); | ||
var v = reduceSrcBlock(values); | ||
work[localId] = v; | ||
} | ||
|
||
fn fetchSrcBuffer(gridX: u32) -> array<Output, BlockArea::value> { | ||
let start = u.sourceOffset + (gridX * BlockArea::value); | ||
let end = arrayLength(&src); | ||
var a = array<Output, BlockArea::value>(); | ||
for (var i = 0u; i < BlockArea::value; i = i + 1u) { | ||
var idx = i + start; | ||
if idx < end { | ||
a[i] = Op::loadOp(src[idx]); | ||
} else { | ||
a[i] = Op::identityOp(); | ||
} | ||
} | ||
return a; | ||
} | ||
|
||
fn reduceSrcBlock(a: array<Output, BlockArea::value>) -> Output { | ||
var v = a[0]; | ||
for (var i = 1u; i < BlockArea::value; i = i + 1u) { | ||
v = Op::binaryOp(v, a[i]); | ||
} | ||
return v; | ||
} | ||
} | ||
// To actually realize a concrete ReduceBuffer module, we need concrete const values: | ||
|
||
mod BlockArea -> Const<U32> { | ||
const value: u32 = 4u; | ||
} | ||
|
||
mod WorkSize -> Const<U32> { | ||
const value: u32 = 18u; | ||
} | ||
|
||
mod Threads -> Const<U32> { | ||
const value: u32 = 10u; | ||
} | ||
|
||
// Putting everything together and into the global namespace | ||
include ReduceBuffer<SumBinaryOp<F32>, BlockArea, WorkSize, Threads>; | ||
``` | ||
|
||
|
||
# Reference-level explanation |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Do you think generics w/o type bounds are a useful intermediate step?
I'm not certain, but I'd been hoping so to make things easier.
(w/o type constraints generics would be more about getting the flexibility to replace the string templating that current wgsl users do. But that weaker form wouldn't add type safety, so some errors would be caught later perhaps by the dawn/wgpu parser)
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.
It might be but would entail a breaking change once support for constraints were added.
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 necessarily a breaking change? I'd think simple substitutions e.g. f32 for LoadElem would be sound even when better typechecking comes in later. Inference would reduce boilerplate, but not render the boilerplate unsound.. hmm.. I bet there's a case I'm missing, can you explain?
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.
@mighdoll The problem is that this design does not allow you to access any members of the module were the generic constraint omitted (which is why it's not permitted to omit the constraint in the grammar in the first place). So if you added typechecking after the fact, you'd get a lot of errors.
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 because this design for simplicity does not use type inference. Only checking.
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.
Let me restate to see if I understand:
. if we were to release an edition with a generics-light (no contraints, no typechecking, no inference)
. it's perhaps possible to add typechecking in a later edition in principle, but
. if the next stage is generics-checked (constraints, typechecking, no inference)
. generics-checked will want to require explicit constraints for every type parameter (because it doesn't have inference)
. so best not to allow unconstrained type parameters, lest generics-light code become incompatible with generics-checked
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.
As mentioned on discord, there is one way to preserve compatibility in this design which is to add the concept of an "any" module type, which much like in typescript would mean that module arguments that do not specify type constraints would be unchecked