From c8ebd80fe2241a7e153389add0cb91dd44380d2c Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Wed, 10 Nov 2021 12:18:20 +0800 Subject: [PATCH] NAC3 integration WIP --- artiq/coredevice/core.py | 130 +++++--------- artiq/frontend/artiq_compile.py | 27 +-- artiq/language/core.py | 296 +++++++------------------------- artiq/tools.py | 3 + 4 files changed, 111 insertions(+), 345 deletions(-) diff --git a/artiq/coredevice/core.py b/artiq/coredevice/core.py index 191a90de7..e10b0ccc4 100644 --- a/artiq/coredevice/core.py +++ b/artiq/coredevice/core.py @@ -1,55 +1,29 @@ import os, sys -import numpy +from numpy import int32, int64 -from artiq import __artiq_dir__ as artiq_dir +import nac3artiq from artiq.language.core import * -from artiq.language.types import * +from artiq.language import core as core_language from artiq.language.units import * -from artiq.compiler.module import Module -from artiq.compiler.embedding import Stitcher -from artiq.compiler.targets import RISCVTarget, CortexA9Target - from artiq.coredevice.comm_kernel import CommKernel, CommKernelDummy -# Import for side effects (creating the exception classes). -from artiq.coredevice import exceptions -def _render_diagnostic(diagnostic, colored): - def shorten_path(path): - return path.replace(artiq_dir, "") - lines = [shorten_path(path) for path in diagnostic.render(colored=colored)] - return "\n".join(lines) - -colors_supported = os.name == "posix" -class _DiagnosticEngine(diagnostic.Engine): - def render_diagnostic(self, diagnostic): - sys.stderr.write(_render_diagnostic(diagnostic, colored=colors_supported) + "\n") - -class CompileError(Exception): - def __init__(self, diagnostic): - self.diagnostic = diagnostic - - def __str__(self): - # Prepend a newline so that the message shows up on after - # exception class name printed by Python. - return "\n" + _render_diagnostic(self.diagnostic, colored=colors_supported) - - -@syscall -def rtio_init() -> TNone: +@extern +def rtio_init(): raise NotImplementedError("syscall not simulated") -@syscall(flags={"nounwind", "nowrite"}) -def rtio_get_destination_status(linkno: TInt32) -> TBool: +@extern +def rtio_get_destination_status(destination: int32) -> bool: raise NotImplementedError("syscall not simulated") -@syscall(flags={"nounwind", "nowrite"}) -def rtio_get_counter() -> TInt64: +@extern +def rtio_get_counter() -> int64: raise NotImplementedError("syscall not simulated") +@nac3 class Core: """Core device driver. @@ -64,89 +38,65 @@ class Core: and the RTIO coarse timestamp frequency (e.g. SERDES multiplication factor). """ - - kernel_invariants = { - "core", "ref_period", "coarse_ref_period", "ref_multiplier", - } + ref_period: KernelInvariant[float] + ref_multiplier: KernelInvariant[int32] + coarse_ref_period: KernelInvariant[float] def __init__(self, dmgr, host, ref_period, ref_multiplier=8, target="riscv"): self.ref_period = ref_period self.ref_multiplier = ref_multiplier - if target == "riscv": - self.target_cls = RISCVTarget - elif target == "cortexa9": - self.target_cls = CortexA9Target - else: - raise ValueError("Unsupported target") self.coarse_ref_period = ref_period*ref_multiplier if host is None: self.comm = CommKernelDummy() else: self.comm = CommKernel(host) - self.first_run = True self.dmgr = dmgr self.core = self self.comm.core = self + self.compiler = nac3artiq.NAC3(target) def close(self): self.comm.close() - def compile(self, function, args, kwargs, set_result=None, - attribute_writeback=True, print_as_rpc=True): - try: - engine = _DiagnosticEngine(all_errors_are_fatal=True) + def compile(self, method, args, kwargs, file_output=None): + if core_language._allow_module_registration: + self.compiler.analyze_modules(core_language._registered_modules) + core_language._allow_module_registration = False - stitcher = Stitcher(engine=engine, core=self, dmgr=self.dmgr, - print_as_rpc=print_as_rpc) - stitcher.stitch_call(function, args, kwargs, set_result) - stitcher.finalize() + if hasattr(method, "__self__"): + obj = method.__self__ + name = method.__name__ + else: + obj = method + name = "" - module = Module(stitcher, - ref_period=self.ref_period, - attribute_writeback=attribute_writeback) - target = self.target_cls() - - library = target.compile_and_link([module]) - stripped_library = target.strip(library) - - return stitcher.embedding_map, stripped_library, \ - lambda addresses: target.symbolize(library, addresses), \ - lambda symbols: target.demangle(symbols) - except diagnostic.Error as error: - raise CompileError(error.diagnostic) from error + if file_output is None: + return self.compiler.compile_method_to_mem(obj, name, args) + else: + self.compiler.compile_method_to_file(obj, name, args, file_output) def run(self, function, args, kwargs): - result = None - @rpc(flags={"async"}) - def set_result(new_result): - nonlocal result - result = new_result - - embedding_map, kernel_library, symbolizer, demangler = \ - self.compile(function, args, kwargs, set_result) - + kernel_library = self.compile(function, args, kwargs) if self.first_run: self.comm.check_system_info() self.first_run = False - self.comm.load(kernel_library) self.comm.run() - self.comm.serve(embedding_map, symbolizer, demangler) - + self.comm.serve(None, None, None) return result @portable - def seconds_to_mu(self, seconds): + def seconds_to_mu(self, seconds: float): """Convert seconds to the corresponding number of machine units (RTIO cycles). :param seconds: time (in seconds) to convert. """ - return numpy.int64(seconds//self.ref_period) + return int64(seconds//self.ref_period) @portable - def mu_to_seconds(self, mu): + def mu_to_seconds(self, mu: int64): """Convert machine units (RTIO cycles) to seconds. :param mu: cycle count to convert. @@ -154,7 +104,11 @@ class Core: return mu*self.ref_period @kernel - def get_rtio_counter_mu(self): + def delay(self, dt: float): + delay_mu(self.seconds_to_mu(dt)) + + @kernel + def get_rtio_counter_mu(self) -> int64: """Retrieve the current value of the hardware RTIO timeline counter. As the timing of kernel code executed on the CPU is inherently @@ -167,7 +121,7 @@ class Core: return rtio_get_counter() @kernel - def wait_until_mu(self, cursor_mu): + def wait_until_mu(self, cursor_mu: int64): """Block execution until the hardware RTIO counter reaches the given value (see :meth:`get_rtio_counter_mu`). @@ -178,7 +132,7 @@ class Core: pass @kernel - def get_rtio_destination_status(self, destination): + def get_rtio_destination_status(self, destination: int32): """Returns whether the specified RTIO destination is up. This is particularly useful in startup kernels to delay startup until certain DRTIO destinations are up.""" @@ -190,7 +144,7 @@ class Core: at the current value of the hardware RTIO counter plus a margin of 125000 machine units.""" rtio_init() - at_mu(rtio_get_counter() + 125000) + at_mu(rtio_get_counter() + int64(125000)) @kernel def break_realtime(self): @@ -199,6 +153,6 @@ class Core: If the time cursor is already after that position, this function does nothing.""" - min_now = rtio_get_counter() + 125000 + min_now = rtio_get_counter() + int64(125000) if now_mu() < min_now: at_mu(min_now) diff --git a/artiq/frontend/artiq_compile.py b/artiq/frontend/artiq_compile.py index 1609971e0..95be09c9c 100755 --- a/artiq/frontend/artiq_compile.py +++ b/artiq/frontend/artiq_compile.py @@ -8,7 +8,6 @@ from artiq import __version__ as artiq_version from artiq.master.databases import DeviceDB, DatasetDB from artiq.master.worker_db import DeviceManager, DatasetManager from artiq.language.environment import ProcessArgumentManager -from artiq.coredevice.core import CompileError from artiq.tools import * @@ -47,6 +46,11 @@ def main(): device_mgr = DeviceManager(DeviceDB(args.device_db)) dataset_mgr = DatasetManager(DatasetDB(args.dataset_db)) + output = args.output + if output is None: + basename, ext = os.path.splitext(args.file) + output = "{}.elf".format(basename) + try: module = file_import(args.file, prefix="artiq_run_") exp = get_experiment(module, args.class_name) @@ -54,29 +58,12 @@ def main(): argument_mgr = ProcessArgumentManager(arguments) exp_inst = exp((device_mgr, dataset_mgr, argument_mgr, {})) - if not hasattr(exp.run, "artiq_embedded"): + if not getattr(exp.run, "__artiq_kernel__", False): raise ValueError("Experiment entry point must be a kernel") - core_name = exp.run.artiq_embedded.core_name - core = getattr(exp_inst, core_name) - - object_map, kernel_library, _, _ = \ - core.compile(exp.run, [exp_inst], {}, - attribute_writeback=False, print_as_rpc=False) - except CompileError as error: - return + exp_inst.core.compile(exp_inst.run, [], {}, file_output=output) finally: device_mgr.close_devices() - if object_map.has_rpc(): - raise ValueError("Experiment must not use RPC") - - output = args.output - if output is None: - basename, ext = os.path.splitext(args.file) - output = "{}.elf".format(basename) - - with open(output, "wb") as f: - f.write(kernel_library) if __name__ == "__main__": main() diff --git a/artiq/language/core.py b/artiq/language/core.py index 5560398dd..4dfb22e9a 100644 --- a/artiq/language/core.py +++ b/artiq/language/core.py @@ -2,273 +2,95 @@ Core ARTIQ extensions to the Python language. """ -from collections import namedtuple +from typing import Generic, TypeVar from functools import wraps -import numpy +from inspect import getfullargspec, getmodule +from types import SimpleNamespace -__all__ = ["kernel", "portable", "rpc", "syscall", "host_only", - "kernel_from_string", "set_time_manager", "set_watchdog_factory", - "TerminationRequested"] - -# global namespace for kernels -kernel_globals = ( - "sequential", "parallel", "interleave", - "delay_mu", "now_mu", "at_mu", "delay", - "watchdog" -) -__all__.extend(kernel_globals) +__all__ = [ + "KernelInvariant", + "extern", "kernel", "portable", "nac3", "rpc", + "parallel", "sequential", + "set_watchdog_factory", "watchdog", "TerminationRequested" +] -_ARTIQEmbeddedInfo = namedtuple("_ARTIQEmbeddedInfo", - "core_name portable function syscall forbidden flags") +T = TypeVar('T') +class KernelInvariant(Generic[T]): + pass -def kernel(arg=None, flags={}): - """ - This decorator marks an object's method for execution on the core - device. - When a decorated method is called from the Python interpreter, the :attr:`core` - attribute of the object is retrieved and used as core device driver. The - core device driver will typically compile, transfer and run the method - (kernel) on the device. +_allow_module_registration = True +_registered_modules = set() - When kernels call another method: +def _register_module_of(obj): + assert _allow_module_registration + # Delay NAC3 analysis until all referenced variables are supposed to exist on the CPython side. + _registered_modules.add(getmodule(obj)) - - if the method is a kernel for the same core device, it is compiled - and sent in the same binary. Calls between kernels happen entirely on - the device. - - if the method is a regular Python method (not a kernel), it generates - a remote procedure call (RPC) for execution on the host. - The decorator takes an optional parameter that defaults to :attr`core` and - specifies the name of the attribute to use as core device driver. +def extern(function): + """Decorates a function declaration defined by the core device runtime.""" + _register_module_of(function) + return function - This decorator must be present in the global namespace of all modules using - it for the import cache to work properly. - """ - if isinstance(arg, str): - def inner_decorator(function): - @wraps(function) - def run_on_core(self, *k_args, **k_kwargs): - return getattr(self, arg).run(run_on_core, ((self,) + k_args), k_kwargs) - run_on_core.artiq_embedded = _ARTIQEmbeddedInfo( - core_name=arg, portable=False, function=function, syscall=None, - forbidden=False, flags=set(flags)) - return run_on_core - return inner_decorator - elif arg is None: - def inner_decorator(function): - return kernel(function, flags) - return inner_decorator + +def kernel(function_or_method): + """Decorates a function or method to be executed on the core device.""" + _register_module_of(function_or_method) + argspec = getfullargspec(function_or_method) + if argspec.args and argspec.args[0] == "self": + @wraps(function_or_method) + def run_on_core(self, *args, **kwargs): + fake_method = SimpleNamespace(__self__=self, __name__=function_or_method.__name__) + self.core.run(fake_method, *args, **kwargs) else: - return kernel("core", flags)(arg) + @wraps(function_or_method) + def run_on_core(*args, **kwargs): + raise RuntimeError("Kernel functions need explicit core.run()") + run_on_core.__artiq_kernel__ = True + return run_on_core -def portable(arg=None, flags={}): + +def portable(function): + """Decorates a function or method to be executed on the same device (host/core device) as the caller.""" + _register_module_of(function) + return function + + +def nac3(cls): """ - This decorator marks a function for execution on the same device as its - caller. - - In other words, a decorated function called from the interpreter on the - host will be executed on the host (no compilation and execution on the - core device). A decorated function called from a kernel will be executed - on the core device (no RPC). - - This decorator must be present in the global namespace of all modules using - it for the import cache to work properly. + Decorates a class to be analyzed by NAC3. + All classes containing kernels or portable methods must use this decorator. """ - if arg is None: - def inner_decorator(function): - return portable(function, flags) - return inner_decorator - else: - arg.artiq_embedded = \ - _ARTIQEmbeddedInfo(core_name=None, portable=True, function=arg, syscall=None, - forbidden=False, flags=set(flags)) - return arg + _register_module_of(cls) + return cls + def rpc(arg=None, flags={}): """ This decorator marks a function for execution on the host interpreter. - This is also the default behavior of ARTIQ; however, this decorator allows - specifying additional flags. """ if arg is None: def inner_decorator(function): return rpc(function, flags) return inner_decorator - else: - arg.artiq_embedded = \ - _ARTIQEmbeddedInfo(core_name=None, portable=False, function=arg, syscall=None, - forbidden=False, flags=set(flags)) - return arg - -def syscall(arg=None, flags={}): - """ - This decorator marks a function as a system call. When executed on a core - device, a C function with the provided name (or the same name as - the Python function, if not provided) will be called. When executed on - host, the Python function will be called as usual. - - Every argument and the return value must be annotated with ARTIQ types. - - Only drivers should normally define syscalls. - """ - if isinstance(arg, str): - def inner_decorator(function): - function.artiq_embedded = \ - _ARTIQEmbeddedInfo(core_name=None, portable=False, function=None, - syscall=arg, forbidden=False, - flags=set(flags)) - return function - return inner_decorator - elif arg is None: - def inner_decorator(function): - return syscall(function.__name__, flags)(function) - return inner_decorator - else: - return syscall(arg.__name__)(arg) - -def host_only(function): - """ - This decorator marks a function so that it can only be executed - in the host Python interpreter. - """ - function.artiq_embedded = \ - _ARTIQEmbeddedInfo(core_name=None, portable=False, function=None, syscall=None, - forbidden=True, flags={}) - return function -def kernel_from_string(parameters, body_code, decorator=kernel): - """Build a kernel function from the supplied source code in string form, - similar to ``exec()``/``eval()``. - - Operating on pieces of source code as strings is a very brittle form of - metaprogramming; kernels generated like this are hard to debug, and - inconvenient to write. Nevertheless, this can sometimes be useful to work - around restrictions in ARTIQ Python. In that instance, care should be taken - to keep string-generated code to a minimum and cleanly separate it from - surrounding code. - - The resulting function declaration is also evaluated using ``exec()`` for - use from host Python code. To encourage a modicum of code hygiene, no - global symbols are available by default; any objects accessed by the - function body must be passed in explicitly as parameters. - - :param parameters: A list of parameter names the generated functions - accepts. Each entry can either be a string or a tuple of two strings; - if the latter, the second element specifies the type annotation. - :param body_code: The code for the function body, in string form. - ``return`` statements can be used to return values, as usual. - :param decorator: One of ``kernel`` or ``portable`` (optionally with - parameters) to specify how the function will be executed. - - :return: The function generated from the arguments. - """ - - # Build complete function declaration. - decl = "def kernel_from_string_fn(" - for p in parameters: - type_annotation = "" - if isinstance(p, tuple): - name, typ = p - type_annotation = ": " + typ - else: - name = p - decl += name + type_annotation + "," - decl += "):\n" - decl += "\n".join(" " + line for line in body_code.split("\n")) - - # Evaluate to get host-side function declaration. - context = {} - try: - exec(decl, context) - except SyntaxError: - raise SyntaxError("Error parsing kernel function: '{}'".format(decl)) - fn = decorator(context["kernel_from_string_fn"]) - - # Save source code for the compiler to pick up later. - fn.artiq_embedded = fn.artiq_embedded._replace(function=decl) - return fn - - -class _DummyTimeManager: - def _not_implemented(self, *args, **kwargs): - raise NotImplementedError( - "Attempted to interpret kernel without a time manager") - - enter_sequential = _not_implemented - enter_parallel = _not_implemented - exit = _not_implemented - take_time_mu = _not_implemented - get_time_mu = _not_implemented - set_time_mu = _not_implemented - take_time = _not_implemented - -_time_manager = _DummyTimeManager() - - -def set_time_manager(time_manager): - """Set the time manager used for simulating kernels by running them - directly inside the Python interpreter. The time manager responds to the - entering and leaving of interleave/parallel/sequential blocks, delays, etc. and - provides a time-stamped logging facility for events. - """ - global _time_manager - _time_manager = time_manager - - -class _Sequential: - """In a sequential block, statements are executed one after another, with - the time increasing as one moves down the statement list.""" +@nac3 +class KernelContextManager: + @kernel def __enter__(self): - _time_manager.enter_sequential() + pass - def __exit__(self, type, value, traceback): - _time_manager.exit() -sequential = _Sequential() + @kernel + def __exit__(self): + pass +parallel = KernelContextManager() +sequential = KernelContextManager() -class _Parallel: - """In a parallel block, all top-level statements start their execution at - the same time. - - The execution time of a parallel block is the execution time of its longest - statement. A parallel block may contain sequential blocks, which themselves - may contain interleave blocks, etc. - """ - def __enter__(self): - _time_manager.enter_parallel() - - def __exit__(self, type, value, traceback): - _time_manager.exit() -parallel = _Parallel() -interleave = _Parallel() # no difference in semantics on host - -def delay_mu(duration): - """Increases the RTIO time by the given amount (in machine units).""" - _time_manager.take_time_mu(duration) - - -def now_mu(): - """Retrieve the current RTIO timeline cursor, in machine units. - - Note the conceptual difference between this and the current value of the - hardware RTIO counter; see e.g. - :meth:`artiq.coredevice.core.Core.get_rtio_counter_mu` for the latter. - """ - return _time_manager.get_time_mu() - - -def at_mu(time): - """Sets the RTIO time to the specified absolute value, in machine units.""" - _time_manager.set_time_mu(time) - - -def delay(duration): - """Increases the RTIO time by the given amount (in seconds).""" - _time_manager.take_time(duration) class _DummyWatchdog: diff --git a/artiq/tools.py b/artiq/tools.py index 167f8cf74..106fcf958 100644 --- a/artiq/tools.py +++ b/artiq/tools.py @@ -80,6 +80,9 @@ def file_import(filename, prefix="file_import_"): try: spec = importlib.util.spec_from_file_location(modname, filename) module = importlib.util.module_from_spec(spec) + # Add to sys.modules, otherwise inspect thinks it is a built-in module and often breaks. + # This must take place before module execution because NAC3 decorators call inspect. + sys.modules[modname] = module spec.loader.exec_module(module) finally: sys.path.remove(path)