Recent pich and corresponding proposal mentions that one of future enhancements might include yield-once functions as currently functions do not compose well with coroutine accessors.
It seems that we really need this feature to enable automatic differentiation of coroutine accessors.
First of all, let me describe why functions do not compose well with coroutines. In fact, the situation is backwards: coroutines do not exist in Swift AST at all. They are generated directly into SIL from coroutines accessors (_modify
and _read
). As a result in the following code snippet
struct S {
private var _x : Float
var x: Float {
get{_x}
_modify { yield &_x }
}
}
The AST type of S.x._modify
is essentially (S) -> () -> ()
.
What does this mean? Well, essentially coroutines are not first-class objects as one cannot use them where proper AST type is required: they cannot be returned from functions, they cannot be stored into tuples, they cannot be used as enum associated values. Plus lots of other things Also, when some kind of AST type is required currently the compiler
has lots of special code for accessors.
Why do we need them for autodiff then? The automatic differentiation is essentially based on 3 components:
Differentiable
protocol & corresponding conformances- Compiler transformations
- All kinds of custom derivatives available from
_Differentiable
module (or provided by a user).
In particular, we'd like to be able to synthesize derivatives for functions that might use such standard library collections as Array
or Dictionary
. And here we might easily get calls to coroutine accessors as, for example, Array.subscript._modify
could be called even for code as simple as a[0] *= x
or even a[0] = x
.
As I already mentioned above, the autodiff relies on the possibility to register custom derivatives for the operations in order for compiler transforms to work. However here we are seeing some problems.
The reverse-mode derivative for a function normally returns a pair of function result and a so-called pullback function ("derivative" itself). However, as Array.subscript._modify
is a coroutine, then its reverse-mode derivative should also be a coroutine to yield the corresponding value. Even more, it should return (not yield) the pullback that itself must be a coroutine. As one can imagine, currently there is no way to define coroutines rather outside of coroutine accessor context and this certainly demands some possible compiler extensions.
Before we proceed, I must note two things:
- Certainly, there is a way to workaround the inability to define derivatives for coroutine accessors. See e.g. [SR-14113] Support `_read` and `_modify` accessor differentiation · Issue #54401 · swiftlang/swift · GitHub, however this imposes severe performance complications and also requires switching from using subscripts to other functions.
- We only need to define coroutines. We do not need to call them. The compiler would synthesize the calls for them in autodiff transformations. This simplifies lots of things as we do not need to think about possible ownership complications across coroutines boundaries from Swift language perspective.
I've been working on coroutine AST support and corresponding autodiff bits over some time recently and will soon submit a PR with proof-of-concept implementation. As this feature is mostly intended to be used inside standard library I decided to make some simplifications that could be refined further if needed:
- The coroutines are introduced via special
@yield_once
attribute. We already had it, but it was SIL-only. So, it was "promoted" to common attribute. It is now also possible to use this attribute on types to declare coroutine function types. - For SIL functions we are having yields separately from results and parameters. We do not have such luxury on AST level and I decided not to introduce one in order not to complicate the existing code. Instead, the yields are represented as
yield results
via special@yields
attribute (again, we already having it on SIL level, so there is nothing new, the attribute was just promoted). Yields then are represented as specialYieldResultType
AST node, so these could be represented in the compiler type system and handled accordingly. The node also carries if we are yielding value or address (so if we're having value or inout yield). - I would expect that normally functions would have either normal results or yields, but the cases when we'd need both yields and normal results would be extremely rare. Probably non-existent practically outside of autodiff context where we'd need to return pullback closure from the reverse-mode derivative. Still this corner case is supported as well, the function is supposed to "return" a tuple containing both
@yield
result and normal result.
Some internals & known issues worth mentioned:
- Depending on the situation we might need to deal with yields either as parameters (e.g. when need to be reabstracted as parameters in the context of a caller), or results, or separately. Some additional helpers were added (e.g. get only result type, or "full result" including yields or just yields, etc.)
- Some special cases for coroutine accessors were removed. More to follow
- We run out of
ExtBits
. For now I just take single bit that was required to mark coroutines from the# of arguments
bitfield, so essentially maximum number of function arguments is reduced from 64k down to 32k. This likely should be resolved one way or another - All coroutine accessors now obtained AST types. I hope that this will not affect any ABI-related things, but it is certainly possible that I missed some important cases.
To conclude, here is an example of reverse-mode derivative for Array.subscript._modify
that showcases the functionality:
extension Array where Element: Differentiable {
@inlinable
@derivative(of: subscript._modify)
@yield_once
mutating func _vjpModify(index: Int) -> (
value: inout @yields Element, pullback: @yield_once (inout TangentVector) -> inout @yields Element.TangentVector
) {
yield &self[index]
@yield_once
func pullback(_ v: inout TangentVector) -> inout @yields Element.TangentVector {
yield &v[index]
}
return pullback
}
}
This certainly is not intended for end-user consumption (mostly for standard library) and does not constitute a proper language feature. But hopefully it would provide some basic functionality that could eventually end with something usable :)