Updated specification #8

Merged
pca006132 merged 5 commits from updated into master 2021-06-07 10:06:52 +08:00

This gathers discussion from the previous issues.

  • Updated details related to class instance variables.
  • Rewrote section about referencing host variables from kernel.
  • Updated limitation regarding default parameter, function pointers and integer type.
  • Removed section about dynamic dispatch, maybe deal with this later.
  • Added section about kernel only class, although not discussed anywhere.
  • Added a blank section about lifetime.
This gathers discussion from the previous issues. - Updated details related to class instance variables. - Rewrote section about referencing host variables from kernel. - Updated limitation regarding default parameter, function pointers and integer type. - Removed section about dynamic dispatch, maybe deal with this later. - Added section about kernel only class, although not discussed anywhere. - Added a blank section about lifetime.
pca006132 added 2 commits 2021-04-11 23:41:08 +08:00
d82edd2753 updated according to discussions
- Updated details related to class instance variables.
- Rewrote section about referencing host variables from kernel.
- Updated limitation regarding default parameter, function pointers and
  integer type.
- Removed section about dynamic dispatch, maybe deal with this later.

@lriesebos @sb10q: Can you have a look at this?

@LPTK: Please click the 'View File' button in the 'Files Changed' tab.

@lriesebos @sb10q: Can you have a look at this? @LPTK: Please click the 'View File' button in the 'Files Changed' tab.
lriesebos reviewed 2021-04-12 04:46:28 +08:00
lriesebos left a comment

@pca006132 , I added a few questions.

@pca006132 , I added a few questions.
@ -12,2 +8,2 @@
calling the interpreter many times during compilation if there are many
references to host variables)
## Referencing Host Variables from Kernel
Host variable to be accessed must be declared as `global` in the kernel

I am still confused what the idea is about the use of 'global'. Is this what you mean?

FOO = 0

@kernel
def correct() -> int:
    global FOO
    return FOO + 1
    
@kernel
def fail_without_global() -> int:
    return FOO + 2
    
@kernel
def fail_write() -> None:
    FOO += 1
I am still confused what the idea is about the use of 'global'. Is this what you mean? ```python FOO = 0 @kernel def correct() -> int: global FOO return FOO + 1 @kernel def fail_without_global() -> int: return FOO + 2 @kernel def fail_write() -> None: FOO += 1 ```

yes

yes
pca006132 marked this conversation as resolved
README.md Outdated
@ -30,2 +29,4 @@
self.b = b
```
* Three types of instance variables: (Issue #5)
* Host only variables: Do not add type annotation for it in the class.

Would it still be possible to type host-only variables and class variables?

class Foo:
    a: int
    c: ClassVar[int]  # What will this mean?
    
    def __init__(self):
        self.a = 1  # Normal / kernel-only variable
        self.b: str = 'bar'  # Host-only variable, but still typed. Is this possible?
Would it still be possible to type host-only variables and class variables? ```python class Foo: a: int c: ClassVar[int] # What will this mean? def __init__(self): self.a = 1 # Normal / kernel-only variable self.b: str = 'bar' # Host-only variable, but still typed. Is this possible? ```

I did not think about that, maybe we can add some new syntax similar to ClassVar for host only variables?

I did not think about that, maybe we can add some new syntax similar to ClassVar for host only variables?

Sounds a bit messy and would perhaps negate the advantages of host type annotations (e.g. third-party tools may not understand ClassVar and complain that the type annotation is incorrect).
I propose not supporting host type annotations in kernel classes; anything that is annotated goes on the device.

Sounds a bit messy and would perhaps negate the advantages of host type annotations (e.g. third-party tools may not understand ``ClassVar`` and complain that the type annotation is incorrect). I propose not supporting host type annotations in kernel classes; anything that is annotated goes on the device.

We type practically everything in our DAX library, and I think it would be good to still allow typing of host code. Maybe you like one of these two ideas:

Allow host-only variable typing in methods or by using Immutable[]

class Foo:
    a: int  # Kernel-only variable
    c: Immutable[ClassVar[int]]  # Still allows host-variable typing
    
    def __init__(self):
        self.a = 1  # Normal / kernel-only variable
        self.b: str = 'bar'  # Host-only variable typed in method

