Replies: 1 comment
-
Here I begin to discuss the implementation of ideas which I previously have stated only vaguely regarding modal models, causality interfaces, low-level cross-reaction optimization, and the question of what information should appear in an ideal IR for LF. This is not (yet) a proposal. This is intended to be a living document with the potential (but not the promise) to evolve into a proposal. Read statements in this document as being prefixed by "imagine if" or "don't you think it would be nice if." User-visible objectives
Developer-visible objectives
High-level implementation detailsThe design described here is actually quite similar to the current design because I (mostly) like the current design. The difference is that many things which are global in the current design are recast to be reactor-local. Lowered/specialized reactor representations
Executing local levels
MLAAs: Initiating level execution
TAGs: Initiating tag execution
Implementing federated execution
Lower-level implementation details
Speculations that I still need to think about more
Possible optimizations
Addressing potential concernsCode size and compilation time: Our generics implementation is akin to that found in Rust and C++ ("stenciling"). There are other ways to get behavior similar to generics, such as the Java approach, which is similar to the
Overall, the code size and compilation time issues introduced by this approach might be small in comparison to other code size and compilation time issues. Indeed, they might even solve some of these other issues. For example, they would enable compositional cycle detection, which can result in asymptotically better performance in programs with repeated structure (and even infinitely better performance in the case of infinite programs -- programs with recursive instantiation are infinite). They could also reduce code size by simplifying the startup code. Heap space: This approach involves creating more events than would be needed if there were just one central event queue. In particular, it requires child reactors to create extra events in their parent reactors to remind their parents to activate them again when they have something to do.
Performance: This proposal introduces abstractions. Abstractions can introduce a cost. In particular, if communications between sibling reactors have to go through the common parent, then this extra indirection can introduce cost.
Advantages over alternative designsI believe the design sketched above to offer solutions to several architectural problems that we have faced. If one does not follow the design sketched above, then one must consider alternative solutions to these problems. Here are some alternative solutions; I will explain why I am dissatisfied with them. Using mutations instead of mode changesIt is not yet clear to me exactly what mutations should mean. However, they seem to be associated with almost arbitrary changes to program structure. Reactors can add children within themselves and connect the children to any of their other children, and they can add siblings and connect to their siblings. Mode changes are distinguished by the fact that there is only a limited, pre-defined set of possible changes that can be made within any single reactor. The possible local states can be enumerated concisely at compile time. Since this allows for a combinatorially large number of global program states, this should not place fundamental limitations on expressiveness. It should, however, allow invariants regarding the local program structure to be trivially proved by simply checking that they hold true for each of the possible modes. Perhaps the most important invariant is whether a given pair of ports will ever have a zero-delay path between them, but it is possible to imagine other interesting invariants such as whether the presence of a given port at a given time guarantees the presence of another port at another time. Supporting mutations with widely spaced levelsPerhaps the most fundamental problem with dynamic program structure is the need to recompute a global level assignment at runtime. One way to avoid this is to start with levels that are widely spaced; then, if reactions are added, give them levels that are in between the levels that are already used. There are two problems with this approach. The first problem -- that this makes it impractical to use a statically allocated array of pointers to statically allocated arrays of enqueued reactions -- is easily circumvented, although it may be worth keeping in mind since such a data structure is central to some of the most efficient execution strategies that we currently support. The second problem -- that this approach doesn't really work in the sense that it cannot in general obviate the need to do global level reassignments at runtime -- is more grave. It may seem that because a 64-bit number can be astronomically large, there is plenty of space to add more levels in between the existing ones. However, consider the case when we want to add a reaction whose level is between two levels. Reactions can be added before it or after it, so we may wish to put its level halfway between the two existing levels. In such a scheme, the minimum spacing between levels can in the worst case decrease by a factor of two for each reaction that is added! By the time 64 reactions are added, we must recompute the global level assignment. It is not necessary to prototype the "widely spaced levels" strategy in order to find out whether this will be a problem; we know it will be a problem, and we already understand it well, so let's not waste time unless we know of a solution. Supporting mutations with run-time cycle detectionAnother problem that must be solved when programs can change their structure at run-time is that in the absence of guaranteed invariants that are sufficient to prove cycle-freedom, one must contend with cycles that appear at runtime. This is related to the level assignment/scheduling problem in that it is meant to discover programs that are not schedulable. The design space is large and dependent on the scheduling strategy. However, it is in any case highly desirable to ensure that errors relating to cycles happen at compile time or initialization time rather than accepting that they may occur at any time, or -- even worse -- failing to detect cycles at all. Furthermore, no cycle detection strategy, compile-time or run-time, other than the one sketched above, has yet been proposed that would not a) occur at run-time, and b) scale linearly in the number of instantiated reactors and in the number of mutations executed. Supporting federated execution with LF programs that are annotated with TPOI think that the TPO solution that we are currently moving toward is preferable to spawning many threads to support unordered reactions, and I think am glad that it guarantees deadlock freedom. I also think that However, I think that the architecture proposed here is a desirable refinement on the design that we are moving toward now. It allows an RTI-style coordination of time advancement without creating a single point of failure and without requiring one central process (an RTI) to know the structure of all communicating federates. This is because it should be possible to compile a black-box reactor that implements the ABI and that coordinates the time advancement of only the subprogram that it contains, rather than coordinating the time advancement of any whole program. Similarly, it should be possible to compile a wrapper around the instantiated federate that handles network messages and regulates the time advancement of the instantiated federate; this allows a compiled reactor that implements the ABI to be used in a federate without changing the runtime implementation of the compiled reactor. Supporting custom timescalesWe currently support only the nanosecond timescale. This is part of the semantics of LF in the sense that the smallest time units in LF are nanoseconds. However, it is also part of the low-level implementation. This has caused problems for us when we have considered using microseconds instead of nanoseconds on 32-bit platforms. In the current runtime implementation for both C and C++, changes would have to be made deep within the runtime to change the base time unit to any unit other than nanoseconds. This proposal allows us to remove the timescale from the low-level implementation by moving the code for waiting for the next event on the event queue and for assigning explicit meaning to the fields of An added benefit of moving such code into the execution shell is that it makes compiled reactors less platform-specific. Using LF programs in non-LF-based simulationsAlthough we have users who wish to use other simulations (MQSim) in LF programs, we are not currently aware of users who wish to use LF code in other simulations. This is probably because LF is new enough that there do not currently exist any complex, renowned simulations like MQSim that are implemented in LF. However, this will change if LF is successful. In order to integrate a simulation written using one framework into a simulation written in another framework, it is necessary to manage the time advancement of the nested simulation. In current implementations of Lingua Franca, it would be necessary to make changes deep within the runtime; for example, in the C implementation, the By contrast, if an LF program consists of a main reactor that has some ABI, combined with an execution shell that interacts with the ABI to manage time advancement, then all that would be required in order to integrate the LF program into a containing simulation would be to discard the execution shell and adapt the containing simulation to interact directly with the ABI. Integrating multiple target languages into one LF programAlthough this has not been done yet, it is certainly possible to write LF programs involving many target languages; this can be trivially done by having LF programs use C, C++, or Rust code that calls into compiled code in other languages. However, to take advantage of this in the current, either the LF maintainers or their users must maintain complex build systems that integrate multiple toolchains. It is not as simple as providing object files with reaction functions because the reaction functions have to be compatible with certain self structs and port structs, which must be defined in generated header files which must in turn be translated between languages -- this is possible and tools exist for this, but it may not prove to be highly convenient and in any case it forces reaction functions to operate on very specific data structures, which might not be idiomatic for different target languages and which may not come with the same compile-time guarantees that would be possible when larger pieces of code are compiled to or for e.g. Rust. Construction of multilingual programs could be greatly simplified by defining an ABI for reactors and allowing reactors (compiled down from a TPO-specifying JSON IR using arbitrary toolchains) to be distributed in object files. Certainly not all aspects of the proposal here are required for defining an ABI -- for example, any current implementation of LF that compiles reactors to a collection of procedures in machine code could be documented in detail so that the various procedures could in principle be used as a well-defined ABI -- but to my knowledge this has not yet been done, and for the C target at least I know that it would be difficult due to the complex initialization in Implementation steps
|
Beta Was this translation helpful? Give feedback.
-
This discussion is meant as a depository of ideas discussed during meetings around the topic of mutations. Please feel welcome to edit.
Related discussions: #1191
Related issues: ...
Related PRs: ...
Top-level mutations in a federation
In a federation (i.e., a top-level federated reactor), each reactor instance in the federated reactor gets mapped to its own process to become a federate. Each such instance is instantiated only once; other instances that it communicates with reside in other processes and are interacted with through the network.
It would seem natural to take a similar approach (i.e., have a unique representation of each element) with other entities in the federated reactor, such as reactions, mutations, actions, state, etc. However, one such element might interact with more than one federate (let's call it a shared element), which then begs the question: which federate should it be mapped to? We've come up with some options:
N case
N+1 case
Instantaneous effects
WIP...
Beta Was this translation helpful? Give feedback.
All reactions