Make kernel-only variable typing special using Kernel[] and be fully compatible with regular Python typing

class Foo:
    a: Kernel[int]  # Use Kernel[] to type kernel-only variables
    c: ClassVar[int]  # Host-only typing remains vanilla
    d: str  # Also host-only
    
    def __init__(self):
        self.a = 1  # Normal / kernel-only variable
        self.b: str = 'bar'  # Host-only variable
We type practically everything in [our DAX library](https://gitlab.com/duke-artiq/dax), and I think it would be good to still allow typing of host code. Maybe you like one of these two ideas: **Allow host-only variable typing in methods or by using `Immutable[]`** ```python class Foo: a: int # Kernel-only variable c: Immutable[ClassVar[int]] # Still allows host-variable typing def __init__(self): self.a = 1 # Normal / kernel-only variable self.b: str = 'bar' # Host-only variable typed in method ``` **Make kernel-only variable typing special using `Kernel[]` and be fully compatible with regular Python typing** ```python class Foo: a: Kernel[int] # Use Kernel[] to type kernel-only variables c: ClassVar[int] # Host-only typing remains vanilla d: str # Also host-only def __init__(self): self.a = 1 # Normal / kernel-only variable self.b: str = 'bar' # Host-only variable ```

Make kernel-only variable typing special using Kernel[] and be fully compatible with regular Python typing

That option would be ok, but might need better names than Kernel (and Immutable).

> Make kernel-only variable typing special using Kernel[] and be fully compatible with regular Python typing That option would be ok, but might need better names than `Kernel` (and `Immutable`).

Maybe Kernel[] / KernelImmutable[] since the two annotations are similar. And we would use "kernel" terminology in nac3core.

Maybe `Kernel[]` / `KernelImmutable[]` since the two annotations are similar. And we would use "kernel" terminology in nac3core.

Yes, I think Kernel[] and KernelImmutable[] is great and I like the fact that it is explicit!

Yes, I think `Kernel[]` and `KernelImmutable[]` is great and I like the fact that it is explicit!

Yes, it is fine for me. Did you think about the syntax I mentioned in the screenshot? Using Immutable(...) instead of Immutable[].

I'm not entirely sure if we can let IDE understand Immutable[T] == T for completion and checking, but we can do that with Immutable(T). However, one caveat is that it would be harder to let users to define the type elsewhere and use it, as the information of Immutable() would be loss. Maybe we should have a look at how typing does it for other generics.

Yes, it is fine for me. Did you think about the syntax I mentioned in the screenshot? Using `Immutable(...)` instead of `Immutable[]`. I'm not entirely sure if we can let IDE understand `Immutable[T] == T` for completion and checking, but we can do that with `Immutable(T)`. However, one caveat is that it would be harder to let users to define the type elsewhere and use it, as the information of `Immutable()` would be loss. Maybe we should have a look at how typing does it for other generics.

Tbh, I do not think the IDE autocomplete feature should be leading this choice. And besides, IDE's understand that ClassVar[T] == T, so it would not surprise me if we can also make that happen for Kernel[] and KernelImmutable[]. Brackets are the syntax used for typing in Python, so I would prefer we stick with that.

To further motivate my point of view, type checkers such as mypy go totally bad when using parenthesis. See for example this error message for TArray() (ARTIQ 6):

dax/modules/rtio_benchmark.py:206: error: Invalid type comment or annotation  [valid-type]
dax/modules/rtio_benchmark.py:206: note: Suggestion: use TArray[...] instead of TArray(...)
Tbh, I do not think the IDE autocomplete feature should be leading this choice. And besides, IDE's understand that `ClassVar[T] == T`, so it would not surprise me if we can also make that happen for `Kernel[]` and `KernelImmutable[]`. Brackets are the syntax used for typing in Python, so I would prefer we stick with that. To further motivate my point of view, type checkers such as mypy go totally bad when using parenthesis. See for example this error message for `TArray()` (ARTIQ 6): ``` dax/modules/rtio_benchmark.py:206: error: Invalid type comment or annotation [valid-type] dax/modules/rtio_benchmark.py:206: note: Suggestion: use TArray[...] instead of TArray(...) ```

@pca006132 @sb10q did we now decide on the Kernel[] and KernelImmutable[] annotations? It is not contained in this merge request.

@pca006132 @sb10q did we now decide on the `Kernel[]` and `KernelImmutable[]` annotations? It is not contained in this merge request.

Sorry I forgot this one, this is two months ago :(.

I think Kernel is used for kernel only attributes, and KernelImmutable is for kernel invariants right? Can KernelImmutable be changed by the host? I think it is OK, or we can open another issue If we want further discussion about this.

Sorry I forgot this one, this is two months ago :(. I think `Kernel` is used for kernel only attributes, and `KernelImmutable` is for kernel invariants right? Can `KernelImmutable` be changed by the host? I think it is OK, or we can open another issue If we want further discussion about this.

As per the discussion in #5, an attribute with KernelImmutable can be modified by the host, but only when no kernel is running. In particular, it cannot be modified inside a RPC call, and this would be enforced in the Python interpreter via the @kernel decorator on the class.

As per the discussion in https://git.m-labs.hk/M-Labs/nac3-spec/issues/5, an attribute with ``KernelImmutable`` can be modified by the host, but only when no kernel is running. In particular, it cannot be modified inside a RPC call, and this would be enforced in the Python interpreter via the ``@kernel`` decorator on the class.
README.md Outdated
@ -32,0 +31,4 @@
* Three types of instance variables: (Issue #5)
* Host only variables: Do not add type annotation for it in the class.
* Kernel Invariants: Immutable in the kernel and in the host while the kernel
is executing. Type: `KernelInvariant(T)`. The types must be immutable.

Should we use brackets instead, just like used for all other typing? KernelInvariant[T]

Should we use brackets instead, just like used for all other typing? `KernelInvariant[T]`

I think so.

I think so.

Yes, I would fix this later.

Yes, I would fix this later.
pca006132 marked this conversation as resolved
README.md Outdated
@ -48,2 +46,2 @@
* Should we support function pointers? What about subtyping with function
pointers, and generic types?
* Function default parameters must be immutable.
* Function pointers are supported, and lambda expression is not supported

How do we expect to type function pointers, using the typing library? e.g. Call[[int32, int32], int32]

How do we expect to type function pointers, using the typing library? e.g. `Call[[int32, int32], int32]`

Yes

Yes
pca006132 marked this conversation as resolved
README.md Outdated
@ -90,0 +93,4 @@
* Functions are all kernel only, including constructor.
Questions:
* Should we also do `@portable`?

Does this just refer to kernel-only classes or is this a general question?

  • In general, @portable is a decorator we use very often, so I would like to keep that.
  • For kernel-only classes, portable functions sound contradicting
  • For portable classes (if existing), that might be useful but I do not have a use case for that right now.
Does this just refer to kernel-only classes or is this a general question? - In general, `@portable` is a decorator we use very often, so I would like to keep that. - For kernel-only classes, portable functions sound contradicting - For portable classes (if existing), that might be useful but I do not have a use case for that right now.

Yes, we definitely want @portable.

Yes, we definitely want ``@portable``.

Portable would be for testing the code in the host, similar to the use case of portable functions I guess.

Portable would be for testing the code in the host, similar to the use case of portable functions I guess.

Not just testing. One use case is converting between SI units (e.g. a floating point number in volts) and chip control words (e.g. integer values to program into a DAC). This allows 1. caching the latter and 2. determination of quantization errors.

Not just testing. One use case is converting between SI units (e.g. a floating point number in volts) and chip control words (e.g. integer values to program into a DAC). This allows 1. caching the latter and 2. determination of quantization errors.

Perhaps we should give it another name? Kernel only seems a poor naming choice, I chose it previously because the object is created and destroyed in the kernel, the host has no access to it. Actually this is just an ordinary object.

Or maybe we should call the object of type EnvExperiment something like experiment object? I'm not good at naming things...

Perhaps we should give it another name? Kernel only seems a poor naming choice, I chose it previously because the object is created and destroyed in the kernel, the host has no access to it. Actually this is just an ordinary object. Or maybe we should call the object of type `EnvExperiment` something like experiment object? I'm not good at naming things...

Kernel-only sounds fine to me.

EnvExperiment is not specific to the core device (you can perfectly create an ARTIQ experiment with this class, that does not use the core device and the compiler at all) and I had also suggested to use a decorator instead of inheritance to have more possibilities to implement immutability.

Kernel-only sounds fine to me. `EnvExperiment` is not specific to the core device (you can perfectly create an ARTIQ experiment with this class, that does not use the core device and the compiler at all) and I had also suggested to [use a decorator instead of inheritance](https://git.m-labs.hk/M-Labs/nac3-spec/issues/5#issuecomment-2022) to have more possibilities to implement immutability.

I've updated the explanation, but still I'm not entirely sure if the name should be kernel only class... It does sound weird when we allow it to be portable...

I've updated the explanation, but still I'm not entirely sure if the name should be kernel only class... It does sound weird when we allow it to be portable...

So if I understand it correctly now, a kernel-only class is a class decorated with @kernel or @portable.

  • Both can be constructed inside kernels
  • @kernel classes can only be used in kernels
  • @portable classes can be constructed in kernels and on the host. Besides, objects can be shared between the kernel and the host

Regarding the kernel-only name, I am fine with that.

So if I understand it correctly now, a kernel-only class is a class decorated with `@kernel` or `@portable`. - Both can be constructed inside kernels - `@kernel` classes can only be used in kernels - `@portable` classes can be constructed in kernels and on the host. Besides, objects can be shared between the kernel and the host Regarding the kernel-only name, I am fine with that.

Or maybe, have a single type of "kernel-capable" class, decorated with @kernel.

Or maybe, have a single type of "kernel-capable" class, decorated with ``@kernel``.

That decorator simply registers the class with the compiler, and enforces Immutable in the interpreter. There are no restrictions on the type of methods (kernel, portable, rpc, host-only) that it contains.

That decorator simply registers the class with the compiler, and enforces ``Immutable`` in the interpreter. There are no restrictions on the type of methods (kernel, portable, rpc, host-only) that it contains.

Ok great, then I guess I misunderstood the situation when read it and what we discussed in #5 is still valid. So to summarize:

from artiq.experiment import *

@kernel  # Register class with the compiler and more
class Foo(EnvExperiment):

    def __init__(self):
        pass
        
    def run(self) -> None:  # Host-only function
        self.kernel_fn()  # Runs a kernel
        self.rpc_fn()  # Runs function on host
        self.portable_fn()  # Runs function on host
        self.host_only_fn()  # Runs function on host
            
    @kernel
    def kernel_fn(self) -> None:  # Kernel function
        self.rpc_fn()  # RPC call
        self.portable_fn()  # Portable, so remains in kernel
        
    @rpc
    def rpc_fn(self) -> None:  # RPC function
        pass
        
    @portable
    def portable_fn(self) -> None  # Portable function
        pass
        
    def host_only_fn(self) -> None  # Host-only function
        pass
Ok great, then I guess I misunderstood the situation when read it and what we discussed in #5 is still valid. So to summarize: ```python from artiq.experiment import * @kernel # Register class with the compiler and more class Foo(EnvExperiment): def __init__(self): pass def run(self) -> None: # Host-only function self.kernel_fn() # Runs a kernel self.rpc_fn() # Runs function on host self.portable_fn() # Runs function on host self.host_only_fn() # Runs function on host @kernel def kernel_fn(self) -> None: # Kernel function self.rpc_fn() # RPC call self.portable_fn() # Portable, so remains in kernel @rpc def rpc_fn(self) -> None: # RPC function pass @portable def portable_fn(self) -> None # Portable function pass def host_only_fn(self) -> None # Host-only function pass ```

So if I understand it correctly now, a kernel-only class is a class decorated with @kernel or @portable.

Both can be constructed inside kernels
@kernel classes can only be used in kernels
@portable classes can be constructed in kernels and on the host. Besides, objects can be shared between the kernel and the host

This is exactly what I mean by kernel-only class.

Or maybe, have a single type of "kernel-capable" class, decorated with @kernel.
That decorator simply registers the class with the compiler, and enforces Immutable in the interpreter. There are no restrictions on the type of methods (kernel, portable, rpc, host-only) that it contains.

This is different, I don't expect kernel-only class to contain host-only/rpc methods, and the semantic for kernel and portable functions are different. It would not be possible to call a method of a kernel object on the host if it is not portable.

I think the kernel-only object is just a simple object in our language, that can be fully-understood by the compiler, without dealing with the complicated stuff like RPC and host functions etc.

> So if I understand it correctly now, a kernel-only class is a class decorated with @kernel or @portable. > > Both can be constructed inside kernels > @kernel classes can only be used in kernels > @portable classes can be constructed in kernels and on the host. Besides, objects can be shared between the kernel and the host This is exactly what I mean by kernel-only class. > Or maybe, have a single type of "kernel-capable" class, decorated with @kernel. > That decorator simply registers the class with the compiler, and enforces Immutable in the interpreter. There are no restrictions on the type of methods (kernel, portable, rpc, host-only) that it contains. This is different, I don't expect kernel-only class to contain host-only/rpc methods, and the semantic for kernel and portable functions are different. It would not be possible to call a method of a kernel object on the host if it is not portable. I think the kernel-only object is just a simple object in our language, that can be fully-understood by the compiler, without dealing with the complicated stuff like RPC and host functions etc.

What is the advantage of kernel-only classes? We'd need to support classes that mix kernel, portable, rpc, and host-only methods anyway.

What is the advantage of kernel-only classes? We'd need to support classes that mix kernel, portable, rpc, and host-only methods anyway.

I agree with @sb10q that "mixed" classes are a must. Kernel-only classes could be interesting to allow construction of objects inside kernels, though tbh at this moment I do not have any use-case for that.

I agree with @sb10q that "mixed" classes are a must. Kernel-only classes could be interesting to allow construction of objects inside kernels, though tbh at this moment I do not have any use-case for that.

Objects that can be constructed inside kernels could have __init__ decorated with @portable or @kernel. But I'm not sure if we want to support them anyway.

Objects that can be constructed inside kernels could have ``__init__`` decorated with ``@portable`` or ``@kernel``. But I'm not sure if we want to support them anyway.

If the object is stored in the kernel only, RPC would have to pass the object to the host and run the method. But that probably seems fine...

If the object is stored in the kernel only, RPC would have to pass the object to the host and run the method. But that probably seems fine...

@pca006132 @sb10q also, I think the proposal in this tread makes sense, which would drop "kernel-only" classes. It is not contained in the current merge request. Should we still add this?

@pca006132 @sb10q also, I think the proposal in this tread makes sense, which would drop "kernel-only" classes. It is not contained in the current merge request. Should we still add this?

Yes, I think we can drop this, but we should make it explicit that users can create objects in the kernel if the constructor is portable or kernel. And maybe we should also document the behavior for RPC when the object only exists on the kernel. What do you think?

Yes, I think we can drop this, but we should make it explicit that users can create objects in the kernel if the constructor is `portable` or `kernel`. And maybe we should also document the behavior for RPC when the object only exists on the kernel. What do you think?

@pca006132 Sounds good.

@pca006132 Sounds good.
README.md Outdated
@ -131,48 +137,6 @@ Questions:
* Would it be better to assert on the type variable directly instead of
`type(x)` for type guards?

Not sure what alternative you have in mind, but if type(x) == int32: is fine for me, see also #7 .

Not sure what alternative you have in mind, but `if type(x) == int32:` is fine for me, see also #7 .

Perhaps something like this:

from typing import TypeVar
T = Union[int, bool]

def add1(x: T) -> int:
  if T == int:
      # x is int
      return x + 1
  else:
      # x must be bool
      return 2 if x else 1

but idk if this would be valid Python code, not yet tested.

Perhaps something like this: ```python from typing import TypeVar T = Union[int, bool] def add1(x: T) -> int: if T == int: # x is int return x + 1 else: # x must be bool return 2 if x else 1 ``` but idk if this would be valid Python code, not yet tested.

It wouldn't work in the interpreter (at least, without hacks). if type(x) == int32 is the interpreter-friendly way of doing it.

It wouldn't work in the interpreter (at least, without hacks). ``if type(x) == int32`` is the interpreter-friendly way of doing it.
pca006132 marked this conversation as resolved
sb10q reviewed 2021-04-12 16:06:03 +08:00
@ -15,1 +14,3 @@
Only primitive types and tuple/list of primitive types are allowed.
Kernel cannot modify host variables, this would be checked by the compiler.
Value that can be observed by the kernel would be frozen once the kernel has
been compiled, subsequence modification within the host would not affect the

Ah, this would cause another problem like those in #5
But deleting globals or trying to make them immutable in the interpreter sounds messy, and a global being modified in a RPC seems to be a niche case.
So I'm ok with this current proposal, the behavior simply should be documented.

Ah, this would cause another problem like those in https://git.m-labs.hk/M-Labs/nac3-spec/issues/5 But deleting globals or trying to make them immutable in the interpreter sounds messy, and a global being modified in a RPC seems to be a niche case. So I'm ok with this current proposal, the behavior simply should be documented.

Yes, I was thinking about this either, but it seems like there is no easy way around this.

Yes, I was thinking about this either, but it seems like there is no easy way around this.

Why don't we force users to type globals used in kernels as Invariant[]? The semantics are the same, making it clear to the user how this global variable can be used in kernels.

Why don't we force users to type globals used in kernels as `Invariant[]`? The semantics are the same, making it clear to the user how this global variable can be used in kernels.

It cannot be enforced in the interpreter.

It cannot be enforced in the interpreter.

I understand, it would just potentially make it more clear to the user that the global variable is immutable in the kernel. But I am fine with either!

I understand, it would just potentially make it more clear to the user that the global variable is immutable in the kernel. But I am fine with either!

I think the types that we allow are basically immutable? Int, tuples, string, etc.
Even if they are decorated with Invariant[T], the user can just re-assign them, so hard to enforce.

I think the types that we allow are basically immutable? Int, tuples, string, etc. Even if they are decorated with `Invariant[T]`, the user can just re-assign them, so hard to enforce.

For classes there are special __getattr__ and __setattr__ methods that we can use to hack the interpreter and enforce KernelImmutable restrictions.
There is no equivalent for modules/globals.

For classes there are special ``__getattr__`` and ``__setattr__`` methods that we can use to hack the interpreter and enforce ``KernelImmutable`` restrictions. There is no equivalent for modules/globals.

Yes, @sb10q showed a POC for immutable instance variables in #5, and I agree this does not work for global variables.

For me, the reasoning was to type global variables as KernelImmutable[] because they are per definition kernel immutable. That would align their type annotation with instance variables. Though since we can not enforce that at runtime, globals will anyway be a special case that needs documentation. So as mentioned before, I am also fine with not typing it KernelImmutable[].

Yes, @sb10q showed a POC for immutable instance variables in #5, and I agree this does not work for global variables. For me, the reasoning was to type global variables as `KernelImmutable[]` because they are per definition kernel immutable. That would align their type annotation with instance variables. Though since we can not enforce that at runtime, globals will anyway be a special case that needs documentation. So as mentioned before, I am also fine with not typing it `KernelImmutable[]`.
pca006132 added 2 commits 2021-04-12 19:59:10 +08:00
pca006132 added 1 commit 2021-04-12 20:16:54 +08:00

I've found something interesting:
image

I've found something interesting: ![image](/attachments/134e58e7-fd4b-442f-90c0-4bc4ea7b9049)

Nitpick but I would use Immutable instead of KernelInvariant to keep nac3core generic and not tied to ARTIQ concepts.

<s>Nitpick but I would use ``Immutable`` instead of ``KernelInvariant`` to keep nac3core generic and not tied to ARTIQ concepts.</s>

so should we merge this?

so should we merge this?

Yes

Yes
pca006132 merged commit d06a044189 into master 2021-06-07 10:06:52 +08:00
Sign in to join this conversation.
No reviewers
No Label
No Milestone
No Assignees
3 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: M-Labs/nac3-spec#8
There is no content yet.