Compare commits
5 commits
79e1a48d28
...
01326b05ed
| Author | SHA1 | Date | |
|---|---|---|---|
| 01326b05ed | |||
| 4a348ab4cd | |||
| 1d0d4d66f0 | |||
| 995f0f8c70 | |||
| f8219f031e |
132 changed files with 5577 additions and 3386 deletions
|
|
@ -1,3 +1,3 @@
|
||||||
[alias]
|
[alias]
|
||||||
xtask = "run --package xtask --"
|
xtask = "run --package xtask --"
|
||||||
mvp-chat = "run --package xtask -- mvp-chat"
|
myelin-chat = "run --package xtask -- myelin-chat"
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
*
|
*
|
||||||
!apps/
|
!apps/
|
||||||
!apps/mvp-node/
|
!apps/myelin/node-image/
|
||||||
!apps/mvp-node/**
|
!apps/myelin/node-image/**
|
||||||
!target/
|
!target/
|
||||||
target/*
|
target/*
|
||||||
!target/release/
|
!target/release/
|
||||||
target/release/*
|
target/release/*
|
||||||
!target/release/mvp-node
|
!target/release/myelin-worker
|
||||||
!target/release/mvp-worker-node
|
|
||||||
!target/debug/
|
!target/debug/
|
||||||
target/debug/*
|
target/debug/*
|
||||||
!target/debug/mvp-node
|
!target/debug/myelin-worker
|
||||||
!target/debug/mvp-worker-node
|
|
||||||
|
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -2,10 +2,7 @@
|
||||||
__pycache__
|
__pycache__
|
||||||
fuzz/artifacts/**
|
fuzz/artifacts/**
|
||||||
corpus
|
corpus
|
||||||
.loop/
|
|
||||||
.model-cache/
|
.model-cache/
|
||||||
.deployment-notes/
|
|
||||||
.omp/
|
|
||||||
|
|
||||||
# environment variables for deployment configurations
|
# environment variables for deployment configurations
|
||||||
.config/
|
.config/
|
||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -2517,7 +2517,7 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mvp-system"
|
name = "myelin"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"blake3",
|
"blake3",
|
||||||
|
|
|
||||||
25
Cargo.toml
25
Cargo.toml
|
|
@ -11,16 +11,37 @@ members = [
|
||||||
"crates/datastream",
|
"crates/datastream",
|
||||||
"crates/data-plane",
|
"crates/data-plane",
|
||||||
"crates/dashboard",
|
"crates/dashboard",
|
||||||
"crates/mvp-system",
|
"apps/myelin",
|
||||||
"xtask",
|
"xtask",
|
||||||
"tools/vastai",
|
"tools/vastai",
|
||||||
]
|
]
|
||||||
exclude = ["crates/bindings/wasm-crypto", "examples", "apps"]
|
default-members = [
|
||||||
|
".",
|
||||||
|
"crates/process",
|
||||||
|
"crates/provisioning",
|
||||||
|
"crates/transport",
|
||||||
|
"crates/distribution",
|
||||||
|
"crates/iroh-driver",
|
||||||
|
"crates/datastream",
|
||||||
|
"crates/data-plane",
|
||||||
|
"crates/dashboard",
|
||||||
|
"apps/myelin",
|
||||||
|
"tools/vastai",
|
||||||
|
]
|
||||||
|
# The language bindings (python, wasm) and xtask are built on demand
|
||||||
|
# (-p / --workspace / cargo-xtask), not during normal native iteration.
|
||||||
|
exclude = ["crates/bindings/wasm-crypto", "examples"]
|
||||||
|
[workspace.dependencies]
|
||||||
|
# Centralized so every member resolves tokio with the SAME feature set.
|
||||||
|
# Without this, members declared tokio with different features, so building
|
||||||
|
# one member vs another (or `run` vs `test`) recompiled tokio each time.
|
||||||
|
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "process", "io-util", "sync", "time", "net", "signal"] }
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "swactor"
|
name = "swactor"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
autobenches = false
|
autobenches = false
|
||||||
|
|
||||||
[profile.bench]
|
[profile.bench]
|
||||||
|
|
|
||||||
661
LICENSE
Normal file
661
LICENSE
Normal file
|
|
@ -0,0 +1,661 @@
|
||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
397
README.md
397
README.md
|
|
@ -1,23 +1,392 @@
|
||||||
# swactor
|
# swactor
|
||||||
|
|
||||||
Minimal actor runtime for Rust. One trait, one message type.
|
A small, WASM-first actor runtime for Rust: one trait, location-transparent
|
||||||
Single-threaded (`tick()`) or multi-threaded (`run()`).
|
addresses, and a built-in cluster. Actors are ordinary structs; a message is
|
||||||
|
any `Clone + Send + Sync` type; an address works the same whether the actor
|
||||||
|
lives in this process, on a peer, or behind NAT on another node.
|
||||||
|
|
||||||
## Description
|
## What it is
|
||||||
|
|
||||||
Core runtime is `src/`. Actors implement `ActorInterface` (in `actor.rs`),
|
Every actor is one trait. There is no separate message trait to implement —
|
||||||
interact through `Ctx` (in `runtime.rs`), and run on worker threads (`worker.rs`).
|
anything `Clone + Send + Sync` is a message by blanket impl:
|
||||||
|
|
||||||
`crates/` builds upward: `std` adds OTP patterns (supervision, monitoring, groups),
|
```rust
|
||||||
`distribution` adds clustering, everything else composes from there.
|
// src/actor.rs
|
||||||
|
pub trait Message: 'static + Sized + Clone + Send + Sync {}
|
||||||
## Dev commands
|
impl<T: 'static + Sized + Clone + Send + Sync> Message for T {}
|
||||||
|
|
||||||
`cargo xtask --help` for the basic test command.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
|
pub trait ActorInterface: 'static + Send {
|
||||||
|
type Incoming: Message;
|
||||||
|
type Response: Message;
|
||||||
|
fn handle(&mut self, ctx: &Ctx, msg: Self::Incoming);
|
||||||
|
// ...on_start, on_stop, handle_down (death monitoring) as needed
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You drive the runtime single-threaded with `tick()` (this is what compiles to
|
||||||
|
WASM) or multi-threaded with `run()`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use swactor::actor::{ActorInterface, Ctx};
|
||||||
|
use swactor::runtime::{Runtime, RuntimeConfig};
|
||||||
|
|
||||||
|
struct Counter { count: u64 }
|
||||||
|
|
||||||
|
impl ActorInterface for Counter {
|
||||||
|
type Incoming = (); // `()` is Clone + Send + Sync, so it's a message
|
||||||
|
type Response = ();
|
||||||
|
fn handle(&mut self, _ctx: &Ctx, _: Self::Incoming) {
|
||||||
|
self.count += 1;
|
||||||
|
println!("hit #{self.count}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), swactor::Error> {
|
||||||
|
let rt = Runtime::new(RuntimeConfig::default()); // 1 worker → drive with tick()
|
||||||
|
let addr = rt.spawn(Counter { count: 0 })?;
|
||||||
|
rt.send_to(addr, ())?;
|
||||||
|
for _ in 0..3 { rt.tick(); } // spawn → handle → done
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`RuntimeConfig` is three knobs — `max_actors`, `channel_buffer_size`,
|
||||||
|
and a per-tick `actor_message_budget` (inspired by BEAM's
|
||||||
|
reduction count, so one chatty mailbox can't starve the rest).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Each feature is shown in the code that implements it.
|
||||||
|
|
||||||
|
### Route messages by actor_id across cluster boundaries
|
||||||
|
|
||||||
|
An actor's identity is a 32-byte address, not a pointer:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/actor.rs
|
||||||
|
pub struct ActorAddress(pub [u8; 32]);
|
||||||
|
```
|
||||||
|
|
||||||
|
You send to it the same way whether it lives in this process, on a peer across
|
||||||
|
the room, or behind NAT on another continent — one call, `ctx.send(addr, msg)`.
|
||||||
|
The locality decision is exactly one address-map lookup; a miss hands the
|
||||||
|
message to the cluster transport instead of a local worker:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/delivery.rs — the cluster boundary
|
||||||
|
fn route_nonlocal(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error> {
|
||||||
|
#[cfg(feature = "transport")]
|
||||||
|
{
|
||||||
|
if self.inbox_registry.contains(&addr) { // a pending reply inbox
|
||||||
|
return self.inbox_registry.try_deliver(addr, msg);
|
||||||
|
}
|
||||||
|
if let Some(sink) = self.remote_sink { // ← off this node
|
||||||
|
return sink.send(addr, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.inbox_registry.try_deliver(addr, msg)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The transport seam is a single trait — any cluster driver satisfies it:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/runtime.rs
|
||||||
|
pub trait RemoteSink: Send + Sync {
|
||||||
|
fn send(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `addr` lives is resolved from a signed, gossiped directory: a host-signed
|
||||||
|
`DirectoryEntry` binds `actor_addr → node_id`, backed by an LRU `LocationCache`.
|
||||||
|
The node's own mailbox (`node_id → ActorAddress`) is rebound on restart without
|
||||||
|
changing identity, which is why `NodeId` and `ActorAddress` are deliberately
|
||||||
|
distinct types.
|
||||||
|
|
||||||
|
### WASM by default
|
||||||
|
|
||||||
|
The same runtime that runs on a thread pool also compiles to `wasm32` and steps
|
||||||
|
one tick at a time from a host (browser, Node, an embedding app). Randomness,
|
||||||
|
timers, and the address map all have a no-op/wasm path behind the `wasm`
|
||||||
|
feature. The [ping-pong demo](examples/ping-pong) is one runtime compiled to a
|
||||||
|
`.wasm` cdylib and driven by Node:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd examples/ping-pong && ./run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Built-in engine and driver, tasks included
|
||||||
|
|
||||||
|
A node ships batteries-included: the actor engine (worker threads), a network
|
||||||
|
driver, and the pump/fanout tasks that wire them — you don't assemble the glue.
|
||||||
|
The driver tracks its own established send/recv pumps per QUIC stream and emits
|
||||||
|
lifecycle events (edge ready, stream fault, pump stopped):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// crates/iroh-driver/src/driver_pumps.rs
|
||||||
|
pub enum DriverEventOut {
|
||||||
|
DriverEdgeReady { edge_id: EdgeId },
|
||||||
|
StreamFault { edge_id: EdgeId },
|
||||||
|
PumpStopped { edge_id: EdgeId, ring_id: RingId },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `iroh` integration — QUIC, TLS, and NAT traversal
|
||||||
|
|
||||||
|
The bundled driver is [iroh](https://iroh.computer): QUIC-based peer transport
|
||||||
|
with built-in TLS, NAT hole-punching, and relay-server fallback. You hand it a
|
||||||
|
Tokio engine and it runs accepts, dials, stream I/O, retries, and shutdown on
|
||||||
|
it; peers are admitted through an optional allow-list:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// crates/iroh-driver/src/iroh_driver.rs
|
||||||
|
pub struct IrohDriverConfig {
|
||||||
|
pub secret_key: Option<SecretKey>, // None → fresh random identity
|
||||||
|
pub relay_mode: RelayMode, // default: n0 production relays
|
||||||
|
pub node: DistributedNodeConfig,
|
||||||
|
pub peer_auth: Option<Arc<Mutex<PeerAllowList>>>,
|
||||||
|
pub additional_alpns: Vec<Vec<u8>>, // opaque to the driver
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Built-in SWIM cluster
|
||||||
|
|
||||||
|
Membership is a real SWIM implementation — direct + indirect (relay) probes,
|
||||||
|
suspicion timers, and Lifeguard local-health scaling — not heartbeats. The probe
|
||||||
|
state machine emits typed actions; the host adapter turns them into sends:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// crates/distribution/src/swim/probe.rs
|
||||||
|
pub enum SwimAction {
|
||||||
|
SendPing { to: NodeId, sequence: u64 },
|
||||||
|
SendPingReq { relay: NodeId, target: NodeId, sequence: u64 }, // indirect probe
|
||||||
|
Suspect(NodeId),
|
||||||
|
DeclareDead(NodeId),
|
||||||
|
Diag(SwimDiagEvent), // RTT samples — observation only, no protocol effect
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Name bindings, node metadata (relay URL, human name), and the actor→host
|
||||||
|
directory each run as independent gossip actors, so the cluster goes quiet once
|
||||||
|
it converges.
|
||||||
|
|
||||||
|
### OTP-style standard library
|
||||||
|
|
||||||
|
Behind the `std` feature, one extension adds the patterns you reach for:
|
||||||
|
symbolic naming, death monitoring (watching), and process groups. Install it on
|
||||||
|
the runtime and actors get `CtxWatching` / `CtxNaming`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/std/extension.rs
|
||||||
|
pub struct StdExtension {
|
||||||
|
name_registry: NameRegistry, // logical names → addresses
|
||||||
|
watch_registry: WatchRegistry, // exit notifications
|
||||||
|
group_registry: GroupRegistry, // named groups / pub-sub
|
||||||
|
}
|
||||||
|
// runtime.with_extension(Arc::new(StdExtension::new()));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Process manager for external processes
|
||||||
|
|
||||||
|
Treat an OS process like an actor. Describe it, spawn it onto the runtime, and
|
||||||
|
send it commands; its lifecycle (started / exited / signaled) arrives as
|
||||||
|
messages:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// crates/process/src/types.rs
|
||||||
|
pub struct ProcessSpec {
|
||||||
|
pub command: String,
|
||||||
|
pub args: Vec<String>,
|
||||||
|
pub env: HashMap<String, String>,
|
||||||
|
pub working_dir: Option<std::path::PathBuf>,
|
||||||
|
pub label: Option<String>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There's a YAML-driven pipeline layer on top (`crates/process/src/pipeline.rs`)
|
||||||
|
for multi-step jobs with progress and failure reporting.
|
||||||
|
|
||||||
|
### Zero-copy movement of large byte objects
|
||||||
|
|
||||||
|
The data-plane ships large, *typed* byte objects between actors — and across
|
||||||
|
nodes — without copying. The motivating workload is sharded inference: one GPU's
|
||||||
|
output activation is the next GPU's input, so it rides a bounded ring leased from
|
||||||
|
a shared arena. A ring is either local (IPC between two actors on one node) or
|
||||||
|
the two ends of a QUIC edge:
|
||||||
|
|
||||||
|
```text
|
||||||
|
producer → egress ring ──QUIC──▶ ingress ring → consumer
|
||||||
|
(arena lease; zero-copy at both ends)
|
||||||
|
```
|
||||||
|
|
||||||
|
What travels on a wire edge is typed, so the receiver knows what arrived:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// crates/data-plane/src/actor.rs
|
||||||
|
pub enum EdgeKind {
|
||||||
|
TokenIn,
|
||||||
|
Activation, // a hidden-state tensor forwarded to the next shard
|
||||||
|
TokenOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
// crates/data-plane/src/edge_lifecycle.rs
|
||||||
|
pub struct ObjectSpec {
|
||||||
|
pub kind: ObjectKind, // Activation
|
||||||
|
pub dtype: DType, // F16
|
||||||
|
pub max_extent_bytes: u64,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The edge actor owns the lifecycle: lease a byte range, install an ingress or
|
||||||
|
egress ring, and release it only with a `QuiescenceProof`, so a range is never
|
||||||
|
recycled under a live reader. The worker consumes whole objects off its ingress
|
||||||
|
ring as plain bytes:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// apps/myelin/src/node/worker_node_runtime.rs
|
||||||
|
match ingress::read_object_record(buffer, object_spec, false)? {
|
||||||
|
ingress::ObjectRecordRead::Incomplete => Ok(None), // wait for more bytes
|
||||||
|
ingress::ObjectRecordRead::Complete(record) => { // a full activation arrived
|
||||||
|
let bytes: Vec<u8> = buffer.drain(..record.total_len).collect();
|
||||||
|
Ok(Some(IngressRecordBytes { bytes, object_id: record.object_id.0, .. }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom metrics through the datastream
|
||||||
|
|
||||||
|
Telemetry is a deliberately dumb pipe: producers tag bytes with a channel id,
|
||||||
|
nothing in the middle interprets the payload, and views are read-time
|
||||||
|
projections. To add a metric stream, define a type and implement one constant —
|
||||||
|
no schema registry to negotiate with:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// crates/datastream/src/record.rs
|
||||||
|
pub trait Record {
|
||||||
|
const CHANNEL: &'static str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// crates/data-plane/src/arena.rs — the arena publishes its own health this way
|
||||||
|
impl Record for ArenaSample {
|
||||||
|
const CHANNEL: &'static str = ARENA_SAMPLE_CHANNEL; // "mvp.arena"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Built-in dashboard
|
||||||
|
|
||||||
|
A read-only HTML/SSE dashboard renders the frames the datastream already
|
||||||
|
carries — workers, the actor roster, per-actor dossiers, fleet hardware. It sends
|
||||||
|
no control signals back, so the whole thing is one Axum router:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// crates/dashboard/src/server.rs
|
||||||
|
fn router(state: AppState) -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(root_page))
|
||||||
|
.route("/events", get(frame_stream)) // live frames over SSE
|
||||||
|
.route("/api/frames", get(recent_frames))
|
||||||
|
.route("/api/views", get(views_json))
|
||||||
|
.route("/api/view/{*path}", get(view_snapshot))
|
||||||
|
.route("/view/{*path}", get(view_page))
|
||||||
|
.with_state(state)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The workspace builds upward from a tiny core. Each crate is one responsibility:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
app["myelin<br/>node · orchestrator"]
|
||||||
|
driver["iroh-driver<br/>QUIC network driver"]
|
||||||
|
dist["distribution<br/>SWIM · registry · directory"]
|
||||||
|
dash["dashboard<br/>read-only view over datastream"]
|
||||||
|
proc["process<br/>managed external processes"]
|
||||||
|
dp["data-plane<br/>zero-copy byte movement · IPC · links"]
|
||||||
|
ds["datastream<br/>metrics / telemetry pipe"]
|
||||||
|
trans["transport<br/>codec · identity · crypto"]
|
||||||
|
core["swactor<br/>core runtime + std (OTP)<br/>ActorInterface · Ctx"]
|
||||||
|
|
||||||
|
app --> driver
|
||||||
|
app --> dist
|
||||||
|
app --> dp
|
||||||
|
app --> dash
|
||||||
|
app --> proc
|
||||||
|
driver --> dist
|
||||||
|
driver --> ds
|
||||||
|
dist --> trans
|
||||||
|
dist --> ds
|
||||||
|
dash --> ds
|
||||||
|
proc --> ds
|
||||||
|
ds --> trans
|
||||||
|
trans --> core
|
||||||
|
dp --> core
|
||||||
|
ds --> core
|
||||||
|
proc --> core
|
||||||
|
```
|
||||||
|
|
||||||
|
| Crate | Role |
|
||||||
|
| --- | --- |
|
||||||
|
| `swactor` (`src/`) | Core runtime: the actor trait, `Ctx`, scheduler, address map, channels |
|
||||||
|
| `crates/transport` | Codec registry, node identity, message signing — transport-agnostic |
|
||||||
|
| `crates/distribution` | Clustering: SWIM membership, naming, metadata + actor directory gossip |
|
||||||
|
| `crates/iroh-driver` | iroh/QUIC network driver with its pump/fanout tasks |
|
||||||
|
| `crates/datastream` | Metrics / telemetry pipe — nothing in the middle interprets payloads |
|
||||||
|
| `crates/data-plane` | Zero-copy movement of large typed byte objects over IPC or a network link |
|
||||||
|
| `crates/process` | Managed external processes and YAML pipelines |
|
||||||
|
| `crates/dashboard` | Read-only HTML/SSE dashboard over datastream frames |
|
||||||
|
| `crates/provisioning` | Cloud node provisioning |
|
||||||
|
| `crates/bindings/{python,wasm-runtime,wasm-crypto}` | Language / target bindings |
|
||||||
|
| `apps/myelin` | The node binary that wires the above into a runnable cluster node |
|
||||||
|
| `tools/vastai` | Vast.ai GPU-marketplace tooling |
|
||||||
|
|
||||||
|
## Used for
|
||||||
|
|
||||||
|
- **A drop-in replacement for GPU jobs on cloud infrastructure.** `apps/myelin`
|
||||||
|
ships an orchestrator plus a containerized worker node
|
||||||
|
(`apps/myelin/node-image`) that provisions, stages models, and runs them.
|
||||||
|
- **Orchestration for sharded inference.** The node image includes a
|
||||||
|
[tinygrad](https://github.com/tinygrad/tinygrad) worker
|
||||||
|
(`apps/myelin/node-image/tinygrad_worker.py`); a vLLM backend is planned.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
**Working:** [`examples/ping-pong`](examples/ping-pong) — two actors volley on a
|
||||||
|
single-threaded runtime compiled to WebAssembly, driven tick-by-tick from Node.
|
||||||
|
Shows spawning, message passing, and death monitoring in one file.
|
||||||
|
|
||||||
|
**Planned** (tracked in `.deployment-notes/ROADMAP.md`):
|
||||||
|
|
||||||
|
- Two actors talking in-process, over IPC, and across a WAN — in Rust, Python,
|
||||||
|
and WASM — from the same code.
|
||||||
|
- Typed chunk streaming across IPC or WAN.
|
||||||
|
- A non-trivially sharded training job (not easily expressed in Ray).
|
||||||
|
- A full pipeline-parallel example of a model sharded across N ≥ 3 cheap GPUs.
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Requires the pinned nightly toolchain (`rust-toolchain.toml`):
|
||||||
|
|
||||||
|
```sh
|
||||||
cargo check --workspace
|
cargo check --workspace
|
||||||
cargo xtask test
|
cargo xtask test # see `cargo xtask --help`
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The language bindings (`crates/bindings/*`) build on demand with `-p` /
|
||||||
|
`--workspace`, not during normal native iteration.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
swactor is early and under active development. The runtime, clustering, and the
|
||||||
|
`myelin` node are exercised by the workspace test suite, but APIs are still
|
||||||
|
moving.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under the [GNU Affero General Public License v3.0 only]
|
||||||
|
(./LICENSE) (`AGPL-3.0-only`). The AGPL's network clause (§13) requires anyone
|
||||||
|
who runs a modified swactor as a network service to offer its source to users.
|
||||||
|
The copyright holder is not bound by their own license and may relicense future
|
||||||
|
releases freely; external contributors should expect a CLA requirement before
|
||||||
|
changes are merged.
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
# MVP node CODE image — thin layer over the CUDA/tinygrad base.
|
|
||||||
# Build from workspace root after compiling the Rust binary:
|
|
||||||
# cargo build --release -p mvp-system --bin mvp-worker-node
|
|
||||||
# docker build -f apps/mvp-node/Dockerfile.base -t swactor-mvp-node-base:cuda12.6 .
|
|
||||||
# docker build -f apps/mvp-node/Dockerfile --build-arg BASE_IMAGE=swactor-mvp-node-base:cuda12.6 -t swactor-mvp-node:latest .
|
|
||||||
|
|
||||||
ARG BASE_IMAGE=swactor-mvp-node-base:cuda12.6
|
|
||||||
ARG MVP_NODE_BIN=target/release/mvp-worker-node
|
|
||||||
FROM ${BASE_IMAGE}
|
|
||||||
ARG MVP_NODE_BIN=target/release/mvp-worker-node
|
|
||||||
|
|
||||||
COPY ${MVP_NODE_BIN} /usr/local/bin/mvp-node
|
|
||||||
COPY apps/mvp-node/tinygrad_worker.py /usr/local/share/mvp/tinygrad_worker.py
|
|
||||||
|
|
||||||
RUN chmod +x /usr/local/bin/mvp-node /usr/local/share/mvp/tinygrad_worker.py
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
BASE_IMAGE=${BASE_IMAGE:-swactor-mvp-node-base:cuda12.6}
|
|
||||||
IMAGE=${IMAGE:-swactor-mvp-node:latest}
|
|
||||||
CONTAINER=${CONTAINER:-swactor-mvp-node-e2e-$$}
|
|
||||||
GPUS=${MVP_CUDA_GPUS:-all}
|
|
||||||
PROMPT=${MVP_NODE_SELF_TEST_PROMPT:-ping}
|
|
||||||
TIMEOUT_SECS=${MVP_NODE_E2E_TIMEOUT_SECS:-1800}
|
|
||||||
FRAME_LOG=${MVP_DATASTREAM_FRAME_LOG:-/var/log/mvp-datastream.ndjson}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
cargo build --release -p mvp-system --bin mvp-worker-node
|
|
||||||
docker build -f apps/mvp-node/Dockerfile.base -t "$BASE_IMAGE" .
|
|
||||||
docker build -f apps/mvp-node/Dockerfile --build-arg BASE_IMAGE="$BASE_IMAGE" -t "$IMAGE" .
|
|
||||||
|
|
||||||
docker run -d \
|
|
||||||
--name "$CONTAINER" \
|
|
||||||
--gpus "$GPUS" \
|
|
||||||
-e MVP_NODE_SELF_TEST_PROMPT="$PROMPT" \
|
|
||||||
-e MVP_NODE_MAX_RUNTIME_SECS=1 \
|
|
||||||
-e MVP_SELF_TEST_MAX_TOKENS="${MVP_SELF_TEST_MAX_TOKENS:-1}" \
|
|
||||||
-e MVP_MODEL_CACHE_DIR=/var/cache/mvp-models \
|
|
||||||
-e MVP_DATASTREAM_FRAME_LOG="$FRAME_LOG" \
|
|
||||||
${HF_TOKEN:+-e HF_TOKEN="$HF_TOKEN"} \
|
|
||||||
"$IMAGE" >/dev/null
|
|
||||||
|
|
||||||
deadline=$((SECONDS + TIMEOUT_SECS))
|
|
||||||
while (( SECONDS < deadline )); do
|
|
||||||
logs=$(docker logs "$CONTAINER" 2>&1 || true)
|
|
||||||
if grep -q '"type":"ready"' <<<"$logs" && grep -q '"type":"self_test_completed"' <<<"$logs"; then
|
|
||||||
frames=$(docker exec "$CONTAINER" cat "$FRAME_LOG" 2>/dev/null || true)
|
|
||||||
if grep -q '"channel":"mvp.node.ready"' <<<"$frames" &&
|
|
||||||
grep -q '"channel":"mvp.worker.weights"' <<<"$frames" &&
|
|
||||||
grep -q '"channel":"mvp.worker.prompt"' <<<"$frames"; then
|
|
||||||
printf '%s\n' "$logs"
|
|
||||||
printf '%s\n' "$frames"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if grep -q 'WorkerFatal\|mvp-node: .*failed\|ModelLoadFailed\|GgufDownloadFailed' <<<"$logs"; then
|
|
||||||
printf '%s\n' "$logs" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep 5
|
|
||||||
done
|
|
||||||
|
|
||||||
docker logs "$CONTAINER" 2>&1 || true
|
|
||||||
echo "mvp-node Docker E2E timed out after ${TIMEOUT_SECS}s" >&2
|
|
||||||
exit 1
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -u
|
|
||||||
|
|
||||||
if ! mkdir /run/mvp_entrypoint.lock 2>/dev/null; then
|
|
||||||
echo "mvp-entrypoint: already running (lock held); parking" >&2
|
|
||||||
exec sleep infinity
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p /root/.ssh && chmod 700 /root/.ssh
|
|
||||||
: > /root/.ssh/authorized_keys
|
|
||||||
if [ -n "${PUBLIC_KEY:-}" ]; then printf '%s\n' "$PUBLIC_KEY" >> /root/.ssh/authorized_keys; fi
|
|
||||||
if [ -n "${SSH_PUBLIC_KEY:-}" ]; then printf '%s\n' "$SSH_PUBLIC_KEY" >> /root/.ssh/authorized_keys; fi
|
|
||||||
if [ -f /etc/mvp_deploy_key.pub ]; then cat /etc/mvp_deploy_key.pub >> /root/.ssh/authorized_keys; fi
|
|
||||||
chmod 600 /root/.ssh/authorized_keys
|
|
||||||
|
|
||||||
if [ ! -s /root/.ssh/authorized_keys ]; then
|
|
||||||
echo "mvp-entrypoint: WARNING no SSH public key found (PUBLIC_KEY/SSH_PUBLIC_KEY unset, no baked key); SSH will reject logins" >&2
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p /run/sshd /var/log /var/cache/mvp-models
|
|
||||||
ssh-keygen -A 2>/dev/null || true
|
|
||||||
/usr/sbin/sshd -e
|
|
||||||
echo "mvp-entrypoint: sshd up on :22" >&2
|
|
||||||
|
|
||||||
NODE_LOG=/var/log/mvp-node.log
|
|
||||||
echo "mvp-entrypoint: launching mvp-node (log -> $NODE_LOG)" >&2
|
|
||||||
set -o pipefail
|
|
||||||
/usr/local/bin/mvp-node "$@" 2>&1 | tee "$NODE_LOG"
|
|
||||||
code=${PIPESTATUS[0]}
|
|
||||||
|
|
||||||
echo "mvp-entrypoint: mvp-node exited with code $code; NOT restarting (node held for postmortem)" >&2
|
|
||||||
echo "mvp-entrypoint: --- last 80 lines of $NODE_LOG ---" >&2
|
|
||||||
tail -n 80 "$NODE_LOG" >&2 || true
|
|
||||||
exec sleep infinity
|
|
||||||
48
apps/myelin/Cargo.toml
Normal file
48
apps/myelin/Cargo.toml
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
[package]
|
||||||
|
name = "myelin"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
publish = false
|
||||||
|
autobins = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
dashboard = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
datastream = { path = "../../crates/datastream" }
|
||||||
|
data-plane = { path = "../../crates/data-plane" }
|
||||||
|
provisioning = { path = "../../crates/provisioning" }
|
||||||
|
dashboard = { path = "../../crates/dashboard" }
|
||||||
|
serde_json = "1"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
swactor = { path = "../..", features = ["serde", "transport"] }
|
||||||
|
swactor-transport = { path = "../../crates/transport" }
|
||||||
|
swactor-process = { path = "../../crates/process" }
|
||||||
|
distribution = { path = "../../crates/distribution" }
|
||||||
|
iroh-driver = { path = "../../crates/iroh-driver" }
|
||||||
|
iroh = "0.98"
|
||||||
|
tokio.workspace = true
|
||||||
|
swactor-vastai = { path = "../../tools/vastai" }
|
||||||
|
parking_lot = "0.12"
|
||||||
|
blake3 = "1"
|
||||||
|
toml = "0.8"
|
||||||
|
ureq = "2"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
libc = "0.2"
|
||||||
|
signal-hook = "0.3"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "myelin-worker"
|
||||||
|
path = "src/bin/worker_node.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "myelin-orchestrator"
|
||||||
|
path = "src/bin/orchestrator.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "myelin-chat"
|
||||||
|
path = "src/bin/chat.rs"
|
||||||
|
|
||||||
15
apps/myelin/node-image/Dockerfile
Normal file
15
apps/myelin/node-image/Dockerfile
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Myelin node CODE image — thin layer over the CUDA/tinygrad base.
|
||||||
|
# Build from workspace root after compiling the Rust binary:
|
||||||
|
# cargo build --release -p myelin --bin myelin-worker
|
||||||
|
# docker build -f apps/myelin/node-image/Dockerfile.base -t myelin-node-base:cuda12.6 .
|
||||||
|
# docker build -f apps/myelin/node-image/Dockerfile --build-arg BASE_IMAGE=myelin-node-base:cuda12.6 -t myelin-node:latest .
|
||||||
|
|
||||||
|
ARG BASE_IMAGE=myelin-node-base:cuda12.6
|
||||||
|
ARG MYELIN_NODE_BIN=target/release/myelin-worker
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
ARG MYELIN_NODE_BIN=target/release/myelin-worker
|
||||||
|
|
||||||
|
COPY ${MYELIN_NODE_BIN} /usr/local/bin/myelin-node
|
||||||
|
COPY apps/myelin/node-image/tinygrad_worker.py /usr/local/share/myelin/tinygrad_worker.py
|
||||||
|
|
||||||
|
RUN chmod +x /usr/local/bin/myelin-node /usr/local/share/myelin/tinygrad_worker.py
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# MVP node BASE image — CUDA/Python/tinygrad/sshd/PID-1 foundation.
|
# Myelin node BASE image — CUDA/Python/tinygrad/sshd/PID-1 foundation.
|
||||||
# Build from workspace root:
|
# Build from workspace root:
|
||||||
# docker build -f apps/mvp-node/Dockerfile.base -t swactor-mvp-node-base:cuda12.6 .
|
# docker build -f apps/myelin/node-image/Dockerfile.base -t myelin-node-base:cuda12.6 .
|
||||||
|
|
||||||
FROM nvidia/cuda:12.6.3-runtime-ubuntu24.04 AS builder
|
FROM nvidia/cuda:12.6.3-runtime-ubuntu24.04 AS builder
|
||||||
|
|
||||||
|
|
@ -13,17 +13,17 @@ RUN apt-get update && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN python3 -m pip install --no-cache-dir --break-system-packages \
|
RUN python3 -m pip install --no-cache-dir --break-system-packages \
|
||||||
--target=/opt/mvp-pydeps \
|
--target=/opt/myelin-pydeps \
|
||||||
tinygrad==0.12.0 numpy && \
|
tinygrad==0.12.0 numpy && \
|
||||||
find /opt/mvp-pydeps -depth -type d \
|
find /opt/myelin-pydeps -depth -type d \
|
||||||
\( -name '__pycache__' -o -name 'tests' -o -name 'test' \) \
|
\( -name '__pycache__' -o -name 'tests' -o -name 'test' \) \
|
||||||
-exec rm -rf {} + && \
|
-exec rm -rf {} + && \
|
||||||
find /opt/mvp-pydeps -name '*.pyc' -delete && \
|
find /opt/myelin-pydeps -name '*.pyc' -delete && \
|
||||||
find /opt/mvp-pydeps -type d -name '*.dist-info' -exec rm -rf {} +
|
find /opt/myelin-pydeps -type d -name '*.dist-info' -exec rm -rf {} +
|
||||||
|
|
||||||
RUN mkdir -p /opt/mvp-nvrtc-include && \
|
RUN mkdir -p /opt/myelin-nvrtc-include && \
|
||||||
cp -rL /usr/local/cuda/include/. /opt/mvp-nvrtc-include/ && \
|
cp -rL /usr/local/cuda/include/. /opt/myelin-nvrtc-include/ && \
|
||||||
test -f /opt/mvp-nvrtc-include/vector_types.h
|
test -f /opt/myelin-nvrtc-include/vector_types.h
|
||||||
|
|
||||||
FROM nvidia/cuda:12.6.3-base-ubuntu24.04 AS runtime
|
FROM nvidia/cuda:12.6.3-base-ubuntu24.04 AS runtime
|
||||||
|
|
||||||
|
|
@ -48,21 +48,21 @@ RUN apt-get update && \
|
||||||
/usr/share/info/* && \
|
/usr/share/info/* && \
|
||||||
find /usr -depth -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true && \
|
find /usr -depth -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true && \
|
||||||
find /usr -type f -name '*.pyc' -delete 2>/dev/null || true && \
|
find /usr -type f -name '*.pyc' -delete 2>/dev/null || true && \
|
||||||
mkdir -p /usr/local/share/mvp /var/cache/mvp-models /var/log
|
mkdir -p /usr/local/share/myelin /var/cache/myelin-models /var/log
|
||||||
|
|
||||||
COPY --from=builder /opt/mvp-nvrtc-include/ /usr/local/cuda/include/
|
COPY --from=builder /opt/myelin-nvrtc-include/ /usr/local/cuda/include/
|
||||||
COPY --from=builder /opt/mvp-pydeps /opt/mvp-pydeps
|
COPY --from=builder /opt/myelin-pydeps /opt/myelin-pydeps
|
||||||
ENV PYTHONPATH=/opt/mvp-pydeps
|
ENV PYTHONPATH=/opt/myelin-pydeps
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
ENV CUDA=1
|
ENV CUDA=1
|
||||||
ENV DEV=CUDA
|
ENV DEV=CUDA
|
||||||
ENV MVP_MODEL_CACHE_DIR=/var/cache/mvp-models
|
ENV MYELIN_MODEL_CACHE_DIR=/var/cache/myelin-models
|
||||||
ENV MVP_TINYGRAD_WORKER=/usr/local/share/mvp/tinygrad_worker.py
|
ENV MYELIN_TINYGRAD_WORKER=/usr/local/share/myelin/tinygrad_worker.py
|
||||||
|
|
||||||
COPY apps/mvp-node/mvp_entrypoint.sh /usr/local/bin/mvp_entrypoint.sh
|
COPY apps/myelin/node-image/myelin_entrypoint.sh /usr/local/bin/myelin_entrypoint.sh
|
||||||
RUN chmod +x /usr/local/bin/mvp_entrypoint.sh
|
RUN chmod +x /usr/local/bin/myelin_entrypoint.sh
|
||||||
|
|
||||||
ARG DEPLOY_PUBKEY=""
|
ARG DEPLOY_PUBKEY=""
|
||||||
RUN if [ -n "$DEPLOY_PUBKEY" ]; then printf '%s\n' "$DEPLOY_PUBKEY" > /etc/mvp_deploy_key.pub; fi
|
RUN if [ -n "$DEPLOY_PUBKEY" ]; then printf '%s\n' "$DEPLOY_PUBKEY" > /etc/myelin_deploy_key.pub; fi
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/mvp_entrypoint.sh"]
|
ENTRYPOINT ["/usr/local/bin/myelin_entrypoint.sh"]
|
||||||
54
apps/myelin/node-image/local_docker_e2e.sh
Executable file
54
apps/myelin/node-image/local_docker_e2e.sh
Executable file
|
|
@ -0,0 +1,54 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE_IMAGE=${BASE_IMAGE:-myelin-node-base:cuda12.6}
|
||||||
|
IMAGE=${IMAGE:-myelin-node:latest}
|
||||||
|
CONTAINER=${CONTAINER:-myelin-node-e2e-$$}
|
||||||
|
GPUS=${MYELIN_CUDA_GPUS:-all}
|
||||||
|
PROMPT=${MYELIN_NODE_SELF_TEST_PROMPT:-ping}
|
||||||
|
TIMEOUT_SECS=${MYELIN_NODE_E2E_TIMEOUT_SECS:-1800}
|
||||||
|
FRAME_LOG=${MYELIN_DATASTREAM_FRAME_LOG:-/var/log/myelin-datastream.ndjson}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
cargo build --release -p myelin --bin myelin-worker
|
||||||
|
docker build -f apps/myelin/node-image/Dockerfile.base -t "$BASE_IMAGE" .
|
||||||
|
docker build -f apps/myelin/node-image/Dockerfile --build-arg BASE_IMAGE="$BASE_IMAGE" -t "$IMAGE" .
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name "$CONTAINER" \
|
||||||
|
--gpus "$GPUS" \
|
||||||
|
-e MYELIN_NODE_SELF_TEST_PROMPT="$PROMPT" \
|
||||||
|
-e MYELIN_NODE_MAX_RUNTIME_SECS=1 \
|
||||||
|
-e MYELIN_SELF_TEST_MAX_TOKENS="${MYELIN_SELF_TEST_MAX_TOKENS:-1}" \
|
||||||
|
-e MYELIN_MODEL_CACHE_DIR=/var/cache/myelin-models \
|
||||||
|
-e MYELIN_DATASTREAM_FRAME_LOG="$FRAME_LOG" \
|
||||||
|
${HF_TOKEN:+-e HF_TOKEN="$HF_TOKEN"} \
|
||||||
|
"$IMAGE" >/dev/null
|
||||||
|
|
||||||
|
deadline=$((SECONDS + TIMEOUT_SECS))
|
||||||
|
while (( SECONDS < deadline )); do
|
||||||
|
logs=$(docker logs "$CONTAINER" 2>&1 || true)
|
||||||
|
if grep -q '"type":"ready"' <<<"$logs" && grep -q '"type":"self_test_completed"' <<<"$logs"; then
|
||||||
|
frames=$(docker exec "$CONTAINER" cat "$FRAME_LOG" 2>/dev/null || true)
|
||||||
|
if grep -q '"channel":"myelin.node.ready"' <<<"$frames" &&
|
||||||
|
grep -q '"channel":"myelin.worker.weights"' <<<"$frames" &&
|
||||||
|
grep -q '"channel":"myelin.worker.prompt"' <<<"$frames"; then
|
||||||
|
printf '%s\n' "$logs"
|
||||||
|
printf '%s\n' "$frames"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if grep -q 'WorkerFatal\|myelin-node: .*failed\|ModelLoadFailed\|GgufDownloadFailed' <<<"$logs"; then
|
||||||
|
printf '%s\n' "$logs" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
docker logs "$CONTAINER" 2>&1 || true
|
||||||
|
echo "myelin-node Docker E2E timed out after ${TIMEOUT_SECS}s" >&2
|
||||||
|
exit 1
|
||||||
34
apps/myelin/node-image/myelin_entrypoint.sh
Executable file
34
apps/myelin/node-image/myelin_entrypoint.sh
Executable file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -u
|
||||||
|
|
||||||
|
if ! mkdir /run/myelin_entrypoint.lock 2>/dev/null; then
|
||||||
|
echo "myelin-entrypoint: already running (lock held); parking" >&2
|
||||||
|
exec sleep infinity
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p /root/.ssh && chmod 700 /root/.ssh
|
||||||
|
: > /root/.ssh/authorized_keys
|
||||||
|
if [ -n "${PUBLIC_KEY:-}" ]; then printf '%s\n' "$PUBLIC_KEY" >> /root/.ssh/authorized_keys; fi
|
||||||
|
if [ -n "${SSH_PUBLIC_KEY:-}" ]; then printf '%s\n' "$SSH_PUBLIC_KEY" >> /root/.ssh/authorized_keys; fi
|
||||||
|
if [ -f /etc/myelin_deploy_key.pub ]; then cat /etc/myelin_deploy_key.pub >> /root/.ssh/authorized_keys; fi
|
||||||
|
chmod 600 /root/.ssh/authorized_keys
|
||||||
|
|
||||||
|
if [ ! -s /root/.ssh/authorized_keys ]; then
|
||||||
|
echo "myelin-entrypoint: WARNING no SSH public key found (PUBLIC_KEY/SSH_PUBLIC_KEY unset, no baked key); SSH will reject logins" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p /run/sshd /var/log /var/cache/myelin-models
|
||||||
|
ssh-keygen -A 2>/dev/null || true
|
||||||
|
/usr/sbin/sshd -e
|
||||||
|
echo "myelin-entrypoint: sshd up on :22" >&2
|
||||||
|
|
||||||
|
NODE_LOG=/var/log/myelin-node.log
|
||||||
|
echo "myelin-entrypoint: launching myelin-node (log -> $NODE_LOG)" >&2
|
||||||
|
set -o pipefail
|
||||||
|
/usr/local/bin/myelin-node "$@" 2>&1 | tee "$NODE_LOG"
|
||||||
|
code=${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
echo "myelin-entrypoint: myelin-node exited with code $code; NOT restarting (node held for postmortem)" >&2
|
||||||
|
echo "myelin-entrypoint: --- last 80 lines of $NODE_LOG ---" >&2
|
||||||
|
tail -n 80 "$NODE_LOG" >&2 || true
|
||||||
|
exec sleep infinity
|
||||||
|
|
@ -107,8 +107,8 @@ def start_cpu_line_sampler(
|
||||||
request_id: int | None,
|
request_id: int | None,
|
||||||
model_id: str | None,
|
model_id: str | None,
|
||||||
) -> CpuLineSampler | None:
|
) -> CpuLineSampler | None:
|
||||||
raw = os.environ.get("MVP_CPU_LINE_PROFILE")
|
raw = os.environ.get("MYELIN_CPU_LINE_PROFILE")
|
||||||
if not env_flag("MVP_CPU_LINE_PROFILE", False):
|
if not env_flag("MYELIN_CPU_LINE_PROFILE", False):
|
||||||
control(
|
control(
|
||||||
type="CpuLineProfileSkipped",
|
type="CpuLineProfileSkipped",
|
||||||
phase=phase,
|
phase=phase,
|
||||||
|
|
@ -117,7 +117,7 @@ def start_cpu_line_sampler(
|
||||||
env_value=raw,
|
env_value=raw,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
interval_ms = float(os.environ.get("MVP_CPU_LINE_PROFILE_INTERVAL_MS", "2"))
|
interval_ms = float(os.environ.get("MYELIN_CPU_LINE_PROFILE_INTERVAL_MS", "2"))
|
||||||
interval_secs = max(0.0005, interval_ms / 1000.0)
|
interval_secs = max(0.0005, interval_ms / 1000.0)
|
||||||
control(
|
control(
|
||||||
type="CpuLineProfileStarted",
|
type="CpuLineProfileStarted",
|
||||||
|
|
@ -158,7 +158,7 @@ def benchmark_stamp() -> dict[str, Any]:
|
||||||
"component": "tinygrad-worker",
|
"component": "tinygrad-worker",
|
||||||
"producer_component": "tinygrad-worker",
|
"producer_component": "tinygrad-worker",
|
||||||
"producer_instance_id": os.environ.get(
|
"producer_instance_id": os.environ.get(
|
||||||
"MVP_BENCHMARK_PRODUCER_INSTANCE",
|
"MYELIN_BENCHMARK_PRODUCER_INSTANCE",
|
||||||
f"tinygrad-worker:{pid}",
|
f"tinygrad-worker:{pid}",
|
||||||
),
|
),
|
||||||
"producer_process_id": pid,
|
"producer_process_id": pid,
|
||||||
|
|
@ -190,12 +190,12 @@ def datastream_endpoint_snapshot() -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"role": "python-worker-stdio-json-bridge",
|
"role": "python-worker-stdio-json-bridge",
|
||||||
"transport": "stdout-json-lines",
|
"transport": "stdout-json-lines",
|
||||||
"endpoint_identity": os.environ.get("MVP_DATASTREAM_ENDPOINT_ID", "worker-stdio-bridge"),
|
"endpoint_identity": os.environ.get("MYELIN_DATASTREAM_ENDPOINT_ID", "worker-stdio-bridge"),
|
||||||
"configured_source": "worker-node-env",
|
"configured_source": "worker-node-env",
|
||||||
"resolved_source": "TinygradWorker::spawn environment",
|
"resolved_source": "TinygradWorker::spawn environment",
|
||||||
"authentication_present": False,
|
"authentication_present": False,
|
||||||
"tls_present": False,
|
"tls_present": False,
|
||||||
"relay_mode": os.environ.get("MVP_IROH_RELAY_MODE"),
|
"relay_mode": os.environ.get("MYELIN_IROH_RELAY_MODE"),
|
||||||
"endpoint_addr_mask": os.environ.get("MVP_IROH_ENDPOINT_ADDR_MASK"),
|
"endpoint_addr_mask": os.environ.get("MVP_IROH_ENDPOINT_ADDR_MASK"),
|
||||||
"connectivity_result": "configured",
|
"connectivity_result": "configured",
|
||||||
}
|
}
|
||||||
|
|
@ -225,17 +225,17 @@ def apply_canonical_envelope(event: dict[str, Any]) -> None:
|
||||||
|
|
||||||
def control(**event: Any) -> None:
|
def control(**event: Any) -> None:
|
||||||
apply_canonical_envelope(event)
|
apply_canonical_envelope(event)
|
||||||
if (run_id := env_int("MVP_RUN_ID")) is not None:
|
if (run_id := env_int("MYELIN_RUN_ID")) is not None:
|
||||||
event.setdefault("run_id", run_id)
|
event.setdefault("run_id", run_id)
|
||||||
if (node_id := env_int("MVP_LOGICAL_NODE_ID")) is not None:
|
if (node_id := env_int("MYELIN_LOGICAL_NODE_ID")) is not None:
|
||||||
event.setdefault("node_id", node_id)
|
event.setdefault("node_id", node_id)
|
||||||
if (stage_index := env_int("MVP_STAGE_INDEX")) is not None:
|
if (stage_index := env_int("MYELIN_STAGE_INDEX")) is not None:
|
||||||
event.setdefault("stage_index", stage_index)
|
event.setdefault("stage_index", stage_index)
|
||||||
print(json.dumps(event, separators=(",", ":")), flush=True)
|
print(json.dumps(event, separators=(",", ":")), flush=True)
|
||||||
|
|
||||||
|
|
||||||
def log(message: str) -> None:
|
def log(message: str) -> None:
|
||||||
print(f"mvp_tinygrad_worker: {message}", file=sys.stderr, flush=True)
|
print(f"myelin_tinygrad_worker: {message}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
|
||||||
def fatal(reason: str, **fields: Any) -> None:
|
def fatal(reason: str, **fields: Any) -> None:
|
||||||
|
|
@ -273,9 +273,9 @@ def initialize(cmd: dict[str, Any]) -> None:
|
||||||
fatal("UnsupportedHelperAbi", helper_abi_version=cmd.get("helper_abi_version"))
|
fatal("UnsupportedHelperAbi", helper_abi_version=cmd.get("helper_abi_version"))
|
||||||
requested_device = str(cmd.get("backend", {}).get("device") or os.environ.get("DEV") or "CUDA")
|
requested_device = str(cmd.get("backend", {}).get("device") or os.environ.get("DEV") or "CUDA")
|
||||||
device = select_tinygrad_device(requested_device)
|
device = select_tinygrad_device(requested_device)
|
||||||
arena_fd = os.environ.get("MVP_ARENA_FD")
|
arena_fd = os.environ.get("MYELIN_ARENA_FD")
|
||||||
if arena_fd is not None:
|
if arena_fd is not None:
|
||||||
arena_bytes = int(os.environ.get("MVP_ARENA_BYTES", "0") or "0")
|
arena_bytes = int(os.environ.get("MYELIN_ARENA_BYTES", "0") or "0")
|
||||||
if arena_bytes > 0:
|
if arena_bytes > 0:
|
||||||
arena = mmap.mmap(int(arena_fd), arena_bytes)
|
arena = mmap.mmap(int(arena_fd), arena_bytes)
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
|
|
@ -311,8 +311,8 @@ def configure_role(cmd: dict[str, Any]) -> None:
|
||||||
|
|
||||||
|
|
||||||
def cache_root() -> Path:
|
def cache_root() -> Path:
|
||||||
raw = os.environ.get("MVP_MODEL_CACHE_DIR", "").strip()
|
raw = os.environ.get("MYELIN_MODEL_CACHE_DIR", "").strip()
|
||||||
root = Path(raw).expanduser() if raw else Path.home() / ".cache" / "mvp-node"
|
root = Path(raw).expanduser() if raw else Path.home() / ".cache" / "myelin-node"
|
||||||
root.mkdir(parents=True, exist_ok=True)
|
root.mkdir(parents=True, exist_ok=True)
|
||||||
return root
|
return root
|
||||||
|
|
||||||
|
|
@ -351,7 +351,7 @@ def cache_path_for(url: str) -> Path:
|
||||||
|
|
||||||
|
|
||||||
def request_headers() -> dict[str, str]:
|
def request_headers() -> dict[str, str]:
|
||||||
headers = {"User-Agent": "swactor-mvp-node/0.1"}
|
headers = {"User-Agent": "myelin-node/0.1"}
|
||||||
token = os.environ.get("HF_TOKEN", "").strip()
|
token = os.environ.get("HF_TOKEN", "").strip()
|
||||||
if token:
|
if token:
|
||||||
headers["Authorization"] = f"Bearer {token}"
|
headers["Authorization"] = f"Bearer {token}"
|
||||||
|
|
@ -392,7 +392,7 @@ def fetch_whole(source: dict[str, Any]) -> Path:
|
||||||
out.write(chunk)
|
out.write(chunk)
|
||||||
done += len(chunk)
|
done += len(chunk)
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
if now - last_event >= float(os.environ.get("MVP_DOWNLOAD_PROGRESS_SECS", "5")):
|
if now - last_event >= float(os.environ.get("MYELIN_DOWNLOAD_PROGRESS_SECS", "5")):
|
||||||
control(
|
control(
|
||||||
type="GgufDownloadProgress",
|
type="GgufDownloadProgress",
|
||||||
bytes_done=done,
|
bytes_done=done,
|
||||||
|
|
@ -672,7 +672,7 @@ def load_weights(cmd: dict[str, Any]) -> None:
|
||||||
llm_backend = "tinygrad.apps.llm"
|
llm_backend = "tinygrad.apps.llm"
|
||||||
|
|
||||||
control(type="TinygradLlmImportReady", model_id=model_id, backend=llm_backend)
|
control(type="TinygradLlmImportReady", model_id=model_id, backend=llm_backend)
|
||||||
max_context_raw = os.environ.get("MVP_MAX_CONTEXT", "512")
|
max_context_raw = os.environ.get("MYELIN_MAX_CONTEXT", "512")
|
||||||
max_context = int(max_context_raw) if max_context_raw else 512
|
max_context = int(max_context_raw) if max_context_raw else 512
|
||||||
control(
|
control(
|
||||||
type="PipelineStageFromGgufStarted",
|
type="PipelineStageFromGgufStarted",
|
||||||
|
|
@ -744,7 +744,7 @@ def load_weights(cmd: dict[str, Any]) -> None:
|
||||||
|
|
||||||
|
|
||||||
def prompt_template_name() -> str:
|
def prompt_template_name() -> str:
|
||||||
explicit = os.environ.get("MVP_PROMPT_TEMPLATE")
|
explicit = os.environ.get("MYELIN_PROMPT_TEMPLATE")
|
||||||
if explicit is not None:
|
if explicit is not None:
|
||||||
return explicit.strip().lower()
|
return explicit.strip().lower()
|
||||||
tokenizer_pre = str(loaded.get("tokenizer_pre", "")).lower()
|
tokenizer_pre = str(loaded.get("tokenizer_pre", "")).lower()
|
||||||
|
|
@ -911,7 +911,7 @@ def infer_prompt(cmd: dict[str, Any]) -> None:
|
||||||
prompt_tokens=len(prompt_tokens),
|
prompt_tokens=len(prompt_tokens),
|
||||||
elapsed_ms=int((time.monotonic() - encode_started) * 1000),
|
elapsed_ms=int((time.monotonic() - encode_started) * 1000),
|
||||||
)
|
)
|
||||||
progress_every = int(os.environ.get("MVP_TOKEN_PROGRESS_EVERY", "16") or "16")
|
progress_every = int(os.environ.get("MYELIN_TOKEN_PROGRESS_EVERY", "16") or "16")
|
||||||
decode_started = time.monotonic()
|
decode_started = time.monotonic()
|
||||||
control(
|
control(
|
||||||
type="DecodeStarted",
|
type="DecodeStarted",
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
# MVP System Specification
|
# Myelin System Specification
|
||||||
***STALE! FOR HISTORICAL REFERENCE ONLY***
|
***STALE! FOR HISTORICAL REFERENCE ONLY***
|
||||||
|
|
||||||
**Status:** draft consolidated system specification.
|
Id: 9
|
||||||
|
Last modified:
|
||||||
|
Last reviewed:
|
||||||
|
|
||||||
This document is the single MVP reference for the swactor GGUF pipeline system.
|
This document is the single Myelin reference for the swactor GGUF pipeline system.
|
||||||
It folds the system behavior previously split across the orchestration,
|
It folds the system behavior previously split across the orchestration,
|
||||||
ring/backpressure, and GPU-worker drafts into one end-to-end specification.
|
ring/backpressure, and GPU-worker drafts into one end-to-end specification.
|
||||||
|
|
||||||
The focused root specs may remain as design history while this document is being
|
The focused root specs may remain as design history while this document is being
|
||||||
stabilized. For the MVP drafting process, this document is the place where the
|
stabilized. For the Myelin drafting process, this document is the place where the
|
||||||
complete system, its components, guarantees, and systemic behavior are described
|
complete system, its components, guarantees, and systemic behavior are described
|
||||||
together.
|
together.
|
||||||
|
|
||||||
|
|
@ -16,7 +18,7 @@ together.
|
||||||
|
|
||||||
## 1. Purpose
|
## 1. Purpose
|
||||||
|
|
||||||
The MVP is a trusted distributed ML runtime for one linear GGUF inference
|
The Myelin is a trusted distributed ML runtime for one linear GGUF inference
|
||||||
pipeline over a rented pool of GPU nodes.
|
pipeline over a rented pool of GPU nodes.
|
||||||
|
|
||||||
The first deployment shape is:
|
The first deployment shape is:
|
||||||
|
|
@ -36,7 +38,7 @@ operator provisions rented GPU nodes
|
||||||
```
|
```
|
||||||
|
|
||||||
The document is intentionally about the first complete feature, not a broad
|
The document is intentionally about the first complete feature, not a broad
|
||||||
graph engine. Later features can generalize from this system after the MVP
|
graph engine. Later features can generalize from this system after the Myelin
|
||||||
behavior is stable.
|
behavior is stable.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -100,7 +102,7 @@ no actor-level credit protocol, RTS/CTS exchange, or per-range acknowledgement.
|
||||||
One persistent iroh uni-stream carries all objects for one edge. The stream
|
One persistent iroh uni-stream carries all objects for one edge. The stream
|
||||||
starts with an `edge_id` preamble and then object records.
|
starts with an `edge_id` preamble and then object records.
|
||||||
|
|
||||||
The MVP permits transfer and device upload/download to overlap with network
|
The Myelin permits transfer and device upload/download to overlap with network
|
||||||
streaming. It does not permit compute to observe a logical object until the
|
streaming. It does not permit compute to observe a logical object until the
|
||||||
whole object has loaded.
|
whole object has loaded.
|
||||||
|
|
||||||
|
|
@ -146,7 +148,7 @@ its active run plan.
|
||||||
|
|
||||||
Other stages cannot redirect a node to a new peer, edge id, or layer range.
|
Other stages cannot redirect a node to a new peer, edge id, or layer range.
|
||||||
|
|
||||||
Resource inventory is known to the orchestrator before placement in the MVP. A
|
Resource inventory is known to the orchestrator before placement in the Myelin. A
|
||||||
node may report boot health and readiness, but there is no distributed
|
node may report boot health and readiness, but there is no distributed
|
||||||
capability negotiation protocol after boot.
|
capability negotiation protocol after boot.
|
||||||
|
|
||||||
|
|
@ -180,7 +182,7 @@ Python/tinygrad worker process
|
||||||
|
|
||||||
The orchestrator may run in its own container or on one of the provisioned GPU
|
The orchestrator may run in its own container or on one of the provisioned GPU
|
||||||
machines. It is a control participant and token endpoint. It does not run model
|
machines. It is a control participant and token endpoint. It does not run model
|
||||||
compute for the MVP.
|
compute for the Myelin.
|
||||||
|
|
||||||
When the orchestrator participates in token edges over the data plane, it has a
|
When the orchestrator participates in token edges over the data plane, it has a
|
||||||
stable `node_id`, an iroh endpoint, and local token edge endpoints like any
|
stable `node_id`, an iroh endpoint, and local token edge endpoints like any
|
||||||
|
|
@ -241,7 +243,7 @@ restart invalidates all prior handles, roles, rings, and in-flight steps.
|
||||||
|
|
||||||
## 7. System Lifecycle
|
## 7. System Lifecycle
|
||||||
|
|
||||||
The complete MVP lifecycle is:
|
The complete Myelin lifecycle is:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Deployment
|
Deployment
|
||||||
|
|
@ -316,7 +318,7 @@ No run edge should be provisioned to that node.
|
||||||
The orchestrator starts run planning only after the intended candidate pool is
|
The orchestrator starts run planning only after the intended candidate pool is
|
||||||
ready.
|
ready.
|
||||||
|
|
||||||
For the MVP, `PoolReady` means:
|
For the Myelin, `PoolReady` means:
|
||||||
|
|
||||||
- every node in the intended candidate pool is known to the orchestrator
|
- every node in the intended candidate pool is known to the orchestrator
|
||||||
- every candidate node is live in the SWIM membership view
|
- every candidate node is live in the SWIM membership view
|
||||||
|
|
@ -334,14 +336,14 @@ membership/liveness input to the orchestrator, not a distributed graph protocol.
|
||||||
If pool readiness is lost before a `RunPlan` is committed, the orchestrator keeps
|
If pool readiness is lost before a `RunPlan` is committed, the orchestrator keeps
|
||||||
waiting or aborts before provisioning according to local policy.
|
waiting or aborts before provisioning according to local policy.
|
||||||
|
|
||||||
If pool readiness is lost after provisioning begins, the run faults. The MVP does
|
If pool readiness is lost after provisioning begins, the run faults. The Myelin does
|
||||||
not re-place an active run after a node disappears.
|
not re-place an active run after a node disappears.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. GGUF Pipeline Workload
|
## 10. GGUF Pipeline Workload
|
||||||
|
|
||||||
The MVP workload is one linear pipeline:
|
The Myelin workload is one linear pipeline:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
orchestrator --tokens--> stage 0 --activations--> stage 1 --activations-->
|
orchestrator --tokens--> stage 0 --activations--> stage 1 --activations-->
|
||||||
|
|
@ -645,7 +647,7 @@ Stopped
|
||||||
terminal for this run
|
terminal for this run
|
||||||
```
|
```
|
||||||
|
|
||||||
The MVP allows one active `ExecuteStep` per stage.
|
The Myelin allows one active `ExecuteStep` per stage.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -752,7 +754,7 @@ ObjectHeader
|
||||||
payload bytes, exactly header.extent bytes
|
payload bytes, exactly header.extent bytes
|
||||||
```
|
```
|
||||||
|
|
||||||
The MVP header is fixed-size:
|
The Myelin header is fixed-size:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
struct ObjectHeader {
|
struct ObjectHeader {
|
||||||
|
|
@ -840,7 +842,7 @@ struct RingLayout {
|
||||||
pointers from its own arena mapping base plus arena offsets.
|
pointers from its own arena mapping base plus arena offsets.
|
||||||
|
|
||||||
The ring header and data region must be aligned for cross-process atomics and
|
The ring header and data region must be aligned for cross-process atomics and
|
||||||
backend copy requirements. Host pinning is optional for the MVP, but if a range
|
backend copy requirements. Host pinning is optional for the Myelin, but if a range
|
||||||
is pinned, it cannot be unpinned or returned to the free list until ring
|
is pinned, it cannot be unpinned or returned to the free list until ring
|
||||||
quiescence.
|
quiescence.
|
||||||
|
|
||||||
|
|
@ -1101,7 +1103,7 @@ The worker parses object records from the ring.
|
||||||
|
|
||||||
The stream is persistent for the edge. It is not opened per object.
|
The stream is persistent for the edge. It is not opened per object.
|
||||||
|
|
||||||
QUIC owns reliability and stream ordering. The MVP does not add app-level
|
QUIC owns reliability and stream ordering. The Myelin does not add app-level
|
||||||
fragment hashes, striping, resume, or per-object streams.
|
fragment hashes, striping, resume, or per-object streams.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -2010,7 +2012,7 @@ Run fault sources include:
|
||||||
Teardown failures are recorded in teardown outcome/status. They do not rewrite
|
Teardown failures are recorded in teardown outcome/status. They do not rewrite
|
||||||
a completed inference into a faulted run outcome.
|
a completed inference into a faulted run outcome.
|
||||||
|
|
||||||
MVP recovery policy is fail-stop at the run level. The orchestrator records the
|
Myelin recovery policy is fail-stop at the run level. The orchestrator records the
|
||||||
first run-level failure reason, stops injecting objects, and begins teardown. It
|
first run-level failure reason, stops injecting objects, and begins teardown. It
|
||||||
does not re-place the run.
|
does not re-place the run.
|
||||||
|
|
||||||
|
|
@ -2322,7 +2324,7 @@ stable.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 42. Canonical MVP Scenario
|
## 42. Canonical Myelin Scenario
|
||||||
|
|
||||||
The first end-to-end scenario is one "hello world" prompt over a rented GPU
|
The first end-to-end scenario is one "hello world" prompt over a rented GPU
|
||||||
pool:
|
pool:
|
||||||
3
apps/myelin/src/bin/chat.rs
Normal file
3
apps/myelin/src/bin/chat.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() -> std::process::ExitCode {
|
||||||
|
myelin::run_chat_from_args(std::env::args().skip(1))
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
match mvp_system::run_orchestrator_from_args(std::env::args().skip(1)) {
|
match myelin::run_orchestrator_from_args(std::env::args().skip(1)) {
|
||||||
Ok(()) => ExitCode::SUCCESS,
|
Ok(()) => ExitCode::SUCCESS,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("mvp-orchestrator: {error}");
|
eprintln!("myelin-orchestrator: {error}");
|
||||||
ExitCode::from(1)
|
ExitCode::from(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
mvp_system::run_worker_node_from_env()
|
myelin::run_worker_node_from_env()
|
||||||
}
|
}
|
||||||
4
apps/myelin/src/chat/mod.rs
Normal file
4
apps/myelin/src/chat/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
//! Myelin operator chat wrapper public surface.
|
||||||
|
|
||||||
|
mod node_image;
|
||||||
|
pub(super) mod runtime;
|
||||||
|
|
@ -8,19 +8,19 @@ use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
const NODE_IMAGE_CONTENT_INPUTS: &[&str] = &[
|
const NODE_IMAGE_CONTENT_INPUTS: &[&str] = &[
|
||||||
"apps/mvp-node/Dockerfile",
|
"apps/myelin/node-image/Dockerfile",
|
||||||
"apps/mvp-node/tinygrad_worker.py",
|
"apps/myelin/node-image/tinygrad_worker.py",
|
||||||
];
|
];
|
||||||
|
|
||||||
const BASE_IMAGE_SOURCE_INPUTS: &[&str] = &[
|
const BASE_IMAGE_SOURCE_INPUTS: &[&str] = &[
|
||||||
"apps/mvp-node/Dockerfile.base",
|
"apps/myelin/node-image/Dockerfile.base",
|
||||||
"apps/mvp-node/mvp_entrypoint.sh",
|
"apps/myelin/node-image/myelin_entrypoint.sh",
|
||||||
];
|
];
|
||||||
const NODE_IMAGE_TAG_LABEL: &str = "org.swactor.mvp.node-image-tag";
|
const NODE_IMAGE_TAG_LABEL: &str = "org.swactor.myelin.node-image-tag";
|
||||||
const NODE_IMAGE_SOURCE_HASH_LABEL: &str = "org.swactor.mvp.node.source-hash";
|
const NODE_IMAGE_SOURCE_HASH_LABEL: &str = "org.swactor.myelin.node.source-hash";
|
||||||
const NODE_IMAGE_WORKER_HASH_LABEL: &str = "org.swactor.mvp.node.worker-hash";
|
const NODE_IMAGE_WORKER_HASH_LABEL: &str = "org.swactor.myelin.node.worker-hash";
|
||||||
const NODE_IMAGE_BASE_HASH_LABEL: &str = "org.swactor.mvp.node.base-hash";
|
const NODE_IMAGE_BASE_HASH_LABEL: &str = "org.swactor.myelin.node.base-hash";
|
||||||
const BASE_IMAGE_SOURCE_HASH_LABEL: &str = "org.swactor.mvp.base.source-hash";
|
const BASE_IMAGE_SOURCE_HASH_LABEL: &str = "org.swactor.myelin.base.source-hash";
|
||||||
|
|
||||||
pub(super) struct NodeImageRequest {
|
pub(super) struct NodeImageRequest {
|
||||||
pub(super) requested_image: String,
|
pub(super) requested_image: String,
|
||||||
|
|
@ -110,11 +110,11 @@ fn prepare_node_image_inner(
|
||||||
"build",
|
"build",
|
||||||
"--quiet",
|
"--quiet",
|
||||||
"-p",
|
"-p",
|
||||||
"mvp-system",
|
"myelin",
|
||||||
"--bin",
|
"--bin",
|
||||||
"mvp-worker-node",
|
"myelin-worker",
|
||||||
],
|
],
|
||||||
"build mvp-worker-node",
|
"build myelin-worker",
|
||||||
None,
|
None,
|
||||||
progress,
|
progress,
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -128,7 +128,7 @@ fn prepare_node_image_inner(
|
||||||
&root,
|
&root,
|
||||||
vec![relative_path(
|
vec![relative_path(
|
||||||
&root,
|
&root,
|
||||||
&root.join("apps/mvp-node/tinygrad_worker.py"),
|
&root.join("apps/myelin/node-image/tinygrad_worker.py"),
|
||||||
)?],
|
)?],
|
||||||
&[],
|
&[],
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -176,14 +176,14 @@ fn prepare_node_image_inner(
|
||||||
&[
|
&[
|
||||||
"build",
|
"build",
|
||||||
"-f",
|
"-f",
|
||||||
"apps/mvp-node/Dockerfile.base",
|
"apps/myelin/node-image/Dockerfile.base",
|
||||||
"--label",
|
"--label",
|
||||||
base_source_hash_label.as_str(),
|
base_source_hash_label.as_str(),
|
||||||
"-t",
|
"-t",
|
||||||
request.base_image.as_str(),
|
request.base_image.as_str(),
|
||||||
".",
|
".",
|
||||||
],
|
],
|
||||||
"build mvp node base image",
|
"build myelin node base image",
|
||||||
Some(&request.base_image),
|
Some(&request.base_image),
|
||||||
progress,
|
progress,
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -200,11 +200,11 @@ fn prepare_node_image_inner(
|
||||||
let mut build_args = vec![
|
let mut build_args = vec![
|
||||||
"build".to_owned(),
|
"build".to_owned(),
|
||||||
"-f".to_owned(),
|
"-f".to_owned(),
|
||||||
"apps/mvp-node/Dockerfile".to_owned(),
|
"apps/myelin/node-image/Dockerfile".to_owned(),
|
||||||
"--build-arg".to_owned(),
|
"--build-arg".to_owned(),
|
||||||
format!("BASE_IMAGE={}", request.base_image),
|
format!("BASE_IMAGE={}", request.base_image),
|
||||||
"--build-arg".to_owned(),
|
"--build-arg".to_owned(),
|
||||||
format!("MVP_NODE_BIN={node_bin}"),
|
format!("MYELIN_NODE_BIN={node_bin}"),
|
||||||
];
|
];
|
||||||
for (key, value) in &expected_node_labels {
|
for (key, value) in &expected_node_labels {
|
||||||
build_args.push("--label".to_owned());
|
build_args.push("--label".to_owned());
|
||||||
|
|
@ -215,7 +215,7 @@ fn prepare_node_image_inner(
|
||||||
&root,
|
&root,
|
||||||
"docker",
|
"docker",
|
||||||
&build_args.iter().map(String::as_str).collect::<Vec<_>>(),
|
&build_args.iter().map(String::as_str).collect::<Vec<_>>(),
|
||||||
"build mvp node image",
|
"build myelin node image",
|
||||||
Some(&image_ref),
|
Some(&image_ref),
|
||||||
progress,
|
progress,
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -437,7 +437,7 @@ fn ensure_aliases_local(
|
||||||
root,
|
root,
|
||||||
"docker",
|
"docker",
|
||||||
&["tag", source_ref, &alias],
|
&["tag", source_ref, &alias],
|
||||||
"tag mvp node image",
|
"tag myelin node image",
|
||||||
Some(&alias),
|
Some(&alias),
|
||||||
progress,
|
progress,
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -461,7 +461,7 @@ fn ensure_aliases_for_remote(
|
||||||
root,
|
root,
|
||||||
"docker",
|
"docker",
|
||||||
&["pull", source_ref],
|
&["pull", source_ref],
|
||||||
"pull mvp node image",
|
"pull myelin node image",
|
||||||
Some(source_ref),
|
Some(source_ref),
|
||||||
progress,
|
progress,
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -489,7 +489,7 @@ fn push_image(
|
||||||
root,
|
root,
|
||||||
"docker",
|
"docker",
|
||||||
&["push", image_ref],
|
&["push", image_ref],
|
||||||
"push mvp node image",
|
"push myelin node image",
|
||||||
Some(image_ref),
|
Some(image_ref),
|
||||||
progress,
|
progress,
|
||||||
)
|
)
|
||||||
|
|
@ -509,7 +509,7 @@ fn docker_image_labels_match(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prune_old_dirty_images(root: &Path, image: &ImageName, keep_tag: &str) {
|
fn prune_old_dirty_images(root: &Path, image: &ImageName, keep_tag: &str) {
|
||||||
let prune_enabled = std::env::var("MVP_NODE_IMAGE_PRUNE")
|
let prune_enabled = std::env::var("MYELIN_NODE_IMAGE_PRUNE")
|
||||||
.map(|value| {
|
.map(|value| {
|
||||||
let value = value.trim().to_ascii_lowercase();
|
let value = value.trim().to_ascii_lowercase();
|
||||||
!matches!(value.as_str(), "0" | "false" | "no" | "off")
|
!matches!(value.as_str(), "0" | "false" | "no" | "off")
|
||||||
|
|
@ -522,12 +522,12 @@ fn prune_old_dirty_images(root: &Path, image: &ImageName, keep_tag: &str) {
|
||||||
let tags = match docker_image_tags(root, &image.repository) {
|
let tags = match docker_image_tags(root, &image.repository) {
|
||||||
Ok(tags) => tags,
|
Ok(tags) => tags,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("mvp-node-image: prune old dirty images skipped: {error}");
|
eprintln!("myelin-node-image: prune old dirty images skipped: {error}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let keep_old = std::env::var("MVP_NODE_IMAGE_PRUNE_KEEP")
|
let keep_old = std::env::var("MYELIN_NODE_IMAGE_PRUNE_KEEP")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|value| value.trim().parse::<usize>().ok())
|
.and_then(|value| value.trim().parse::<usize>().ok())
|
||||||
.unwrap_or(3);
|
.unwrap_or(3);
|
||||||
|
|
@ -554,7 +554,7 @@ fn prune_old_dirty_images(root: &Path, image: &ImageName, keep_tag: &str) {
|
||||||
}
|
}
|
||||||
if docker_image_has_container(root, &image_ref) {
|
if docker_image_has_container(root, &image_ref) {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"mvp-node-image: prune old dirty image {image_ref} skipped: container exists"
|
"myelin-node-image: prune old dirty image {image_ref} skipped: container exists"
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -563,9 +563,9 @@ fn prune_old_dirty_images(root: &Path, image: &ImageName, keep_tag: &str) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
eprintln!("mvp-node-image: prune old dirty image {image_ref}");
|
eprintln!("myelin-node-image: prune old dirty image {image_ref}");
|
||||||
if let Err(error) = docker_image_remove(root, &image_ref) {
|
if let Err(error) = docker_image_remove(root, &image_ref) {
|
||||||
eprintln!("mvp-node-image: prune old dirty image {image_ref} skipped: {error}");
|
eprintln!("myelin-node-image: prune old dirty image {image_ref} skipped: {error}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -669,7 +669,7 @@ fn run_status_command(
|
||||||
progress: &mut Option<&mut dyn NodeImageProgressSink>,
|
progress: &mut Option<&mut dyn NodeImageProgressSink>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let args: Vec<String> = args.iter().map(|arg| (*arg).to_owned()).collect();
|
let args: Vec<String> = args.iter().map(|arg| (*arg).to_owned()).collect();
|
||||||
eprintln!("mvp-node-image: {label}");
|
eprintln!("myelin-node-image: {label}");
|
||||||
if progress.is_none() {
|
if progress.is_none() {
|
||||||
let status = Command::new(program)
|
let status = Command::new(program)
|
||||||
.current_dir(root)
|
.current_dir(root)
|
||||||
|
|
@ -38,14 +38,14 @@ use iroh_driver::EndpointAddrMask;
|
||||||
|
|
||||||
const DEFAULT_CONFIG_PATH: &str = ".config/config.toml";
|
const DEFAULT_CONFIG_PATH: &str = ".config/config.toml";
|
||||||
const DEFAULT_RPC_ADDR: &str = "127.0.0.1:19777";
|
const DEFAULT_RPC_ADDR: &str = "127.0.0.1:19777";
|
||||||
const BASE_NODE_IMAGE: &str = "swactor-mvp-node-base:cuda12.6";
|
const BASE_NODE_IMAGE: &str = "myelin-node-base:cuda12.6";
|
||||||
const REPO_MODEL_CACHE_DIR: &str = ".model-cache";
|
const REPO_MODEL_CACHE_DIR: &str = ".model-cache";
|
||||||
const DEFAULT_MAX_TOKENS: u32 = 64;
|
const DEFAULT_MAX_TOKENS: u32 = 64;
|
||||||
const ORCH_SHUTDOWN_GRACE_MS: u64 = 5_000;
|
const ORCH_SHUTDOWN_GRACE_MS: u64 = 5_000;
|
||||||
const VASTAI_ORCH_SHUTDOWN_GRACE_MS: u64 = 180_000;
|
const VASTAI_ORCH_SHUTDOWN_GRACE_MS: u64 = 180_000;
|
||||||
const MVP_CHAT_GPU_RUN_ENV: &str = "MVP_CHAT_GPU_RUN";
|
const MYELIN_CHAT_GPU_RUN_ENV: &str = "MYELIN_CHAT_GPU_RUN";
|
||||||
const MVP_CHAT_USAGE: &str = "\
|
const MYELIN_CHAT_USAGE: &str = "\
|
||||||
USAGE: cargo mvp-chat [OPTIONS]
|
USAGE: cargo myelin-chat [OPTIONS]
|
||||||
|
|
||||||
OPTIONS:
|
OPTIONS:
|
||||||
--gpu Run the local GPU path: in-process orchestrator plus DEV=CUDA worker selection
|
--gpu Run the local GPU path: in-process orchestrator plus DEV=CUDA worker selection
|
||||||
|
|
@ -54,20 +54,20 @@ OPTIONS:
|
||||||
--config <path> Load config overlay
|
--config <path> Load config overlay
|
||||||
--pipeline-stages|--pipeline-parallel <count>
|
--pipeline-stages|--pipeline-parallel <count>
|
||||||
--relay-mode <mode> Relay mode: default or disabled
|
--relay-mode <mode> Relay mode: default or disabled
|
||||||
--relay-url <url> Custom relay URL passed to mvp-orchestrator
|
--relay-url <url> Custom relay URL passed to myelin-orchestrator
|
||||||
--endpoint-addr-mask <mask> Endpoint address mask: full or relay-only
|
--endpoint-addr-mask <mask> Endpoint address mask: full or relay-only
|
||||||
--cached-model[=<path>] Use discovered or explicit cached GGUF model
|
--cached-model[=<path>] Use discovered or explicit cached GGUF model (default for --process)
|
||||||
--dump-logs[=<path>] Write datastream frame log
|
--dump-logs[=<path>] Write datastream frame log
|
||||||
--run-id <id> Override run id
|
--run-id <id> Override run id
|
||||||
--skip-rebuild Reuse existing Cargo artifacts
|
--skip-rebuild Reuse existing Cargo artifacts
|
||||||
--yes, -y Approve Vast.ai lease prompts
|
--yes, -y Approve Vast.ai lease prompts
|
||||||
--help, -h Print this help";
|
--help, -h Print this help";
|
||||||
const ORCH_SHUTDOWN_POLL_MS: u64 = 50;
|
const ORCH_SHUTDOWN_POLL_MS: u64 = 50;
|
||||||
const CHAT_LIFECYCLE_CHANNEL: &str = "mvp.chat.lifecycle";
|
const CHAT_LIFECYCLE_CHANNEL: &str = "myelin.chat.lifecycle";
|
||||||
const CHAT_RUNTIME_CHANNEL: &str = "mvp.chat.runtime";
|
const CHAT_RUNTIME_CHANNEL: &str = "myelin.chat.runtime";
|
||||||
const CHAT_PROMPT_CHANNEL: &str = "mvp.chat.prompt";
|
const CHAT_PROMPT_CHANNEL: &str = "myelin.chat.prompt";
|
||||||
const CHAT_COMPONENT_CHANNEL: &str = "mvp.chat.component";
|
const CHAT_COMPONENT_CHANNEL: &str = "myelin.chat.component";
|
||||||
const CHAT_BENCHMARK_CHANNEL: &str = "mvp.chat.benchmark";
|
const CHAT_BENCHMARK_CHANNEL: &str = "myelin.chat.benchmark";
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum PromptInput {
|
enum PromptInput {
|
||||||
|
|
@ -86,7 +86,7 @@ where
|
||||||
match install_signal_handlers().and_then(|()| run(args)) {
|
match install_signal_handlers().and_then(|()| run(args)) {
|
||||||
Ok(()) => ExitCode::SUCCESS,
|
Ok(()) => ExitCode::SUCCESS,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("mvp-chat: {error}");
|
eprintln!("myelin-chat: {error}");
|
||||||
ExitCode::from(1)
|
ExitCode::from(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -129,7 +129,7 @@ where
|
||||||
.iter()
|
.iter()
|
||||||
.any(|arg| matches!(arg.as_str(), "--help" | "-h" | "help"))
|
.any(|arg| matches!(arg.as_str(), "--help" | "-h" | "help"))
|
||||||
{
|
{
|
||||||
println!("{MVP_CHAT_USAGE}");
|
println!("{MYELIN_CHAT_USAGE}");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let config = Config::from_args(provided_args)?;
|
let config = Config::from_args(provided_args)?;
|
||||||
|
|
@ -386,11 +386,11 @@ struct ChatDatastream {
|
||||||
|
|
||||||
impl ChatDatastream {
|
impl ChatDatastream {
|
||||||
fn new(run_id: u64, archive_path: Option<PathBuf>) -> Result<Self, String> {
|
fn new(run_id: u64, archive_path: Option<PathBuf>) -> Result<Self, String> {
|
||||||
let stream = StreamId::new(NodeId::new("mvp-chat"), Lifetime(run_id));
|
let stream = StreamId::new(NodeId::new("myelin-chat"), Lifetime(run_id));
|
||||||
let endpoint = DatastreamEndpoint::with_descriptor(
|
let endpoint = DatastreamEndpoint::with_descriptor(
|
||||||
StreamDescriptor {
|
StreamDescriptor {
|
||||||
stream: stream.clone(),
|
stream: stream.clone(),
|
||||||
label: Some("mvp chat".to_owned()),
|
label: Some("myelin chat".to_owned()),
|
||||||
origin: StreamOrigin::Orchestrator,
|
origin: StreamOrigin::Orchestrator,
|
||||||
},
|
},
|
||||||
1024,
|
1024,
|
||||||
|
|
@ -436,7 +436,7 @@ impl ChatDatastream {
|
||||||
|
|
||||||
fn emit(&mut self, channel: &str, phase: &str, status: &str, detail: Value) {
|
fn emit(&mut self, channel: &str, phase: &str, status: &str, detail: Value) {
|
||||||
let id = self.channel_by_name(channel);
|
let id = self.channel_by_name(channel);
|
||||||
let benchmark = benchmark::stamp("mvp-chat");
|
let benchmark = benchmark::stamp("myelin-chat");
|
||||||
let payload = serde_json::to_vec(&json!({
|
let payload = serde_json::to_vec(&json!({
|
||||||
"schema_version": benchmark["schema_version"].clone(),
|
"schema_version": benchmark["schema_version"].clone(),
|
||||||
"type": "ChatProgress",
|
"type": "ChatProgress",
|
||||||
|
|
@ -452,19 +452,19 @@ impl ChatDatastream {
|
||||||
"wall_clock_unix_ms": benchmark["wall_clock_unix_ms"].clone(),
|
"wall_clock_unix_ms": benchmark["wall_clock_unix_ms"].clone(),
|
||||||
"monotonic_ms": benchmark["monotonic_ms"].clone(),
|
"monotonic_ms": benchmark["monotonic_ms"].clone(),
|
||||||
"clock_source": benchmark["clock_source"].clone(),
|
"clock_source": benchmark["clock_source"].clone(),
|
||||||
"span_id": format!("mvp-chat:{}:{}:{phase}", self.run_id, benchmark["producer_sequence"]),
|
"span_id": format!("myelin-chat:{}:{}:{phase}", self.run_id, benchmark["producer_sequence"]),
|
||||||
"parent_span_id": Value::Null,
|
"parent_span_id": Value::Null,
|
||||||
"benchmark": benchmark,
|
"benchmark": benchmark,
|
||||||
"detail": detail,
|
"detail": detail,
|
||||||
}))
|
}))
|
||||||
.expect("serialize mvp-chat progress event");
|
.expect("serialize myelin-chat progress event");
|
||||||
self.producer.submit_bytes(id, payload);
|
self.producer.submit_bytes(id, payload);
|
||||||
self.flush();
|
self.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn emit_benchmark_envelope(&mut self, config: &Config) {
|
fn emit_benchmark_envelope(&mut self, config: &Config) {
|
||||||
let id = self.channel_by_name(CHAT_BENCHMARK_CHANNEL);
|
let id = self.channel_by_name(CHAT_BENCHMARK_CHANNEL);
|
||||||
let benchmark = benchmark::stamp("mvp-chat");
|
let benchmark = benchmark::stamp("myelin-chat");
|
||||||
let payload = serde_json::to_vec(&json!({
|
let payload = serde_json::to_vec(&json!({
|
||||||
"schema_version": benchmark["schema_version"].clone(),
|
"schema_version": benchmark["schema_version"].clone(),
|
||||||
"type": "BenchmarkRunEnvelope",
|
"type": "BenchmarkRunEnvelope",
|
||||||
|
|
@ -480,11 +480,11 @@ impl ChatDatastream {
|
||||||
"wall_clock_unix_ms": benchmark["wall_clock_unix_ms"].clone(),
|
"wall_clock_unix_ms": benchmark["wall_clock_unix_ms"].clone(),
|
||||||
"monotonic_ms": benchmark["monotonic_ms"].clone(),
|
"monotonic_ms": benchmark["monotonic_ms"].clone(),
|
||||||
"clock_source": benchmark["clock_source"].clone(),
|
"clock_source": benchmark["clock_source"].clone(),
|
||||||
"span_id": format!("mvp-chat:{}:{}:run_envelope", self.run_id, benchmark["producer_sequence"]),
|
"span_id": format!("myelin-chat:{}:{}:run_envelope", self.run_id, benchmark["producer_sequence"]),
|
||||||
"parent_span_id": Value::Null,
|
"parent_span_id": Value::Null,
|
||||||
"benchmark": benchmark,
|
"benchmark": benchmark,
|
||||||
"detail": {
|
"detail": {
|
||||||
"scenario": "mvp-chat",
|
"scenario": "myelin-chat",
|
||||||
"detail_level": "benchmark_observability_v1",
|
"detail_level": "benchmark_observability_v1",
|
||||||
"workload": {
|
"workload": {
|
||||||
"mode": "stdin_prompt_corpus",
|
"mode": "stdin_prompt_corpus",
|
||||||
|
|
@ -533,7 +533,7 @@ impl ChatDatastream {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
.expect("serialize mvp-chat benchmark envelope");
|
.expect("serialize myelin-chat benchmark envelope");
|
||||||
self.producer.submit_bytes(id, payload);
|
self.producer.submit_bytes(id, payload);
|
||||||
self.flush();
|
self.flush();
|
||||||
}
|
}
|
||||||
|
|
@ -551,7 +551,7 @@ impl ChatDatastream {
|
||||||
"relay_configured": config.relay_url.is_some(),
|
"relay_configured": config.relay_url.is_some(),
|
||||||
"endpoint_addr_mask": config.endpoint_addr_mask.as_str(),
|
"endpoint_addr_mask": config.endpoint_addr_mask.as_str(),
|
||||||
});
|
});
|
||||||
let synthetic_id = format!("mvp-chat-{}-datastream-preflight", self.run_id);
|
let synthetic_id = format!("myelin-chat-{}-datastream-preflight", self.run_id);
|
||||||
for (phase, status) in [
|
for (phase, status) in [
|
||||||
("DatastreamProducerConfigured", "configured"),
|
("DatastreamProducerConfigured", "configured"),
|
||||||
("DatastreamProducerConnected", "ready"),
|
("DatastreamProducerConnected", "ready"),
|
||||||
|
|
@ -563,7 +563,7 @@ impl ChatDatastream {
|
||||||
phase,
|
phase,
|
||||||
status,
|
status,
|
||||||
json!({
|
json!({
|
||||||
"producer": "mvp-chat",
|
"producer": "myelin-chat",
|
||||||
"producer_class": "rust-chat",
|
"producer_class": "rust-chat",
|
||||||
"synthetic_id": synthetic_id,
|
"synthetic_id": synthetic_id,
|
||||||
"datastream_endpoint": endpoint,
|
"datastream_endpoint": endpoint,
|
||||||
|
|
@ -576,8 +576,8 @@ impl ChatDatastream {
|
||||||
"endpoint_config_snapshot",
|
"endpoint_config_snapshot",
|
||||||
"ready",
|
"ready",
|
||||||
json!({
|
json!({
|
||||||
"producer": "mvp-chat",
|
"producer": "myelin-chat",
|
||||||
"expected_producers": ["mvp-chat", "mvp-orchestrator", "mvp-worker-node", "tinygrad-worker"],
|
"expected_producers": ["myelin-chat", "myelin-orchestrator", "myelin-worker", "tinygrad-worker"],
|
||||||
"datastream_endpoint": endpoint,
|
"datastream_endpoint": endpoint,
|
||||||
"runtime_endpoint": runtime_endpoint,
|
"runtime_endpoint": runtime_endpoint,
|
||||||
"connectivity_preflight": {
|
"connectivity_preflight": {
|
||||||
|
|
@ -597,7 +597,7 @@ impl ChatDatastream {
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| format!("channel#{}", frame.channel.0));
|
.unwrap_or_else(|| format!("channel#{}", frame.channel.0));
|
||||||
self.pending
|
self.pending
|
||||||
.push(("mvp-chat".to_owned(), stream.clone(), channel, frame));
|
.push(("myelin-chat".to_owned(), stream.clone(), channel, frame));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -609,7 +609,7 @@ impl ChatDatastream {
|
||||||
if self.pending.is_empty() {
|
if self.pending.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let mut archive = FrameArchive::open_with_label(path, "mvp-chat datastream frame log")?;
|
let mut archive = FrameArchive::open_with_label(path, "myelin-chat datastream frame log")?;
|
||||||
for (source, stream, channel, frame) in self.pending.drain(..) {
|
for (source, stream, channel, frame) in self.pending.drain(..) {
|
||||||
archive.record(&source, &stream, &channel, &frame)?;
|
archive.record(&source, &stream, &channel, &frame)?;
|
||||||
}
|
}
|
||||||
|
|
@ -790,12 +790,18 @@ impl Config {
|
||||||
}
|
}
|
||||||
let pipeline_stages = Self::pipeline_stages(&args, &toml)?;
|
let pipeline_stages = Self::pipeline_stages(&args, &toml)?;
|
||||||
let max_tokens = Self::max_tokens(&toml)?;
|
let max_tokens = Self::max_tokens(&toml)?;
|
||||||
let gpu_run = args.gpu || env_flag(MVP_CHAT_GPU_RUN_ENV, false);
|
let gpu_run = args.gpu || env_flag(MYELIN_CHAT_GPU_RUN_ENV, false);
|
||||||
let endpoint_addr_mask = Self::endpoint_addr_mask(&args, &toml)?;
|
let endpoint_addr_mask = Self::endpoint_addr_mask(&args, &toml)?;
|
||||||
let (relay_mode, relay_url) = Self::relay_settings(&args, &toml, endpoint_addr_mask)?;
|
let (relay_mode, relay_url) = Self::relay_settings(&args, &toml, endpoint_addr_mask)?;
|
||||||
let cached_model = Self::cached_model_source(&args, gpu_run, &provider)
|
let cached_model = match Self::cached_model_source(&args, &provider) {
|
||||||
.map(CachedModelConfig::from_source)
|
None => None,
|
||||||
.transpose()?;
|
Some(CachedModelSource::Path(path)) => Some(CachedModelConfig::from_path(path)?),
|
||||||
|
Some(CachedModelSource::Discover) => match CachedModelConfig::discover() {
|
||||||
|
Ok(config) => Some(config),
|
||||||
|
Err(error) if args.cached_model.is_some() => return Err(error),
|
||||||
|
Err(_) => None,
|
||||||
|
},
|
||||||
|
};
|
||||||
let model = Self::model_config(&provider, &toml, cached_model.as_ref())?;
|
let model = Self::model_config(&provider, &toml, cached_model.as_ref())?;
|
||||||
let datastream_frame_log = Self::datastream_frame_log(&args, &toml);
|
let datastream_frame_log = Self::datastream_frame_log(&args, &toml);
|
||||||
let vastai = if provider == provider_kind::vastai() {
|
let vastai = if provider == provider_kind::vastai() {
|
||||||
|
|
@ -805,7 +811,7 @@ impl Config {
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
orch_bin: artifact_root().join("target/debug/mvp-orchestrator"),
|
orch_bin: artifact_root().join("target/debug/myelin-orchestrator"),
|
||||||
worker_bin: default_worker_bin(),
|
worker_bin: default_worker_bin(),
|
||||||
rpc_addr: DEFAULT_RPC_ADDR.to_owned(),
|
rpc_addr: DEFAULT_RPC_ADDR.to_owned(),
|
||||||
node_image,
|
node_image,
|
||||||
|
|
@ -876,16 +882,15 @@ impl Config {
|
||||||
Ok((relay_mode, relay_url))
|
Ok((relay_mode, relay_url))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cached_model_source(
|
/// Resolve the cached-model source. The process provider defaults to
|
||||||
args: &ParsedArgs,
|
/// best-effort discovery of `.model-cache/` so `cargo myelin-chat` runs a
|
||||||
gpu_run: bool,
|
/// cached GGUF model without explicit flags. Explicit `--cached-model` is
|
||||||
provider: &ProviderKind,
|
/// always honored (and stays strict); the default degrades gracefully to
|
||||||
) -> Option<CachedModelSource> {
|
/// the normal download path when no cached model is present.
|
||||||
|
fn cached_model_source(args: &ParsedArgs, provider: &ProviderKind) -> Option<CachedModelSource> {
|
||||||
match &args.cached_model {
|
match &args.cached_model {
|
||||||
Some(source) => Some(source.clone()),
|
Some(source) => Some(source.clone()),
|
||||||
None if gpu_run && provider == &provider_kind::process() => {
|
None if provider == &provider_kind::process() => Some(CachedModelSource::Discover),
|
||||||
Some(CachedModelSource::Discover)
|
|
||||||
}
|
|
||||||
None => None,
|
None => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -911,7 +916,7 @@ impl Config {
|
||||||
return Some(
|
return Some(
|
||||||
args.dump_log_path
|
args.dump_log_path
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| PathBuf::from("mvp-chat.log")),
|
.unwrap_or_else(|| PathBuf::from("myelin-chat.log")),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if !toml.observability.dump_logs.unwrap_or(false) {
|
if !toml.observability.dump_logs.unwrap_or(false) {
|
||||||
|
|
@ -920,7 +925,7 @@ impl Config {
|
||||||
Some(
|
Some(
|
||||||
first_non_empty([toml.observability.dump_log_path.clone()])
|
first_non_empty([toml.observability.dump_log_path.clone()])
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.unwrap_or_else(|| PathBuf::from("mvp-chat.log")),
|
.unwrap_or_else(|| PathBuf::from("myelin-chat.log")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1191,7 +1196,7 @@ impl ParsedArgs {
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return Err(format!("unsupported mvp-chat argument {arg:?}"));
|
return Err(format!("unsupported myelin-chat argument {arg:?}"));
|
||||||
}
|
}
|
||||||
Ok(parsed)
|
Ok(parsed)
|
||||||
}
|
}
|
||||||
|
|
@ -1650,16 +1655,16 @@ where
|
||||||
true,
|
true,
|
||||||
config.skip_rebuild,
|
config.skip_rebuild,
|
||||||
&config.orch_bin,
|
&config.orch_bin,
|
||||||
"mvp-orchestrator",
|
"myelin-orchestrator",
|
||||||
&[
|
&[
|
||||||
"build",
|
"build",
|
||||||
"--quiet",
|
"--quiet",
|
||||||
"-p",
|
"-p",
|
||||||
"mvp-system",
|
"myelin",
|
||||||
"--features",
|
"--features",
|
||||||
"dashboard",
|
"dashboard",
|
||||||
"--bin",
|
"--bin",
|
||||||
"mvp-orchestrator",
|
"myelin-orchestrator",
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
@ -1672,14 +1677,14 @@ where
|
||||||
false,
|
false,
|
||||||
config.skip_rebuild,
|
config.skip_rebuild,
|
||||||
&config.worker_bin,
|
&config.worker_bin,
|
||||||
"mvp-worker-node",
|
"myelin-worker",
|
||||||
&[
|
&[
|
||||||
"build",
|
"build",
|
||||||
"--quiet",
|
"--quiet",
|
||||||
"-p",
|
"-p",
|
||||||
"mvp-system",
|
"myelin",
|
||||||
"--bin",
|
"--bin",
|
||||||
"mvp-worker-node",
|
"myelin-worker",
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
emit_chat_progress(
|
emit_chat_progress(
|
||||||
|
|
@ -1717,14 +1722,14 @@ where
|
||||||
true,
|
true,
|
||||||
config.skip_rebuild,
|
config.skip_rebuild,
|
||||||
&config.worker_bin,
|
&config.worker_bin,
|
||||||
"mvp-worker-node",
|
"myelin-worker",
|
||||||
&[
|
&[
|
||||||
"build",
|
"build",
|
||||||
"--quiet",
|
"--quiet",
|
||||||
"-p",
|
"-p",
|
||||||
"mvp-system",
|
"myelin",
|
||||||
"--bin",
|
"--bin",
|
||||||
"mvp-worker-node",
|
"myelin-worker",
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
emit_chat_progress(
|
emit_chat_progress(
|
||||||
|
|
@ -2062,7 +2067,7 @@ fn run_chat_session_with_output_and_progress(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_worker_bin() -> PathBuf {
|
fn default_worker_bin() -> PathBuf {
|
||||||
artifact_root().join("target/debug/mvp-worker-node")
|
artifact_root().join("target/debug/myelin-worker")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_runtime_binary(
|
fn ensure_runtime_binary(
|
||||||
|
|
@ -2122,13 +2127,6 @@ struct CachedModelConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CachedModelConfig {
|
impl CachedModelConfig {
|
||||||
fn from_source(source: CachedModelSource) -> Result<Self, String> {
|
|
||||||
match source {
|
|
||||||
CachedModelSource::Discover => Self::discover(),
|
|
||||||
CachedModelSource::Path(path) => Self::from_path(path),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_path(path: PathBuf) -> Result<Self, String> {
|
fn from_path(path: PathBuf) -> Result<Self, String> {
|
||||||
let metadata = fs::metadata(&path)
|
let metadata = fs::metadata(&path)
|
||||||
.map_err(|e| format!("stat cached model {}: {e}", path.display()))?;
|
.map_err(|e| format!("stat cached model {}: {e}", path.display()))?;
|
||||||
|
|
@ -2265,9 +2263,9 @@ mod tests {
|
||||||
|
|
||||||
const PROCESS_ENV_KEYS: &[&str] = &[
|
const PROCESS_ENV_KEYS: &[&str] = &[
|
||||||
"VAST_API_KEY",
|
"VAST_API_KEY",
|
||||||
"MVP_PIPELINE_STAGES",
|
"MYELIN_PIPELINE_STAGES",
|
||||||
"MVP_RUNTIME_CONFIG",
|
"MYELIN_RUNTIME_CONFIG",
|
||||||
"MVP_CHAT_GPU_RUN",
|
"MYELIN_CHAT_GPU_RUN",
|
||||||
"DEV",
|
"DEV",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -2279,7 +2277,7 @@ mod tests {
|
||||||
fn new(label: &str) -> Self {
|
fn new(label: &str) -> Self {
|
||||||
let id = NEXT_TEMP_ID.fetch_add(1, AtomicOrdering::SeqCst);
|
let id = NEXT_TEMP_ID.fetch_add(1, AtomicOrdering::SeqCst);
|
||||||
let path = std::env::temp_dir().join(format!(
|
let path = std::env::temp_dir().join(format!(
|
||||||
"mvp-chat-test-{}-{}-{}",
|
"myelin-chat-test-{}-{}-{}",
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
id,
|
id,
|
||||||
label
|
label
|
||||||
|
|
@ -2464,8 +2462,8 @@ mod tests {
|
||||||
|
|
||||||
with_process_state(
|
with_process_state(
|
||||||
&[
|
&[
|
||||||
("MVP_PIPELINE_STAGES", Some("9")),
|
("MYELIN_PIPELINE_STAGES", Some("9")),
|
||||||
("MVP_RUNTIME_CONFIG", Some("local")),
|
("MYELIN_RUNTIME_CONFIG", Some("local")),
|
||||||
],
|
],
|
||||||
Some(temp.path()),
|
Some(temp.path()),
|
||||||
|| {
|
|| {
|
||||||
|
|
@ -2517,6 +2515,34 @@ tag = " alias "
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_defaults_use_cached_model_for_process_when_present() {
|
||||||
|
let temp = TempDir::new("cached-model-default");
|
||||||
|
|
||||||
|
with_process_state(&[], Some(temp.path()), || {
|
||||||
|
let cache_dir = temp.path().join(REPO_MODEL_CACHE_DIR);
|
||||||
|
fs::create_dir_all(&cache_dir).expect("create model cache dir");
|
||||||
|
fs::write(
|
||||||
|
cache_dir.join(DEFAULT_PIPELINE_CACHED_MODEL_FILE),
|
||||||
|
Vec::<u8>::new(),
|
||||||
|
)
|
||||||
|
.expect("seed cached model file");
|
||||||
|
|
||||||
|
let defaults = Config::from_args(Vec::<String>::new()).expect("defaults resolve");
|
||||||
|
|
||||||
|
assert_eq!(defaults.provider, provider_kind::process());
|
||||||
|
let cached_model = defaults
|
||||||
|
.cached_model
|
||||||
|
.expect("process provider discovers a cached model by default");
|
||||||
|
assert!(
|
||||||
|
cached_model
|
||||||
|
.host_path
|
||||||
|
.ends_with(DEFAULT_PIPELINE_CACHED_MODEL_FILE),
|
||||||
|
"discovered the seeded cached model"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn config_max_tokens_drives_orchestrator_args_and_submit_prompt() {
|
fn config_max_tokens_drives_orchestrator_args_and_submit_prompt() {
|
||||||
let temp = TempDir::new("config-max-tokens");
|
let temp = TempDir::new("config-max-tokens");
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
//! Registers wire codecs for every MVP actor message type.
|
//! Registers wire codecs for every Myelin actor message type.
|
||||||
//!
|
//!
|
||||||
//! The runtime's [`CodecRegistry`](swactor_transport::CodecRegistry) needs an
|
//! The runtime's [`CodecRegistry`](swactor_transport::CodecRegistry) needs an
|
||||||
//! encoder/decoder entry for each inter-node message type. This is the single
|
//! encoder/decoder entry for each inter-node message type. This is the single
|
||||||
//! aggregator that wires up the node, orchestrator, prompt, and datastream
|
//! aggregator that wires up the node, orchestrator, prompt, and datastream
|
||||||
//! publisher codecs.
|
//! publisher codecs.
|
||||||
|
|
||||||
pub(crate) fn register_mvp_actor_codecs(registry: &mut swactor_transport::CodecRegistry) {
|
pub(crate) fn register_myelin_actor_codecs(registry: &mut swactor_transport::CodecRegistry) {
|
||||||
crate::node_actor::register_codecs(registry);
|
crate::node_actor::register_codecs(registry);
|
||||||
crate::orchestration::actor::register_codecs(registry);
|
crate::orchestration::actor::register_codecs(registry);
|
||||||
datastream::register_datastream_publisher_codec(registry);
|
datastream::register_datastream_publisher_codec(registry);
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
extern crate self as mvp_system;
|
extern crate self as myelin;
|
||||||
|
|
||||||
const DEFAULT_PIPELINE_CACHED_MODEL_FILE: &str = "SmolLM2-135M-Instruct.Q4_0.gguf";
|
const DEFAULT_PIPELINE_CACHED_MODEL_FILE: &str = "SmolLM2-135M-Instruct.Q4_0.gguf";
|
||||||
const DEFAULT_PIPELINE_CACHED_MODEL_REPO: &str = "QuantFactory/SmolLM2-135M-Instruct-GGUF";
|
const DEFAULT_PIPELINE_CACHED_MODEL_REPO: &str = "QuantFactory/SmolLM2-135M-Instruct-GGUF";
|
||||||
|
|
@ -170,6 +170,19 @@ pub(crate) enum NodeAgentMsg {
|
||||||
WorkerCrashed {
|
WorkerCrashed {
|
||||||
reason: Option<String>,
|
reason: Option<String>,
|
||||||
},
|
},
|
||||||
|
StepFailed {
|
||||||
|
step_id: u64,
|
||||||
|
},
|
||||||
|
ObjectFailed {
|
||||||
|
edge_id: u64,
|
||||||
|
object_id: Option<u64>,
|
||||||
|
},
|
||||||
|
OutputFault {
|
||||||
|
edge_id: u64,
|
||||||
|
},
|
||||||
|
EdgeFault {
|
||||||
|
edge_id: u64,
|
||||||
|
},
|
||||||
StopRun {
|
StopRun {
|
||||||
run_id: u64,
|
run_id: u64,
|
||||||
},
|
},
|
||||||
|
|
@ -208,7 +221,7 @@ pub(crate) enum NodeAgentMsg {
|
||||||
|
|
||||||
impl NetworkMessage for NodeAgentMsg {
|
impl NetworkMessage for NodeAgentMsg {
|
||||||
fn type_tag() -> &'static str {
|
fn type_tag() -> &'static str {
|
||||||
"mvp_system::NodeAgentMsg"
|
"myelin::NodeAgentMsg"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -236,9 +249,6 @@ pub(crate) enum StageCommandWire {
|
||||||
layer_end_exclusive: u32,
|
layer_end_exclusive: u32,
|
||||||
stage_shard_plan: Option<StageShardPlan>,
|
stage_shard_plan: Option<StageShardPlan>,
|
||||||
},
|
},
|
||||||
RewireEdge {
|
|
||||||
edge_id: u64,
|
|
||||||
},
|
|
||||||
ExecuteStep {
|
ExecuteStep {
|
||||||
step_id: u64,
|
step_id: u64,
|
||||||
input_edge_id: u64,
|
input_edge_id: u64,
|
||||||
|
|
@ -314,7 +324,7 @@ pub(crate) enum NodeAgentReport {
|
||||||
|
|
||||||
impl NetworkMessage for NodeAgentReport {
|
impl NetworkMessage for NodeAgentReport {
|
||||||
fn type_tag() -> &'static str {
|
fn type_tag() -> &'static str {
|
||||||
"mvp_system::NodeAgentReport"
|
"myelin::NodeAgentReport"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -531,6 +541,27 @@ impl NodeAgentActor {
|
||||||
self.last_worker_crash = reason;
|
self.last_worker_crash = reason;
|
||||||
self.core.observe(stage::StageEvent::WorkerCrashed)
|
self.core.observe(stage::StageEvent::WorkerCrashed)
|
||||||
}
|
}
|
||||||
|
NodeAgentMsg::StepFailed { step_id } => {
|
||||||
|
self.core.observe(stage::StageEvent::StepFailed {
|
||||||
|
step_id: stage::StepId(step_id),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
NodeAgentMsg::ObjectFailed { edge_id, object_id } => {
|
||||||
|
self.core.observe(stage::StageEvent::ObjectFailed {
|
||||||
|
edge_id: stage::EdgeId(edge_id),
|
||||||
|
object_id: object_id.map(stage::ObjectId),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
NodeAgentMsg::OutputFault { edge_id } => {
|
||||||
|
self.core.observe(stage::StageEvent::OutputFault {
|
||||||
|
edge_id: stage::EdgeId(edge_id),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
NodeAgentMsg::EdgeFault { edge_id } => {
|
||||||
|
self.core.observe(stage::StageEvent::EdgeFault {
|
||||||
|
edge_id: stage::EdgeId(edge_id),
|
||||||
|
})
|
||||||
|
}
|
||||||
NodeAgentMsg::StopRun { run_id } => self.core.observe(stage::StageEvent::StopRun {
|
NodeAgentMsg::StopRun { run_id } => self.core.observe(stage::StageEvent::StopRun {
|
||||||
run_id: stage::RunId(run_id),
|
run_id: stage::RunId(run_id),
|
||||||
}),
|
}),
|
||||||
|
|
@ -692,7 +723,6 @@ impl From<&stage::StageCommand> for StageCommandWire {
|
||||||
layer_end_exclusive: range.end_exclusive,
|
layer_end_exclusive: range.end_exclusive,
|
||||||
stage_shard_plan: shard_plan.clone(),
|
stage_shard_plan: shard_plan.clone(),
|
||||||
},
|
},
|
||||||
stage::StageCommand::RewireEdge { edge_id } => Self::RewireEdge { edge_id: edge_id.0 },
|
|
||||||
stage::StageCommand::ExecuteStep(step) => Self::ExecuteStep {
|
stage::StageCommand::ExecuteStep(step) => Self::ExecuteStep {
|
||||||
step_id: step.step_id.0,
|
step_id: step.step_id.0,
|
||||||
input_edge_id: step.input.edge_id.0,
|
input_edge_id: step.input.edge_id.0,
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! MVP node-local runtime public surface.
|
//! Myelin node-local runtime public surface.
|
||||||
//!
|
//!
|
||||||
//! Worker-node runtime behavior lives behind this module boundary; binaries
|
//! Worker-node runtime behavior lives behind this module boundary; binaries
|
||||||
//! only wire entrypoints into it.
|
//! only wire entrypoints into it.
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! Worker-node runtime behavior behind the `mvp_system::node` boundary.
|
//! Worker-node runtime behavior behind the `myelin::node` boundary.
|
||||||
//!
|
//!
|
||||||
//! The `mvp-worker-node` binary remains a thin entrypoint wrapper; this module
|
//! The `myelin-worker` binary remains a thin entrypoint wrapper; this module
|
||||||
//! owns the reusable node-local runtime, helper-command, datastream archive,
|
//! owns the reusable node-local runtime, helper-command, datastream archive,
|
||||||
//! debug-join, staging, transport, and worker coordination behavior.
|
//! debug-join, staging, transport, and worker coordination behavior.
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ use datastream::{
|
||||||
Lifetime, NodeId, Record, StreamDescriptor, StreamId, StreamOrigin,
|
Lifetime, NodeId, Record, StreamDescriptor, StreamId, StreamOrigin,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::codecs::register_mvp_actor_codecs;
|
use crate::codecs::register_myelin_actor_codecs;
|
||||||
use crate::gguf_shard::{StageShardPlan, materialize_stage_shard_http, validate_stage_shard_cache};
|
use crate::gguf_shard::{StageShardPlan, materialize_stage_shard_http, validate_stage_shard_cache};
|
||||||
use crate::node_actor::{
|
use crate::node_actor::{
|
||||||
NodeAgentActor, NodeAgentMsg, NodeAgentReport, StageCommandWire, StageInboundEdgeWire,
|
NodeAgentActor, NodeAgentMsg, NodeAgentReport, StageCommandWire, StageInboundEdgeWire,
|
||||||
|
|
@ -53,7 +53,7 @@ use swactor::actor::{ActorAddress, ActorInterface};
|
||||||
use swactor::runtime::{Ctx, ExternalSender};
|
use swactor::runtime::{Ctx, ExternalSender};
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
const DEFAULT_WORKER_SCRIPT: &str = "/usr/local/share/mvp/tinygrad_worker.py";
|
const DEFAULT_WORKER_SCRIPT: &str = "/usr/local/share/myelin/tinygrad_worker.py";
|
||||||
const DEFAULT_DEVICE: &str = "CUDA";
|
const DEFAULT_DEVICE: &str = "CUDA";
|
||||||
const DEFAULT_HF_REPO: &str = "bartowski/Llama-3.2-1B-Instruct-GGUF";
|
const DEFAULT_HF_REPO: &str = "bartowski/Llama-3.2-1B-Instruct-GGUF";
|
||||||
const DEFAULT_HF_FILE: &str = "Llama-3.2-1B-Instruct-Q4_K_M.gguf";
|
const DEFAULT_HF_FILE: &str = "Llama-3.2-1B-Instruct-Q4_K_M.gguf";
|
||||||
|
|
@ -63,22 +63,22 @@ const DEFAULT_ARENA_ALIGNMENT: u64 = 64;
|
||||||
const PUMP_INTERVAL: Duration = Duration::from_millis(10);
|
const PUMP_INTERVAL: Duration = Duration::from_millis(10);
|
||||||
const RUNTIME_READY_RETRY_INITIAL: Duration = Duration::from_millis(100);
|
const RUNTIME_READY_RETRY_INITIAL: Duration = Duration::from_millis(100);
|
||||||
const RUNTIME_READY_RETRY_MAX: Duration = Duration::from_secs(2);
|
const RUNTIME_READY_RETRY_MAX: Duration = Duration::from_secs(2);
|
||||||
const NODE_BOOTSTRAP_CHANNEL: &str = "mvp.node.bootstrap";
|
const NODE_BOOTSTRAP_CHANNEL: &str = "myelin.node.bootstrap";
|
||||||
const NODE_RUNTIME_CHANNEL: &str = "mvp.node.runtime";
|
const NODE_RUNTIME_CHANNEL: &str = "myelin.node.runtime";
|
||||||
const NODE_STAGE_CHANNEL: &str = "mvp.node.stage";
|
const NODE_STAGE_CHANNEL: &str = "myelin.node.stage";
|
||||||
const NODE_WORKER_CHANNEL: &str = "mvp.node.worker";
|
const NODE_WORKER_CHANNEL: &str = "myelin.node.worker";
|
||||||
const NODE_PROMPT_CHANNEL: &str = "mvp.node.prompt";
|
const NODE_PROMPT_CHANNEL: &str = "myelin.node.prompt";
|
||||||
const NODE_SHUTDOWN_CHANNEL: &str = "mvp.node.shutdown";
|
const NODE_SHUTDOWN_CHANNEL: &str = "myelin.node.shutdown";
|
||||||
const NODE_SAMPLER_CHANNEL: &str = "mvp.node.sampler";
|
const NODE_SAMPLER_CHANNEL: &str = "myelin.node.sampler";
|
||||||
const WORKER_COMMAND_WAIT_TELEMETRY_INTERVAL: Duration = Duration::from_secs(1);
|
const WORKER_COMMAND_WAIT_TELEMETRY_INTERVAL: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
fn worker_benchmark_stamp(run_id: u64, node_id: u64) -> Value {
|
fn worker_benchmark_stamp(run_id: u64, node_id: u64) -> Value {
|
||||||
let mut benchmark = benchmark::stamp("mvp-worker-node");
|
let mut benchmark = benchmark::stamp("myelin-worker");
|
||||||
let pid = benchmark
|
let pid = benchmark
|
||||||
.get("producer_process_id")
|
.get("producer_process_id")
|
||||||
.and_then(Value::as_u64)
|
.and_then(Value::as_u64)
|
||||||
.unwrap_or_else(|| u64::from(std::process::id()));
|
.unwrap_or_else(|| u64::from(std::process::id()));
|
||||||
let producer_instance_id = format!("mvp-worker-node:{run_id}:node-{node_id}:pid-{pid}");
|
let producer_instance_id = format!("myelin-worker:{run_id}:node-{node_id}:pid-{pid}");
|
||||||
if let Some(object) = benchmark.as_object_mut() {
|
if let Some(object) = benchmark.as_object_mut() {
|
||||||
object.insert(
|
object.insert(
|
||||||
"producer_instance_id".to_owned(),
|
"producer_instance_id".to_owned(),
|
||||||
|
|
@ -112,7 +112,7 @@ fn node_event_payload(
|
||||||
"wall_clock_unix_ms":benchmark["wall_clock_unix_ms"].clone(),
|
"wall_clock_unix_ms":benchmark["wall_clock_unix_ms"].clone(),
|
||||||
"monotonic_ms":benchmark["monotonic_ms"].clone(),
|
"monotonic_ms":benchmark["monotonic_ms"].clone(),
|
||||||
"clock_source":benchmark["clock_source"].clone(),
|
"clock_source":benchmark["clock_source"].clone(),
|
||||||
"span_id":format!("mvp-worker-node:{}:{}:{}:{phase}", config.run_id, config.logical_node_id, benchmark["producer_sequence"]),
|
"span_id":format!("myelin-worker:{}:{}:{}:{phase}", config.run_id, config.logical_node_id, benchmark["producer_sequence"]),
|
||||||
"parent_span_id":Value::Null,
|
"parent_span_id":Value::Null,
|
||||||
"benchmark":benchmark,
|
"benchmark":benchmark,
|
||||||
"detail":detail,
|
"detail":detail,
|
||||||
|
|
@ -123,7 +123,7 @@ fn emit_stdio_datastream_frame(channel: &str, payload: &Value) -> Result<(), Str
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
json!({
|
json!({
|
||||||
"mvp_stdio_event":1,
|
"myelin_stdio_event":1,
|
||||||
"kind":"datastream_frame",
|
"kind":"datastream_frame",
|
||||||
"channel":channel,
|
"channel":channel,
|
||||||
"payload":payload,
|
"payload":payload,
|
||||||
|
|
@ -200,7 +200,7 @@ fn debug_join_client_main(args: Vec<String>) -> ExitCode {
|
||||||
match serde_json::to_string(&response) {
|
match serde_json::to_string(&response) {
|
||||||
Ok(line) => println!("{line}"),
|
Ok(line) => println!("{line}"),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("mvp-worker-node debug-join: serialize response: {error}");
|
eprintln!("myelin-worker debug-join: serialize response: {error}");
|
||||||
return ExitCode::from(1);
|
return ExitCode::from(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -211,11 +211,11 @@ fn debug_join_client_main(args: Vec<String>) -> ExitCode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(DebugJoinClientError::Cli(error)) => {
|
Err(DebugJoinClientError::Cli(error)) => {
|
||||||
eprintln!("mvp-worker-node debug-join: {error}");
|
eprintln!("myelin-worker debug-join: {error}");
|
||||||
ExitCode::from(2)
|
ExitCode::from(2)
|
||||||
}
|
}
|
||||||
Err(DebugJoinClientError::Runtime(error)) => {
|
Err(DebugJoinClientError::Runtime(error)) => {
|
||||||
eprintln!("mvp-worker-node debug-join: {error}");
|
eprintln!("myelin-worker debug-join: {error}");
|
||||||
ExitCode::from(1)
|
ExitCode::from(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -339,7 +339,7 @@ fn spawn_debug_join_listener(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("mvp-worker-node debug join listener stopped: {error}");
|
eprintln!("myelin-worker debug join listener stopped: {error}");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -483,7 +483,7 @@ fn sampler_health_payload(
|
||||||
"type":"SamplerHealth",
|
"type":"SamplerHealth",
|
||||||
"event_type":"SamplerHealth",
|
"event_type":"SamplerHealth",
|
||||||
"event_name":"host_sampler_health",
|
"event_name":"host_sampler_health",
|
||||||
"schema":"mvp.node.sampler.health.v1",
|
"schema":"myelin.node.sampler.health.v1",
|
||||||
"run_id":context.run_id,
|
"run_id":context.run_id,
|
||||||
"node_id":context.node_id,
|
"node_id":context.node_id,
|
||||||
"stage_index":context.stage_index,
|
"stage_index":context.stage_index,
|
||||||
|
|
@ -494,7 +494,7 @@ fn sampler_health_payload(
|
||||||
"wall_clock_unix_ms":benchmark["wall_clock_unix_ms"].clone(),
|
"wall_clock_unix_ms":benchmark["wall_clock_unix_ms"].clone(),
|
||||||
"monotonic_ms":benchmark["monotonic_ms"].clone(),
|
"monotonic_ms":benchmark["monotonic_ms"].clone(),
|
||||||
"clock_source":benchmark["clock_source"].clone(),
|
"clock_source":benchmark["clock_source"].clone(),
|
||||||
"span_id":format!("mvp-worker-node:{}:{}:{}:host_sampler_health", context.run_id, context.node_id, benchmark["producer_sequence"]),
|
"span_id":format!("myelin-worker:{}:{}:{}:host_sampler_health", context.run_id, context.node_id, benchmark["producer_sequence"]),
|
||||||
"parent_span_id":Value::Null,
|
"parent_span_id":Value::Null,
|
||||||
"phase":"host_sampler_health",
|
"phase":"host_sampler_health",
|
||||||
"status":status,
|
"status":status,
|
||||||
|
|
@ -1013,7 +1013,7 @@ impl WorkerEdgeRuntime {
|
||||||
);
|
);
|
||||||
let step_started = Instant::now();
|
let step_started = Instant::now();
|
||||||
let mut pump = || pump_network(driver, stack);
|
let mut pump = || pump_network(driver, stack);
|
||||||
let committed_bytes = worker.execute_step(
|
let committed_bytes = match worker.execute_step(
|
||||||
u64::from(config.stage_index) + 1,
|
u64::from(config.stage_index) + 1,
|
||||||
step_id,
|
step_id,
|
||||||
object_id,
|
object_id,
|
||||||
|
|
@ -1027,7 +1027,15 @@ impl WorkerEdgeRuntime {
|
||||||
config,
|
config,
|
||||||
datastream,
|
datastream,
|
||||||
&mut pump,
|
&mut pump,
|
||||||
)?;
|
) {
|
||||||
|
Ok(bytes) => bytes,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = stack
|
||||||
|
.runtime
|
||||||
|
.send_to(node_actor, NodeAgentMsg::StepFailed { step_id });
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
let helper_execute_ms = duration_ms_u64(step_started.elapsed());
|
let helper_execute_ms = duration_ms_u64(step_started.elapsed());
|
||||||
let egress_read_started = Instant::now();
|
let egress_read_started = Instant::now();
|
||||||
let record = {
|
let record = {
|
||||||
|
|
@ -1035,9 +1043,16 @@ impl WorkerEdgeRuntime {
|
||||||
let lease = arena
|
let lease = arena
|
||||||
.lookup_lease(arena::RingId(output_ring_id))
|
.lookup_lease(arena::RingId(output_ring_id))
|
||||||
.ok_or_else(|| format!("outbound ring {output_ring_id} lease missing"))?;
|
.ok_or_else(|| format!("outbound ring {output_ring_id} lease missing"))?;
|
||||||
arena
|
arena.read_arena(lease.layout.data_offset, committed_bytes)
|
||||||
.read_arena(lease.layout.data_offset, committed_bytes)
|
};
|
||||||
.map_err(|e| format!("read egress ring: {e}"))?
|
let record = match record {
|
||||||
|
Ok(record) => record,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = stack
|
||||||
|
.runtime
|
||||||
|
.send_to(node_actor, NodeAgentMsg::OutputFault { edge_id: outbound.edge_id });
|
||||||
|
return Err(format!("read egress ring: {e}"));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let egress_read_ms = duration_ms_u64(egress_read_started.elapsed());
|
let egress_read_ms = duration_ms_u64(egress_read_started.elapsed());
|
||||||
let record_bytes = record.len();
|
let record_bytes = record.len();
|
||||||
|
|
@ -1067,7 +1082,12 @@ impl WorkerEdgeRuntime {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| "outbound edge sender missing".to_owned())?;
|
.ok_or_else(|| "outbound edge sender missing".to_owned())?;
|
||||||
let edge_send_started = Instant::now();
|
let edge_send_started = Instant::now();
|
||||||
sender.send(record)?;
|
if let Err(e) = sender.send(record) {
|
||||||
|
let _ = stack
|
||||||
|
.runtime
|
||||||
|
.send_to(node_actor, NodeAgentMsg::OutputFault { edge_id: outbound.edge_id });
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
let edge_send_ms = duration_ms_u64(edge_send_started.elapsed());
|
let edge_send_ms = duration_ms_u64(edge_send_started.elapsed());
|
||||||
node_stage(
|
node_stage(
|
||||||
datastream,
|
datastream,
|
||||||
|
|
@ -1131,8 +1151,17 @@ impl WorkerEdgeRuntime {
|
||||||
buffer.extend_from_slice(&bytes);
|
buffer.extend_from_slice(&bytes);
|
||||||
let buffered_bytes = buffer.len();
|
let buffered_bytes = buffer.len();
|
||||||
let mut records = Vec::new();
|
let mut records = Vec::new();
|
||||||
while let Some(record) = take_complete_ingress_record(buffer, inbound.object_spec)? {
|
loop {
|
||||||
records.push(record);
|
match take_complete_ingress_record(buffer, inbound.object_spec) {
|
||||||
|
Ok(Some(record)) => records.push(record),
|
||||||
|
Ok(None) => break,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = stack
|
||||||
|
.runtime
|
||||||
|
.send_to(node_actor, NodeAgentMsg::ObjectFailed { edge_id, object_id: None });
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(records, buffered_bytes)
|
(records, buffered_bytes)
|
||||||
};
|
};
|
||||||
|
|
@ -1171,14 +1200,22 @@ impl WorkerEdgeRuntime {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
let object_load_started = Instant::now();
|
let object_load_started = Instant::now();
|
||||||
let loaded = worker.ring_readable(
|
let loaded = match worker.ring_readable(
|
||||||
ring_id,
|
ring_id,
|
||||||
edge_id,
|
edge_id,
|
||||||
inbound.object_spec,
|
inbound.object_spec,
|
||||||
config,
|
config,
|
||||||
datastream,
|
datastream,
|
||||||
&mut || {},
|
&mut || {},
|
||||||
)?;
|
) {
|
||||||
|
Ok(loaded) => loaded,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = stack
|
||||||
|
.runtime
|
||||||
|
.send_to(node_actor, NodeAgentMsg::ObjectFailed { edge_id, object_id: Some(record.object_id) });
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
let object_load_ms = duration_ms_u64(object_load_started.elapsed());
|
let object_load_ms = duration_ms_u64(object_load_started.elapsed());
|
||||||
let key = ObjectKey {
|
let key = ObjectKey {
|
||||||
edge_id,
|
edge_id,
|
||||||
|
|
@ -1510,11 +1547,9 @@ impl WorkerEdgeRuntime {
|
||||||
.runtime
|
.runtime
|
||||||
.send_to(
|
.send_to(
|
||||||
node_actor,
|
node_actor,
|
||||||
NodeAgentMsg::WorkerCrashed {
|
NodeAgentMsg::EdgeFault { edge_id: edge_id.0 },
|
||||||
reason: Some(format!("edge {} faulted: {reason:?}", edge_id.0)),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("mark worker crashed after edge fault: {e}"))?;
|
.map_err(|e| format!("report edge fault: {e}"))?;
|
||||||
return Err(format!("edge {} faulted: {reason:?}", edge_id.0));
|
return Err(format!("edge {} faulted: {reason:?}", edge_id.0));
|
||||||
}
|
}
|
||||||
edge::EdgeLifecycleEvent::EdgeStopped { .. } => {}
|
edge::EdgeLifecycleEvent::EdgeStopped { .. } => {}
|
||||||
|
|
@ -1584,7 +1619,7 @@ pub(crate) fn run_from_env() -> ExitCode {
|
||||||
_ => match run() {
|
_ => match run() {
|
||||||
Ok(()) => ExitCode::SUCCESS,
|
Ok(()) => ExitCode::SUCCESS,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("mvp-worker-node: {error}");
|
eprintln!("myelin-worker: {error}");
|
||||||
ExitCode::from(1)
|
ExitCode::from(1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1636,7 +1671,7 @@ fn run() -> Result<(), String> {
|
||||||
boot(
|
boot(
|
||||||
"process",
|
"process",
|
||||||
"started",
|
"started",
|
||||||
json!({"binary":"mvp-worker-node","pid":std::process::id()}),
|
json!({"binary":"myelin-worker","pid":std::process::id()}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let tokio = match tokio::runtime::Runtime::new() {
|
let tokio = match tokio::runtime::Runtime::new() {
|
||||||
|
|
@ -1687,17 +1722,20 @@ fn run() -> Result<(), String> {
|
||||||
boot(
|
boot(
|
||||||
"coordinator_join",
|
"coordinator_join",
|
||||||
"skipped",
|
"skipped",
|
||||||
json!({"reason":"MVP_COORDINATOR_ENDPOINT not set","mode":"standalone"}),
|
json!({"reason":"MYELIN_COORDINATOR_ENDPOINT not set","mode":"standalone"}),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut datastream = NodeDatastream::new(&config);
|
||||||
|
let worker_stats_hook = datastream.producer.stats_hook();
|
||||||
let stack = DistributionRuntimeStack::new_with_codecs(
|
let stack = DistributionRuntimeStack::new_with_codecs(
|
||||||
driver.node_id(),
|
driver.node_id(),
|
||||||
DistributedNodeConfig::default(),
|
DistributedNodeConfig::default(),
|
||||||
|registry| {
|
|registry| {
|
||||||
register_mvp_actor_codecs(registry);
|
register_myelin_actor_codecs(registry);
|
||||||
datastream::wire::register_datastream_codec(registry);
|
datastream::wire::register_datastream_codec(registry);
|
||||||
},
|
},
|
||||||
|
Some(worker_stats_hook),
|
||||||
);
|
);
|
||||||
boot(
|
boot(
|
||||||
"distribution_stack",
|
"distribution_stack",
|
||||||
|
|
@ -1750,7 +1788,6 @@ fn run() -> Result<(), String> {
|
||||||
};
|
};
|
||||||
let arena_fd = arena_manager.lock().arena_fd();
|
let arena_fd = arena_manager.lock().arena_fd();
|
||||||
|
|
||||||
let mut datastream = NodeDatastream::new(&config);
|
|
||||||
let node_boot = |ds: &mut NodeDatastream, phase: &str, status: &str, detail: Value| {
|
let node_boot = |ds: &mut NodeDatastream, phase: &str, status: &str, detail: Value| {
|
||||||
emit_node_event(ds, &config, NODE_BOOTSTRAP_CHANNEL, phase, status, detail)
|
emit_node_event(ds, &config, NODE_BOOTSTRAP_CHANNEL, phase, status, detail)
|
||||||
};
|
};
|
||||||
|
|
@ -1806,7 +1843,7 @@ fn run() -> Result<(), String> {
|
||||||
json!({"actor":datastream_publisher,"name":DATASTREAM_PUBLISHER_NAME,"subscription_transport":"iroh"}),
|
json!({"actor":datastream_publisher,"name":DATASTREAM_PUBLISHER_NAME,"subscription_transport":"iroh"}),
|
||||||
);
|
);
|
||||||
let worker_synthetic_id = format!(
|
let worker_synthetic_id = format!(
|
||||||
"mvp-worker-node-{}-{}-datastream-preflight",
|
"myelin-worker-{}-{}-datastream-preflight",
|
||||||
config.logical_node_id, config.stage_index
|
config.logical_node_id, config.stage_index
|
||||||
);
|
);
|
||||||
for (phase, status) in [
|
for (phase, status) in [
|
||||||
|
|
@ -1820,7 +1857,7 @@ fn run() -> Result<(), String> {
|
||||||
phase,
|
phase,
|
||||||
status,
|
status,
|
||||||
json!({
|
json!({
|
||||||
"producer":"mvp-worker-node",
|
"producer":"myelin-worker",
|
||||||
"producer_class":"rust-worker-node",
|
"producer_class":"rust-worker-node",
|
||||||
"synthetic_id":worker_synthetic_id,
|
"synthetic_id":worker_synthetic_id,
|
||||||
"datastream_endpoint":{
|
"datastream_endpoint":{
|
||||||
|
|
@ -1860,7 +1897,7 @@ fn run() -> Result<(), String> {
|
||||||
&mut datastream,
|
&mut datastream,
|
||||||
"debug_join_socket",
|
"debug_join_socket",
|
||||||
"skipped",
|
"skipped",
|
||||||
json!({"reason":"MVP_DEBUG_JOIN_SOCKET=disabled"}),
|
json!({"reason":"MYELIN_DEBUG_JOIN_SOCKET=disabled"}),
|
||||||
);
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -1881,7 +1918,7 @@ fn run() -> Result<(), String> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let orchestrator = config.orchestrator_actor.ok_or_else(|| {
|
let orchestrator = config.orchestrator_actor.ok_or_else(|| {
|
||||||
"MVP_ORCHESTRATOR_ACTOR is required for runtime readiness signaling".to_owned()
|
"MYELIN_ORCHESTRATOR_ACTOR is required for runtime readiness signaling".to_owned()
|
||||||
})?;
|
})?;
|
||||||
let orchestrator_source = "env";
|
let orchestrator_source = "env";
|
||||||
boot(
|
boot(
|
||||||
|
|
@ -2071,7 +2108,7 @@ fn run() -> Result<(), String> {
|
||||||
&mut datastream,
|
&mut datastream,
|
||||||
"datastream_handoff",
|
"datastream_handoff",
|
||||||
"ready",
|
"ready",
|
||||||
json!({"from":"runtime_ready_ack","to":"cluster_datastream","channel":"mvp.node.ready"}),
|
json!({"from":"runtime_ready_ack","to":"cluster_datastream","channel":"myelin.node.ready"}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2214,7 +2251,7 @@ impl NodeDatastream {
|
||||||
let endpoint = Arc::new(DatastreamEndpoint::with_descriptor(
|
let endpoint = Arc::new(DatastreamEndpoint::with_descriptor(
|
||||||
StreamDescriptor {
|
StreamDescriptor {
|
||||||
stream: stream.clone(),
|
stream: stream.clone(),
|
||||||
label: Some("mvp worker node".to_owned()),
|
label: Some("myelin worker node".to_owned()),
|
||||||
origin: StreamOrigin::RemoteNode,
|
origin: StreamOrigin::RemoteNode,
|
||||||
},
|
},
|
||||||
256,
|
256,
|
||||||
|
|
@ -2232,16 +2269,16 @@ impl NodeDatastream {
|
||||||
NODE_PROMPT_CHANNEL,
|
NODE_PROMPT_CHANNEL,
|
||||||
NODE_SHUTDOWN_CHANNEL,
|
NODE_SHUTDOWN_CHANNEL,
|
||||||
NODE_SAMPLER_CHANNEL,
|
NODE_SAMPLER_CHANNEL,
|
||||||
"mvp.worker.initialize",
|
"myelin.worker.initialize",
|
||||||
"mvp.worker.role",
|
"myelin.worker.role",
|
||||||
"mvp.worker.weights",
|
"myelin.worker.weights",
|
||||||
"mvp.worker.prompt",
|
"myelin.worker.prompt",
|
||||||
"mvp.worker.tokenizer",
|
"myelin.worker.tokenizer",
|
||||||
"mvp.worker.ring",
|
"myelin.worker.ring",
|
||||||
"mvp.worker.ingress",
|
"myelin.worker.ingress",
|
||||||
"mvp.worker.step",
|
"myelin.worker.step",
|
||||||
"mvp.worker.device_object",
|
"myelin.worker.device_object",
|
||||||
"mvp.worker.shutdown",
|
"myelin.worker.shutdown",
|
||||||
] {
|
] {
|
||||||
register_json_channel(&producer, &mut by_name, &mut by_id, name);
|
register_json_channel(&producer, &mut by_name, &mut by_id, name);
|
||||||
}
|
}
|
||||||
|
|
@ -2251,25 +2288,25 @@ impl NodeDatastream {
|
||||||
&producer,
|
&producer,
|
||||||
&mut by_name,
|
&mut by_name,
|
||||||
&mut by_id,
|
&mut by_id,
|
||||||
"mvp.node.ready",
|
"myelin.node.ready",
|
||||||
),
|
),
|
||||||
node_lifecycle: register_json_channel(
|
node_lifecycle: register_json_channel(
|
||||||
&producer,
|
&producer,
|
||||||
&mut by_name,
|
&mut by_name,
|
||||||
&mut by_id,
|
&mut by_id,
|
||||||
"mvp.node.lifecycle",
|
"myelin.node.lifecycle",
|
||||||
),
|
),
|
||||||
node_self_test: register_json_channel(
|
node_self_test: register_json_channel(
|
||||||
&producer,
|
&producer,
|
||||||
&mut by_name,
|
&mut by_name,
|
||||||
&mut by_id,
|
&mut by_id,
|
||||||
"mvp.node.self_test",
|
"myelin.node.self_test",
|
||||||
),
|
),
|
||||||
worker_stderr: register_json_channel(
|
worker_stderr: register_json_channel(
|
||||||
&producer,
|
&producer,
|
||||||
&mut by_name,
|
&mut by_name,
|
||||||
&mut by_id,
|
&mut by_id,
|
||||||
"mvp.worker.stderr",
|
"myelin.worker.stderr",
|
||||||
),
|
),
|
||||||
host_cpu: register_record_channel::<datastream::hardware::cpu::HostCpuSample>(
|
host_cpu: register_record_channel::<datastream::hardware::cpu::HostCpuSample>(
|
||||||
&producer,
|
&producer,
|
||||||
|
|
@ -3218,10 +3255,10 @@ fn publish_stage_shard_fetch_event(
|
||||||
config: &DeploymentConfig,
|
config: &DeploymentConfig,
|
||||||
event: &Value,
|
event: &Value,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let channel = datastream.channel_by_name("mvp.worker.weights");
|
let channel = datastream.channel_by_name("myelin.worker.weights");
|
||||||
let payload = node_event_payload(config, "stage_shard_fetch", "event", event.clone());
|
let payload = node_event_payload(config, "stage_shard_fetch", "event", event.clone());
|
||||||
datastream.submit_text(channel, payload.to_string());
|
datastream.submit_text(channel, payload.to_string());
|
||||||
emit_stdio_datastream_frame("mvp.worker.weights", &payload)
|
emit_stdio_datastream_frame("myelin.worker.weights", &payload)
|
||||||
.map_err(|e| format!("emit stage shard fetch datastream frame: {e}"))?;
|
.map_err(|e| format!("emit stage shard fetch datastream frame: {e}"))?;
|
||||||
datastream.tick();
|
datastream.tick();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -3234,11 +3271,11 @@ fn materialize_stage_shard_with_process(
|
||||||
driver: &mut IrohDriver,
|
driver: &mut IrohDriver,
|
||||||
stack: &DistributionRuntimeStack,
|
stack: &DistributionRuntimeStack,
|
||||||
) -> Result<PathBuf, String> {
|
) -> Result<PathBuf, String> {
|
||||||
let output_path = std::env::var("MVP_MODEL_CACHE_DIR")
|
let output_path = std::env::var("MYELIN_MODEL_CACHE_DIR")
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|value| !value.trim().is_empty())
|
.filter(|value| !value.trim().is_empty())
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.unwrap_or_else(|| PathBuf::from("/var/cache/mvp-models"))
|
.unwrap_or_else(|| PathBuf::from("/var/cache/myelin-models"))
|
||||||
.join(plan.cache_file_name());
|
.join(plan.cache_file_name());
|
||||||
if output_path.is_file() {
|
if output_path.is_file() {
|
||||||
match validate_stage_shard_cache(&output_path, plan) {
|
match validate_stage_shard_cache(&output_path, plan) {
|
||||||
|
|
@ -3560,15 +3597,6 @@ fn handle_stage_command(
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
StageCommandWire::RewireEdge { .. } => {
|
|
||||||
node_stage(
|
|
||||||
datastream,
|
|
||||||
"rewire_edge",
|
|
||||||
"skipped",
|
|
||||||
json!({"reason":"not implemented in mvp-worker-node image path"}),
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
StageCommandWire::ReleaseInputHandle { handle_id, .. } => {
|
StageCommandWire::ReleaseInputHandle { handle_id, .. } => {
|
||||||
edge_runtime.release_input_handle(handle_id, worker, config, datastream, driver, stack)
|
edge_runtime.release_input_handle(handle_id, worker, config, datastream, driver, stack)
|
||||||
}
|
}
|
||||||
|
|
@ -3708,22 +3736,22 @@ impl DeploymentConfig {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let run_id = env_parse!("MVP_RUN_ID", 1)?;
|
let run_id = env_parse!("MYELIN_RUN_ID", 1)?;
|
||||||
let logical_node_id = env_parse!("MVP_LOGICAL_NODE_ID", 1)?;
|
let logical_node_id = env_parse!("MYELIN_LOGICAL_NODE_ID", 1)?;
|
||||||
let relay = relay_runtime_config_from_env(run_id)?;
|
let relay = relay_runtime_config_from_env(run_id)?;
|
||||||
let debug_join_socket = match env_optional("MVP_DEBUG_JOIN_SOCKET").as_deref() {
|
let debug_join_socket = match env_optional("MYELIN_DEBUG_JOIN_SOCKET").as_deref() {
|
||||||
Some("disabled") => None,
|
Some("disabled") => None,
|
||||||
Some(path) => Some(path.to_owned()),
|
Some(path) => Some(path.to_owned()),
|
||||||
None => Some(
|
None => Some(
|
||||||
std::env::temp_dir()
|
std::env::temp_dir()
|
||||||
.join(format!(
|
.join(format!(
|
||||||
"mvp-node-debug-join-{run_id}-{logical_node_id}.sock"
|
"myelin-node-debug-join-{run_id}-{logical_node_id}.sock"
|
||||||
))
|
))
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.into_owned(),
|
.into_owned(),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
let provider = env_optional("MVP_NODE_PROVIDER").unwrap_or_else(|| "process".to_owned());
|
let provider = env_optional("MYELIN_NODE_PROVIDER").unwrap_or_else(|| "process".to_owned());
|
||||||
let default_device = if provider == "process" {
|
let default_device = if provider == "process" {
|
||||||
"CPU"
|
"CPU"
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -3732,20 +3760,20 @@ impl DeploymentConfig {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
run_id,
|
run_id,
|
||||||
logical_node_id,
|
logical_node_id,
|
||||||
stage_index: env_parse!("MVP_STAGE_INDEX", 0)?,
|
stage_index: env_parse!("MYELIN_STAGE_INDEX", 0)?,
|
||||||
coordinator_endpoint: env_optional("MVP_COORDINATOR_ENDPOINT")
|
coordinator_endpoint: env_optional("MYELIN_COORDINATOR_ENDPOINT")
|
||||||
.map(|value| {
|
.map(|value| {
|
||||||
serde_json::from_str::<EndpointAddr>(&value)
|
serde_json::from_str::<EndpointAddr>(&value)
|
||||||
.map_err(|e| format!("invalid MVP_COORDINATOR_ENDPOINT JSON: {e}"))
|
.map_err(|e| format!("invalid MYELIN_COORDINATOR_ENDPOINT JSON: {e}"))
|
||||||
})
|
})
|
||||||
.transpose()?,
|
.transpose()?,
|
||||||
orchestrator_actor: env_optional("MVP_ORCHESTRATOR_ACTOR")
|
orchestrator_actor: env_optional("MYELIN_ORCHESTRATOR_ACTOR")
|
||||||
.map(|value| {
|
.map(|value| {
|
||||||
serde_json::from_str::<ActorAddress>(&value)
|
serde_json::from_str::<ActorAddress>(&value)
|
||||||
.map_err(|e| format!("invalid MVP_ORCHESTRATOR_ACTOR JSON: {e}"))
|
.map_err(|e| format!("invalid MYELIN_ORCHESTRATOR_ACTOR JSON: {e}"))
|
||||||
})
|
})
|
||||||
.transpose()?,
|
.transpose()?,
|
||||||
datastream_frame_log: env_optional("MVP_DATASTREAM_FRAME_LOG"),
|
datastream_frame_log: env_optional("MYELIN_DATASTREAM_FRAME_LOG"),
|
||||||
debug_join_socket,
|
debug_join_socket,
|
||||||
relay_mode: relay.mode,
|
relay_mode: relay.mode,
|
||||||
endpoint_addr_mask: env_optional(MVP_IROH_ENDPOINT_ADDR_MASK_ENV)
|
endpoint_addr_mask: env_optional(MVP_IROH_ENDPOINT_ADDR_MASK_ENV)
|
||||||
|
|
@ -3753,29 +3781,29 @@ impl DeploymentConfig {
|
||||||
.map(EndpointAddrMask::parse)
|
.map(EndpointAddrMask::parse)
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
worker_script: env_optional("MVP_TINYGRAD_WORKER")
|
worker_script: env_optional("MYELIN_TINYGRAD_WORKER")
|
||||||
.unwrap_or_else(|| DEFAULT_WORKER_SCRIPT.to_owned()),
|
.unwrap_or_else(|| DEFAULT_WORKER_SCRIPT.to_owned()),
|
||||||
device: env_optional("DEV").unwrap_or_else(|| default_device.to_owned()),
|
device: env_optional("DEV").unwrap_or_else(|| default_device.to_owned()),
|
||||||
model_id: env_optional("MVP_MODEL_ID").unwrap_or_else(|| DEFAULT_MODEL_ID.to_owned()),
|
model_id: env_optional("MYELIN_MODEL_ID").unwrap_or_else(|| DEFAULT_MODEL_ID.to_owned()),
|
||||||
gguf_source: if let Some(path) = env_optional("MVP_GGUF_LOCAL_PATH") {
|
gguf_source: if let Some(path) = env_optional("MYELIN_GGUF_LOCAL_PATH") {
|
||||||
GgufSource::LocalPath(path)
|
GgufSource::LocalPath(path)
|
||||||
} else {
|
} else {
|
||||||
GgufSource::HuggingFaceGguf {
|
GgufSource::HuggingFaceGguf {
|
||||||
repo: env_optional("MVP_GGUF_REPO")
|
repo: env_optional("MYELIN_GGUF_REPO")
|
||||||
.unwrap_or_else(|| DEFAULT_HF_REPO.to_owned()),
|
.unwrap_or_else(|| DEFAULT_HF_REPO.to_owned()),
|
||||||
file: env_optional("MVP_GGUF_FILE")
|
file: env_optional("MYELIN_GGUF_FILE")
|
||||||
.unwrap_or_else(|| DEFAULT_HF_FILE.to_owned()),
|
.unwrap_or_else(|| DEFAULT_HF_FILE.to_owned()),
|
||||||
revision: env_optional("MVP_GGUF_REVISION"),
|
revision: env_optional("MYELIN_GGUF_REVISION"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tokenizer: env_optional("MVP_TOKENIZER_LOCAL_PATH")
|
tokenizer: env_optional("MYELIN_TOKENIZER_LOCAL_PATH")
|
||||||
.map(TokenizerSource::LocalPath)
|
.map(TokenizerSource::LocalPath)
|
||||||
.unwrap_or(TokenizerSource::EmbeddedGguf),
|
.unwrap_or(TokenizerSource::EmbeddedGguf),
|
||||||
self_test_prompt: env_optional("MVP_NODE_SELF_TEST_PROMPT"),
|
self_test_prompt: env_optional("MYELIN_NODE_SELF_TEST_PROMPT"),
|
||||||
self_test_layer_end: env_parse!("MVP_SELF_TEST_LAYER_END", 16)?,
|
self_test_layer_end: env_parse!("MYELIN_SELF_TEST_LAYER_END", 16)?,
|
||||||
self_test_max_tokens: env_parse!("MVP_SELF_TEST_MAX_TOKENS", 1)?,
|
self_test_max_tokens: env_parse!("MYELIN_SELF_TEST_MAX_TOKENS", 1)?,
|
||||||
arena_bytes: env_parse!("MVP_ARENA_BYTES", DEFAULT_ARENA_BYTES)?,
|
arena_bytes: env_parse!("MYELIN_ARENA_BYTES", DEFAULT_ARENA_BYTES)?,
|
||||||
arena_alignment: env_parse!("MVP_ARENA_ALIGNMENT", DEFAULT_ARENA_ALIGNMENT)?,
|
arena_alignment: env_parse!("MYELIN_ARENA_ALIGNMENT", DEFAULT_ARENA_ALIGNMENT)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4007,20 +4035,20 @@ impl TinygradWorker {
|
||||||
let mut child = Command::new("python3")
|
let mut child = Command::new("python3")
|
||||||
.arg(&config.worker_script)
|
.arg(&config.worker_script)
|
||||||
.env("DEV", &config.device)
|
.env("DEV", &config.device)
|
||||||
.env("MVP_RUN_ID", config.run_id.to_string())
|
.env("MYELIN_RUN_ID", config.run_id.to_string())
|
||||||
.env("MVP_LOGICAL_NODE_ID", config.logical_node_id.to_string())
|
.env("MYELIN_LOGICAL_NODE_ID", config.logical_node_id.to_string())
|
||||||
.env("MVP_STAGE_INDEX", config.stage_index.to_string())
|
.env("MYELIN_STAGE_INDEX", config.stage_index.to_string())
|
||||||
.env("MVP_ARENA_FD", arena_fd.to_string())
|
.env("MYELIN_ARENA_FD", arena_fd.to_string())
|
||||||
.env("MVP_ARENA_BYTES", config.arena_bytes.to_string())
|
.env("MYELIN_ARENA_BYTES", config.arena_bytes.to_string())
|
||||||
.env(
|
.env(
|
||||||
"MVP_DATASTREAM_ENDPOINT_ID",
|
"MYELIN_DATASTREAM_ENDPOINT_ID",
|
||||||
format!(
|
format!(
|
||||||
"worker-node-{}-stage-{}-stdio-bridge",
|
"worker-node-{}-stage-{}-stdio-bridge",
|
||||||
config.logical_node_id, config.stage_index
|
config.logical_node_id, config.stage_index
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.env(
|
.env(
|
||||||
"MVP_BENCHMARK_PRODUCER_INSTANCE",
|
"MYELIN_BENCHMARK_PRODUCER_INSTANCE",
|
||||||
format!(
|
format!(
|
||||||
"tinygrad-worker:{}:{}",
|
"tinygrad-worker:{}:{}",
|
||||||
config.logical_node_id, config.stage_index
|
config.logical_node_id, config.stage_index
|
||||||
|
|
@ -4030,7 +4058,7 @@ impl TinygradWorker {
|
||||||
"MVP_IROH_ENDPOINT_ADDR_MASK",
|
"MVP_IROH_ENDPOINT_ADDR_MASK",
|
||||||
config.endpoint_addr_mask.as_str(),
|
config.endpoint_addr_mask.as_str(),
|
||||||
)
|
)
|
||||||
.env("MVP_IROH_RELAY_MODE", format!("{:?}", config.relay_mode))
|
.env("MYELIN_IROH_RELAY_MODE", format!("{:?}", config.relay_mode))
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
|
|
@ -4078,7 +4106,7 @@ impl TinygradWorker {
|
||||||
"WorkerReady",
|
"WorkerReady",
|
||||||
config,
|
config,
|
||||||
datastream,
|
datastream,
|
||||||
"mvp.worker.initialize",
|
"myelin.worker.initialize",
|
||||||
pump,
|
pump,
|
||||||
)
|
)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
|
|
@ -4108,7 +4136,7 @@ impl TinygradWorker {
|
||||||
"RoleConfigured",
|
"RoleConfigured",
|
||||||
config,
|
config,
|
||||||
datastream,
|
datastream,
|
||||||
"mvp.worker.role",
|
"myelin.worker.role",
|
||||||
pump,
|
pump,
|
||||||
)
|
)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
|
|
@ -4137,7 +4165,7 @@ impl TinygradWorker {
|
||||||
"WeightsLoaded",
|
"WeightsLoaded",
|
||||||
config,
|
config,
|
||||||
datastream,
|
datastream,
|
||||||
"mvp.worker.weights",
|
"myelin.worker.weights",
|
||||||
pump,
|
pump,
|
||||||
)
|
)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
|
|
@ -4157,7 +4185,7 @@ impl TinygradWorker {
|
||||||
"PromptCompleted",
|
"PromptCompleted",
|
||||||
config,
|
config,
|
||||||
datastream,
|
datastream,
|
||||||
"mvp.worker.prompt",
|
"myelin.worker.prompt",
|
||||||
pump,
|
pump,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -4175,7 +4203,7 @@ impl TinygradWorker {
|
||||||
"PromptEncoded",
|
"PromptEncoded",
|
||||||
config,
|
config,
|
||||||
datastream,
|
datastream,
|
||||||
"mvp.worker.tokenizer",
|
"myelin.worker.tokenizer",
|
||||||
pump,
|
pump,
|
||||||
)?;
|
)?;
|
||||||
result
|
result
|
||||||
|
|
@ -4206,7 +4234,7 @@ impl TinygradWorker {
|
||||||
"TokensDecoded",
|
"TokensDecoded",
|
||||||
config,
|
config,
|
||||||
datastream,
|
datastream,
|
||||||
"mvp.worker.tokenizer",
|
"myelin.worker.tokenizer",
|
||||||
pump,
|
pump,
|
||||||
)?;
|
)?;
|
||||||
result
|
result
|
||||||
|
|
@ -4251,7 +4279,7 @@ impl TinygradWorker {
|
||||||
"RingInstalled",
|
"RingInstalled",
|
||||||
config,
|
config,
|
||||||
datastream,
|
datastream,
|
||||||
"mvp.worker.ring",
|
"myelin.worker.ring",
|
||||||
pump,
|
pump,
|
||||||
)
|
)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
|
|
@ -4269,7 +4297,7 @@ impl TinygradWorker {
|
||||||
"RingUninstalled",
|
"RingUninstalled",
|
||||||
config,
|
config,
|
||||||
datastream,
|
datastream,
|
||||||
"mvp.worker.ring",
|
"myelin.worker.ring",
|
||||||
pump,
|
pump,
|
||||||
)
|
)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
|
|
@ -4297,7 +4325,7 @@ impl TinygradWorker {
|
||||||
"ObjectLoaded",
|
"ObjectLoaded",
|
||||||
config,
|
config,
|
||||||
datastream,
|
datastream,
|
||||||
"mvp.worker.ingress",
|
"myelin.worker.ingress",
|
||||||
pump,
|
pump,
|
||||||
)?;
|
)?;
|
||||||
Ok(LoadedObject {
|
Ok(LoadedObject {
|
||||||
|
|
@ -4345,7 +4373,7 @@ impl TinygradWorker {
|
||||||
"StepExecuted",
|
"StepExecuted",
|
||||||
config,
|
config,
|
||||||
datastream,
|
datastream,
|
||||||
"mvp.worker.step",
|
"myelin.worker.step",
|
||||||
pump,
|
pump,
|
||||||
)?;
|
)?;
|
||||||
usize::try_from(value_u64(&event, "committed_bytes")?)
|
usize::try_from(value_u64(&event, "committed_bytes")?)
|
||||||
|
|
@ -4364,7 +4392,7 @@ impl TinygradWorker {
|
||||||
"DeviceObjectReleased",
|
"DeviceObjectReleased",
|
||||||
config,
|
config,
|
||||||
datastream,
|
datastream,
|
||||||
"mvp.worker.device_object",
|
"myelin.worker.device_object",
|
||||||
pump,
|
pump,
|
||||||
)
|
)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
|
|
@ -4381,7 +4409,7 @@ impl TinygradWorker {
|
||||||
"WorkerStopped",
|
"WorkerStopped",
|
||||||
config,
|
config,
|
||||||
datastream,
|
datastream,
|
||||||
"mvp.worker.shutdown",
|
"myelin.worker.shutdown",
|
||||||
pump,
|
pump,
|
||||||
)
|
)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! Dashboard view over MVP cluster provisioning and lifecycle datastream records.
|
//! Dashboard view over Myelin cluster provisioning and lifecycle datastream records.
|
||||||
|
|
||||||
use std::collections::{BTreeMap, VecDeque};
|
use std::collections::{BTreeMap, VecDeque};
|
||||||
|
|
||||||
|
|
@ -12,29 +12,29 @@ use std::sync::RwLock;
|
||||||
|
|
||||||
use crate::observability::lifecycle as obs;
|
use crate::observability::lifecycle as obs;
|
||||||
use crate::observability::telemetry::{
|
use crate::observability::telemetry::{
|
||||||
MVP_LIFECYCLE, MVP_PROVISIONING_EVENTS, MVP_PROVISIONING_LOGS, MvpLifecycleRecord,
|
MYELIN_LIFECYCLE, MYELIN_PROVISIONING_EVENTS, MYELIN_PROVISIONING_LOGS, MyelinLifecycleRecord,
|
||||||
MvpProvisionEventRecord, MvpProvisionLogRecord,
|
MyelinProvisionEventRecord, MyelinProvisionLogRecord,
|
||||||
};
|
};
|
||||||
use crate::provisioning::{ProvisionEventKind, ProvisionLogStream};
|
use crate::provisioning::{ProvisionEventKind, ProvisionLogStream};
|
||||||
|
|
||||||
const CHANNELS: &[&str] = &[];
|
const CHANNELS: &[&str] = &[];
|
||||||
const PROVISIONING_LOG_PREFIX: &str = "mvp.provisioning.logs.node.";
|
const PROVISIONING_LOG_PREFIX: &str = "myelin.provisioning.logs.node.";
|
||||||
const EVENT_LOG_CAP: usize = 256;
|
const EVENT_LOG_CAP: usize = 256;
|
||||||
const LOG_TAIL_CAP: usize = 128;
|
const LOG_TAIL_CAP: usize = 128;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(crate) struct MvpClusterDashboardView {
|
pub(crate) struct MyelinClusterDashboardView {
|
||||||
state: RwLock<MvpClusterDashboardState>,
|
state: RwLock<MyelinClusterDashboardState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MvpClusterDashboardView {
|
impl MyelinClusterDashboardView {
|
||||||
pub(crate) fn new() -> Self {
|
pub(crate) fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Serialize)]
|
#[derive(Default, Serialize)]
|
||||||
struct MvpClusterDashboardState {
|
struct MyelinClusterDashboardState {
|
||||||
events: VecDeque<DashboardEventEntry>,
|
events: VecDeque<DashboardEventEntry>,
|
||||||
nodes: BTreeMap<u64, ProvisionNodeView>,
|
nodes: BTreeMap<u64, ProvisionNodeView>,
|
||||||
}
|
}
|
||||||
|
|
@ -58,7 +58,7 @@ struct ProvisionNodeView {
|
||||||
provider_tail: VecDeque<String>,
|
provider_tail: VecDeque<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MvpClusterDashboardState {
|
impl MyelinClusterDashboardState {
|
||||||
fn push_event(&mut self, entry: DashboardEventEntry) {
|
fn push_event(&mut self, entry: DashboardEventEntry) {
|
||||||
if self.events.len() == EVENT_LOG_CAP {
|
if self.events.len() == EVENT_LOG_CAP {
|
||||||
self.events.pop_front();
|
self.events.pop_front();
|
||||||
|
|
@ -66,7 +66,7 @@ impl MvpClusterDashboardState {
|
||||||
self.events.push_back(entry);
|
self.events.push_back(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_provision_event(&mut self, frame: &Frame, record: MvpProvisionEventRecord) {
|
fn apply_provision_event(&mut self, frame: &Frame, record: MyelinProvisionEventRecord) {
|
||||||
let phase = provision_kind_label(record.event.kind);
|
let phase = provision_kind_label(record.event.kind);
|
||||||
let node = self
|
let node = self
|
||||||
.nodes
|
.nodes
|
||||||
|
|
@ -78,7 +78,7 @@ impl MvpClusterDashboardState {
|
||||||
node.phase = phase.to_owned();
|
node.phase = phase.to_owned();
|
||||||
node.last_message = record.event.message.clone();
|
node.last_message = record.event.message.clone();
|
||||||
self.push_event(DashboardEventEntry {
|
self.push_event(DashboardEventEntry {
|
||||||
channel: MVP_PROVISIONING_EVENTS.to_owned(),
|
channel: MYELIN_PROVISIONING_EVENTS.to_owned(),
|
||||||
position: frame.position.0,
|
position: frame.position.0,
|
||||||
label: phase.to_owned(),
|
label: phase.to_owned(),
|
||||||
run_id: Some(record.event.run_id),
|
run_id: Some(record.event.run_id),
|
||||||
|
|
@ -86,7 +86,7 @@ impl MvpClusterDashboardState {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_provision_log(&mut self, record: MvpProvisionLogRecord) {
|
fn apply_provision_log(&mut self, record: MyelinProvisionLogRecord) {
|
||||||
let node = self
|
let node = self
|
||||||
.nodes
|
.nodes
|
||||||
.entry(record.line.node_id)
|
.entry(record.line.node_id)
|
||||||
|
|
@ -105,10 +105,10 @@ impl MvpClusterDashboardState {
|
||||||
tail.push_back(record.line.line);
|
tail.push_back(record.line.line);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_lifecycle(&mut self, frame: &Frame, record: MvpLifecycleRecord) {
|
fn apply_lifecycle(&mut self, frame: &Frame, record: MyelinLifecycleRecord) {
|
||||||
let (run_id, node_id) = lifecycle_ids(&record.event);
|
let (run_id, node_id) = lifecycle_ids(&record.event);
|
||||||
self.push_event(DashboardEventEntry {
|
self.push_event(DashboardEventEntry {
|
||||||
channel: MVP_LIFECYCLE.to_owned(),
|
channel: MYELIN_LIFECYCLE.to_owned(),
|
||||||
position: frame.position.0,
|
position: frame.position.0,
|
||||||
label: format!("{:?}", record.event.kind()),
|
label: format!("{:?}", record.event.kind()),
|
||||||
run_id,
|
run_id,
|
||||||
|
|
@ -117,13 +117,13 @@ impl MvpClusterDashboardState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DashboardView for MvpClusterDashboardView {
|
impl DashboardView for MyelinClusterDashboardView {
|
||||||
fn id(&self) -> &'static str {
|
fn id(&self) -> &'static str {
|
||||||
"mvp/cluster"
|
"myelin/cluster"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn title(&self) -> &'static str {
|
fn title(&self) -> &'static str {
|
||||||
"MVP cluster"
|
"Myelin cluster"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn channels(&self) -> &'static [&'static str] {
|
fn channels(&self) -> &'static [&'static str] {
|
||||||
|
|
@ -131,23 +131,23 @@ impl DashboardView for MvpClusterDashboardView {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ingest(&self, _stream: &StreamId, frame: &Frame, event: &FrameEvent) {
|
fn ingest(&self, _stream: &StreamId, frame: &Frame, event: &FrameEvent) {
|
||||||
let mut state = self.state.write().expect("MVP dashboard state poisoned");
|
let mut state = self.state.write().expect("Myelin dashboard state poisoned");
|
||||||
match event.channel.as_str() {
|
match event.channel.as_str() {
|
||||||
MVP_PROVISIONING_EVENTS => {
|
MYELIN_PROVISIONING_EVENTS => {
|
||||||
if let Ok(record) = MvpProvisionEventRecord::decode(&event.payload) {
|
if let Ok(record) = MyelinProvisionEventRecord::decode(&event.payload) {
|
||||||
state.apply_provision_event(frame, record);
|
state.apply_provision_event(frame, record);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
channel
|
channel
|
||||||
if channel == MVP_PROVISIONING_LOGS
|
if channel == MYELIN_PROVISIONING_LOGS
|
||||||
|| channel.starts_with(PROVISIONING_LOG_PREFIX) =>
|
|| channel.starts_with(PROVISIONING_LOG_PREFIX) =>
|
||||||
{
|
{
|
||||||
if let Ok(record) = MvpProvisionLogRecord::decode(&event.payload) {
|
if let Ok(record) = MyelinProvisionLogRecord::decode(&event.payload) {
|
||||||
state.apply_provision_log(record);
|
state.apply_provision_log(record);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MVP_LIFECYCLE => {
|
MYELIN_LIFECYCLE => {
|
||||||
if let Ok(record) = MvpLifecycleRecord::decode(&event.payload) {
|
if let Ok(record) = MyelinLifecycleRecord::decode(&event.payload) {
|
||||||
state.apply_lifecycle(frame, record);
|
state.apply_lifecycle(frame, record);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -156,7 +156,7 @@ impl DashboardView for MvpClusterDashboardView {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn snapshot_json(&self) -> Value {
|
fn snapshot_json(&self) -> Value {
|
||||||
let state = self.state.read().expect("MVP dashboard state poisoned");
|
let state = self.state.read().expect("Myelin dashboard state poisoned");
|
||||||
json!({
|
json!({
|
||||||
"events": state.events,
|
"events": state.events,
|
||||||
"nodes": state.nodes.values().collect::<Vec<_>>(),
|
"nodes": state.nodes.values().collect::<Vec<_>>(),
|
||||||
|
|
@ -164,7 +164,7 @@ impl DashboardView for MvpClusterDashboardView {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn html(&self) -> Option<&'static str> {
|
fn html(&self) -> Option<&'static str> {
|
||||||
Some(MVP_CLUSTER_HTML)
|
Some(MYELIN_CLUSTER_HTML)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,11 +193,11 @@ fn lifecycle_ids(event: &obs::Event) -> (Option<u64>, Option<u64>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const MVP_CLUSTER_HTML: &str = r#"<!doctype html>
|
const MYELIN_CLUSTER_HTML: &str = r#"<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>MVP cluster</title>
|
<title>Myelin cluster</title>
|
||||||
<style>
|
<style>
|
||||||
body { font: 13px system-ui, sans-serif; margin: 1rem; background: #0f1115; color: #e8e8e8; }
|
body { font: 13px system-ui, sans-serif; margin: 1rem; background: #0f1115; color: #e8e8e8; }
|
||||||
table { border-collapse: collapse; width: 100%; margin-bottom: 1rem; }
|
table { border-collapse: collapse; width: 100%; margin-bottom: 1rem; }
|
||||||
|
|
@ -211,7 +211,7 @@ const MVP_CLUSTER_HTML: &str = r#"<!doctype html>
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>MVP cluster</h1>
|
<h1>Myelin cluster</h1>
|
||||||
<p id="status" class="muted">loading…</p>
|
<p id="status" class="muted">loading…</p>
|
||||||
<h2>Provisioned nodes</h2>
|
<h2>Provisioned nodes</h2>
|
||||||
<table><thead><tr><th>node</th><th>phase</th><th>message</th></tr></thead><tbody id="nodes"></tbody></table>
|
<table><thead><tr><th>node</th><th>phase</th><th>message</th></tr></thead><tbody id="nodes"></tbody></table>
|
||||||
|
|
@ -222,7 +222,7 @@ const MVP_CLUSTER_HTML: &str = r#"<!doctype html>
|
||||||
<script>
|
<script>
|
||||||
let selected = null;
|
let selected = null;
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
const data = await fetch('/api/view/mvp/cluster').then(r => r.json());
|
const data = await fetch('/api/view/myelin/cluster').then(r => r.json());
|
||||||
document.getElementById('status').textContent = `${data.nodes.length} node(s), ${data.events.length} event(s)`;
|
document.getElementById('status').textContent = `${data.nodes.length} node(s), ${data.events.length} event(s)`;
|
||||||
const nodes = document.getElementById('nodes');
|
const nodes = document.getElementById('nodes');
|
||||||
nodes.innerHTML = data.nodes.map(n => `<tr data-node="${n.node_id}"><td>${n.node_id}</td><td class="${n.phase === 'provision_failed' ? 'bad' : 'ok'}">${n.phase || ''}</td><td>${n.last_message || ''}</td></tr>`).join('') || '<tr><td colspan="3" class="muted">No provisioning records yet.</td></tr>';
|
nodes.innerHTML = data.nodes.map(n => `<tr data-node="${n.node_id}"><td>${n.node_id}</td><td class="${n.phase === 'provision_failed' ? 'bad' : 'ok'}">${n.phase || ''}</td><td>${n.last_message || ''}</td></tr>`).join('') || '<tr><td colspan="3" class="muted">No provisioning records yet.</td></tr>';
|
||||||
|
|
@ -7,9 +7,9 @@ use serde_json::json;
|
||||||
|
|
||||||
use crate::observability::benchmark;
|
use crate::observability::benchmark;
|
||||||
|
|
||||||
/// JSONL archive for MVP-owned datastream frames.
|
/// JSONL archive for Myelin-owned datastream frames.
|
||||||
///
|
///
|
||||||
/// The datastream crate owns frame transport; this helper owns the MVP archive
|
/// The datastream crate owns frame transport; this helper owns the Myelin archive
|
||||||
/// record shape used as benchmark and contract evidence.
|
/// record shape used as benchmark and contract evidence.
|
||||||
pub(crate) struct FrameArchive {
|
pub(crate) struct FrameArchive {
|
||||||
file: File,
|
file: File,
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! MVP observability public surface.
|
//! Myelin observability public surface.
|
||||||
|
|
||||||
pub(crate) mod benchmark;
|
pub(crate) mod benchmark;
|
||||||
#[cfg(feature = "dashboard")]
|
#[cfg(feature = "dashboard")]
|
||||||
|
|
@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::observability::telemetry::{
|
use crate::observability::telemetry::{
|
||||||
MVP_PROVISIONING_LOGS, MvpProvisionLogRecord, mvp_provision_log_channel,
|
MYELIN_PROVISIONING_LOGS, MyelinProvisionLogRecord, myelin_provision_log_channel,
|
||||||
};
|
};
|
||||||
use crate::provisioning::{
|
use crate::provisioning::{
|
||||||
NodeProvisionSpec, PluginObservation, PluginSink, ProvisionLogLine, ProvisionLogStream,
|
NodeProvisionSpec, PluginObservation, PluginSink, ProvisionLogLine, ProvisionLogStream,
|
||||||
|
|
@ -135,16 +135,16 @@ impl BootstrapDatastreamBridge {
|
||||||
let Some(producer) = &self.producer else {
|
let Some(producer) = &self.producer else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let record = MvpProvisionLogRecord::new(ProvisionLogLine {
|
let record = MyelinProvisionLogRecord::new(ProvisionLogLine {
|
||||||
run_id: self.spec.run_id,
|
run_id: self.spec.run_id,
|
||||||
node_id: self.spec.node_id,
|
node_id: self.spec.node_id,
|
||||||
stream,
|
stream,
|
||||||
line: line.to_owned(),
|
line: line.to_owned(),
|
||||||
});
|
});
|
||||||
let channel = producer.register_channel(
|
let channel = producer.register_channel(
|
||||||
mvp_provision_log_channel(self.spec.node_id, stream),
|
myelin_provision_log_channel(self.spec.node_id, stream),
|
||||||
ChannelContent::JsonRecord {
|
ChannelContent::JsonRecord {
|
||||||
schema: Some(MVP_PROVISIONING_LOGS.to_owned()),
|
schema: Some(MYELIN_PROVISIONING_LOGS.to_owned()),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let payload = serde_json::to_vec(&record).expect("serialize bootstrap log record");
|
let payload = serde_json::to_vec(&record).expect("serialize bootstrap log record");
|
||||||
|
|
@ -154,7 +154,7 @@ impl BootstrapDatastreamBridge {
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
struct StdioDatastreamFrame {
|
struct StdioDatastreamFrame {
|
||||||
mvp_stdio_event: u32,
|
myelin_stdio_event: u32,
|
||||||
kind: String,
|
kind: String,
|
||||||
channel: String,
|
channel: String,
|
||||||
payload: Value,
|
payload: Value,
|
||||||
|
|
@ -165,7 +165,7 @@ pub(crate) fn parse_stdio_datastream_frame(
|
||||||
line: &str,
|
line: &str,
|
||||||
) -> Option<PluginObservation> {
|
) -> Option<PluginObservation> {
|
||||||
let frame = serde_json::from_str::<StdioDatastreamFrame>(line).ok()?;
|
let frame = serde_json::from_str::<StdioDatastreamFrame>(line).ok()?;
|
||||||
if frame.mvp_stdio_event != 1 || frame.kind != "datastream_frame" {
|
if frame.myelin_stdio_event != 1 || frame.kind != "datastream_frame" {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(PluginObservation::DatastreamFrame {
|
Some(PluginObservation::DatastreamFrame {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
//! MVP-system-owned datastream channel records.
|
//! Myelin-system-owned datastream channel records.
|
||||||
|
|
||||||
use datastream::Record;
|
use datastream::Record;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -7,61 +7,61 @@ use serde::{Deserialize, Serialize};
|
||||||
use crate::observability::lifecycle as obs;
|
use crate::observability::lifecycle as obs;
|
||||||
use crate::provisioning::{self, ProvisionLogStream};
|
use crate::provisioning::{self, ProvisionLogStream};
|
||||||
|
|
||||||
/// Structured MVP lifecycle facts: run, node, stage, edge, ring, object, step, and worker events.
|
/// Structured Myelin lifecycle facts: run, node, stage, edge, ring, object, step, and worker events.
|
||||||
pub(crate) const MVP_LIFECYCLE: &str = "mvp.lifecycle";
|
pub(crate) const MYELIN_LIFECYCLE: &str = "myelin.lifecycle";
|
||||||
/// Structured node provisioning milestones emitted before a remote swactor runtime is live.
|
/// Structured node provisioning milestones emitted before a remote swactor runtime is live.
|
||||||
pub(crate) const MVP_PROVISIONING_EVENTS: &str = "mvp.provisioning.events";
|
pub(crate) const MYELIN_PROVISIONING_EVENTS: &str = "myelin.provisioning.events";
|
||||||
|
|
||||||
/// Raw provider/process stream lines captured during provisioning.
|
/// Raw provider/process stream lines captured during provisioning.
|
||||||
pub(crate) const MVP_PROVISIONING_LOGS: &str = "mvp.provisioning.logs";
|
pub(crate) const MYELIN_PROVISIONING_LOGS: &str = "myelin.provisioning.logs";
|
||||||
|
|
||||||
/// Datastream payload for the MVP lifecycle channel.
|
/// Datastream payload for the Myelin lifecycle channel.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub(crate) struct MvpLifecycleRecord {
|
pub(crate) struct MyelinLifecycleRecord {
|
||||||
pub event: obs::Event,
|
pub event: obs::Event,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Record for MvpLifecycleRecord {
|
impl Record for MyelinLifecycleRecord {
|
||||||
const CHANNEL: &'static str = MVP_LIFECYCLE;
|
const CHANNEL: &'static str = MYELIN_LIFECYCLE;
|
||||||
}
|
}
|
||||||
/// Datastream payload for provisioning lifecycle events.
|
/// Datastream payload for provisioning lifecycle events.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub(crate) struct MvpProvisionEventRecord {
|
pub(crate) struct MyelinProvisionEventRecord {
|
||||||
pub event: provisioning::ProvisionEvent,
|
pub event: provisioning::ProvisionEvent,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MvpProvisionEventRecord {
|
impl MyelinProvisionEventRecord {
|
||||||
pub(crate) fn new(event: provisioning::ProvisionEvent) -> Self {
|
pub(crate) fn new(event: provisioning::ProvisionEvent) -> Self {
|
||||||
Self { event }
|
Self { event }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Record for MvpProvisionEventRecord {
|
impl Record for MyelinProvisionEventRecord {
|
||||||
const CHANNEL: &'static str = MVP_PROVISIONING_EVENTS;
|
const CHANNEL: &'static str = MYELIN_PROVISIONING_EVENTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Datastream payload for provisioning stdout/stderr/provider lines.
|
/// Datastream payload for provisioning stdout/stderr/provider lines.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub(crate) struct MvpProvisionLogRecord {
|
pub(crate) struct MyelinProvisionLogRecord {
|
||||||
pub line: provisioning::ProvisionLogLine,
|
pub line: provisioning::ProvisionLogLine,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MvpProvisionLogRecord {
|
impl MyelinProvisionLogRecord {
|
||||||
pub(crate) fn new(line: provisioning::ProvisionLogLine) -> Self {
|
pub(crate) fn new(line: provisioning::ProvisionLogLine) -> Self {
|
||||||
Self { line }
|
Self { line }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn mvp_provision_log_channel(node_id: u64, stream: ProvisionLogStream) -> String {
|
pub(crate) fn myelin_provision_log_channel(node_id: u64, stream: ProvisionLogStream) -> String {
|
||||||
let stream = match stream {
|
let stream = match stream {
|
||||||
ProvisionLogStream::Stdout => "stdout",
|
ProvisionLogStream::Stdout => "stdout",
|
||||||
ProvisionLogStream::Stderr => "stderr",
|
ProvisionLogStream::Stderr => "stderr",
|
||||||
ProvisionLogStream::Provider => "provider",
|
ProvisionLogStream::Provider => "provider",
|
||||||
};
|
};
|
||||||
format!("mvp.provisioning.logs.node.{node_id}.{stream}")
|
format!("myelin.provisioning.logs.node.{node_id}.{stream}")
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Record for MvpProvisionLogRecord {
|
impl Record for MyelinProvisionLogRecord {
|
||||||
const CHANNEL: &'static str = MVP_PROVISIONING_LOGS;
|
const CHANNEL: &'static str = MYELIN_PROVISIONING_LOGS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,6 +68,13 @@ pub(crate) enum OrchestratorMsg {
|
||||||
stage_index: u32,
|
stage_index: u32,
|
||||||
},
|
},
|
||||||
ObserveTokenEndpointsStopped,
|
ObserveTokenEndpointsStopped,
|
||||||
|
ObserveOperatorStop {
|
||||||
|
run_id: u64,
|
||||||
|
},
|
||||||
|
ObserveMembershipLost {
|
||||||
|
run_id: u64,
|
||||||
|
node_id: u64,
|
||||||
|
},
|
||||||
AdvanceTimeMs(u64),
|
AdvanceTimeMs(u64),
|
||||||
Snapshot {
|
Snapshot {
|
||||||
reply_to: ActorAddress,
|
reply_to: ActorAddress,
|
||||||
|
|
@ -76,7 +83,7 @@ pub(crate) enum OrchestratorMsg {
|
||||||
|
|
||||||
impl NetworkMessage for OrchestratorMsg {
|
impl NetworkMessage for OrchestratorMsg {
|
||||||
fn type_tag() -> &'static str {
|
fn type_tag() -> &'static str {
|
||||||
"mvp_system::OrchestratorMsg"
|
"myelin::OrchestratorMsg"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,7 +138,6 @@ pub(crate) enum RunCommandWire {
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub(crate) enum LifecycleEventWire {
|
pub(crate) enum LifecycleEventWire {
|
||||||
RunRejected { run_id: u64 },
|
|
||||||
RunFaulted { run_id: u64 },
|
RunFaulted { run_id: u64 },
|
||||||
RunCompleted { run_id: u64 },
|
RunCompleted { run_id: u64 },
|
||||||
RunOperatorStopped { run_id: u64 },
|
RunOperatorStopped { run_id: u64 },
|
||||||
|
|
@ -180,7 +186,7 @@ pub(crate) enum OrchestratorReport {
|
||||||
|
|
||||||
impl NetworkMessage for OrchestratorReport {
|
impl NetworkMessage for OrchestratorReport {
|
||||||
fn type_tag() -> &'static str {
|
fn type_tag() -> &'static str {
|
||||||
"mvp_system::OrchestratorReport"
|
"myelin::OrchestratorReport"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,6 +278,17 @@ impl OrchestratorActor {
|
||||||
OrchestratorMsg::ObserveTokenEndpointsStopped => {
|
OrchestratorMsg::ObserveTokenEndpointsStopped => {
|
||||||
self.core.observe(core::RunEvent::TokenEndpointsStopped)
|
self.core.observe(core::RunEvent::TokenEndpointsStopped)
|
||||||
}
|
}
|
||||||
|
OrchestratorMsg::ObserveOperatorStop { run_id } => {
|
||||||
|
self.core.observe(core::RunEvent::OperatorStop {
|
||||||
|
run_id: core::RunId(run_id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
OrchestratorMsg::ObserveMembershipLost { run_id, node_id } => {
|
||||||
|
self.core.observe(core::RunEvent::MembershipLost {
|
||||||
|
run_id: core::RunId(run_id),
|
||||||
|
node_id: core::NodeId(node_id),
|
||||||
|
});
|
||||||
|
}
|
||||||
OrchestratorMsg::AdvanceTimeMs(delta) => self.core.advance_time_ms(delta),
|
OrchestratorMsg::AdvanceTimeMs(delta) => self.core.advance_time_ms(delta),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -449,9 +466,6 @@ impl From<&core::RunCommand> for RunCommandWire {
|
||||||
impl From<&core::LifecycleEvent> for LifecycleEventWire {
|
impl From<&core::LifecycleEvent> for LifecycleEventWire {
|
||||||
fn from(event: &core::LifecycleEvent) -> Self {
|
fn from(event: &core::LifecycleEvent) -> Self {
|
||||||
match event {
|
match event {
|
||||||
core::LifecycleEvent::RunRejected { run_id, .. } => {
|
|
||||||
Self::RunRejected { run_id: run_id.0 }
|
|
||||||
}
|
|
||||||
core::LifecycleEvent::RunFaulted { run_id, .. } => {
|
core::LifecycleEvent::RunFaulted { run_id, .. } => {
|
||||||
Self::RunFaulted { run_id: run_id.0 }
|
Self::RunFaulted { run_id: run_id.0 }
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -88,7 +88,7 @@ pub(crate) fn looks_remote_image(image: &str) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared overlay for legacy and multi-binary configuration parsing. This accepts
|
/// Shared overlay for legacy and multi-binary configuration parsing. This accepts
|
||||||
/// fields outside the fixed `mvp-chat` public config surface; `mvp-chat` uses a
|
/// fields outside the fixed `myelin-chat` public config surface; `myelin-chat` uses a
|
||||||
/// bin-local strict config loader instead.
|
/// bin-local strict config loader instead.
|
||||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
//! MVP-system swactor distribution runtime wiring.
|
//! Myelin-system swactor distribution runtime wiring.
|
||||||
//!
|
//!
|
||||||
//! This is the production version of the actor-stack setup that integration
|
//! This is the production version of the actor-stack setup that integration
|
||||||
//! tests used to copy by hand: a swactor runtime, the four distribution protocol
|
//! tests used to copy by hand: a swactor runtime, the four distribution protocol
|
||||||
|
|
@ -13,6 +13,7 @@ use std::time::{Duration, Instant};
|
||||||
use swactor::actor::{ActorAddress, ActorInterface};
|
use swactor::actor::{ActorAddress, ActorInterface};
|
||||||
use swactor::config::RuntimeConfig;
|
use swactor::config::RuntimeConfig;
|
||||||
use swactor::runtime::{Ctx, Runtime};
|
use swactor::runtime::{Ctx, Runtime};
|
||||||
|
use swactor::stats::StatsHook;
|
||||||
use swactor::std::StdExtension;
|
use swactor::std::StdExtension;
|
||||||
use swactor_transport::{CodecRegistry, CodecRemoteSink, NetworkMessage, TransportRouter};
|
use swactor_transport::{CodecRegistry, CodecRemoteSink, NetworkMessage, TransportRouter};
|
||||||
|
|
||||||
|
|
@ -44,7 +45,6 @@ pub(crate) struct DistributionActorAddrs {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct DistributionRuntimeStack {
|
pub(crate) struct DistributionRuntimeStack {
|
||||||
pub node_id: NodeId,
|
|
||||||
pub runtime: Arc<Runtime>,
|
pub runtime: Arc<Runtime>,
|
||||||
pub codec: Arc<CodecRegistry>,
|
pub codec: Arc<CodecRegistry>,
|
||||||
pub outbox: Outbox,
|
pub outbox: Outbox,
|
||||||
|
|
@ -61,6 +61,7 @@ impl DistributionRuntimeStack {
|
||||||
node_id: NodeId,
|
node_id: NodeId,
|
||||||
config: DistributedNodeConfig,
|
config: DistributedNodeConfig,
|
||||||
extend_codecs: impl FnOnce(&mut CodecRegistry),
|
extend_codecs: impl FnOnce(&mut CodecRegistry),
|
||||||
|
stats_hook: Option<Arc<dyn StatsHook>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut runtime =
|
let mut runtime =
|
||||||
Runtime::new(RuntimeConfig::default()).with_extension(Arc::new(StdExtension::new()));
|
Runtime::new(RuntimeConfig::default()).with_extension(Arc::new(StdExtension::new()));
|
||||||
|
|
@ -72,6 +73,9 @@ impl DistributionRuntimeStack {
|
||||||
Arc::clone(&codec),
|
Arc::clone(&codec),
|
||||||
Arc::clone(&transport_router),
|
Arc::clone(&transport_router),
|
||||||
)));
|
)));
|
||||||
|
if let Some(hook) = stats_hook {
|
||||||
|
runtime.set_stats_hook(hook);
|
||||||
|
}
|
||||||
let runtime = Arc::new(runtime);
|
let runtime = Arc::new(runtime);
|
||||||
|
|
||||||
let outbox: Outbox = Arc::new(Mutex::new(Vec::new()));
|
let outbox: Outbox = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
|
@ -146,7 +150,6 @@ impl DistributionRuntimeStack {
|
||||||
.expect("subscribe MembershipFanout");
|
.expect("subscribe MembershipFanout");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
node_id,
|
|
||||||
runtime,
|
runtime,
|
||||||
codec,
|
codec,
|
||||||
outbox,
|
outbox,
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! MVP run orchestration public surface.
|
//! Myelin run orchestration public surface.
|
||||||
//!
|
//!
|
||||||
//! This module owns run planning, membership/readiness coordination, deployment
|
//! This module owns run planning, membership/readiness coordination, deployment
|
||||||
//! dispatch, run authority, prompt admission, and teardown behavior. Provider
|
//! dispatch, run authority, prompt admission, and teardown behavior. Provider
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! MVP-facing compatibility surface for reusable provider-neutral node provisioning.
|
//! Myelin-facing compatibility surface for reusable provider-neutral node provisioning.
|
||||||
//!
|
//!
|
||||||
//! The reusable crate keeps `ProviderKind` opaque. MVP provider selection policy
|
//! The reusable crate keeps `ProviderKind` opaque. Myelin provider selection policy
|
||||||
//! lives here so the reusable contracts do not know about process, Docker, or
|
//! lives here so the reusable contracts do not know about process, Docker, or
|
||||||
//! VastAI runtime choices.
|
//! VastAI runtime choices.
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
//! Relay provisioning shims for MVP runtimes.
|
//! Relay provisioning shims for Myelin runtimes.
|
||||||
//!
|
//!
|
||||||
//! The current implementations are deliberately small: local tests get the same
|
//! The current implementations are deliberately small: local tests get the same
|
||||||
//! abstraction without an external relay, and deploy runs can point every node at
|
//! abstraction without an external relay, and deploy runs can point every node at
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
use iroh::{RelayMode, RelayUrl};
|
use iroh::{RelayMode, RelayUrl};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub(crate) const MVP_IROH_RELAY_MODE_ENV: &str = "MVP_IROH_RELAY_MODE";
|
pub(crate) const MYELIN_IROH_RELAY_MODE_ENV: &str = "MYELIN_IROH_RELAY_MODE";
|
||||||
pub(crate) const MVP_IROH_RELAY_URL_ENV: &str = "MVP_IROH_RELAY_URL";
|
pub(crate) const MYELIN_IROH_RELAY_URL_ENV: &str = "MYELIN_IROH_RELAY_URL";
|
||||||
pub(crate) const SWACTOR_IROH_RELAY_URL_ENV: &str = "SWACTOR_IROH_RELAY_URL";
|
pub(crate) const SWACTOR_IROH_RELAY_URL_ENV: &str = "SWACTOR_IROH_RELAY_URL";
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
|
@ -72,12 +72,6 @@ impl StaticRelayProvider {
|
||||||
parse_relay_url(raw).map(Self::new)
|
parse_relay_url(raw).map(Self::new)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn from_env() -> Result<Option<Self>, String> {
|
|
||||||
selected_relay_url_from_env()
|
|
||||||
.map(|url| Self::from_url_str(&url).map(Some))
|
|
||||||
.unwrap_or(Ok(None))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn url(&self) -> String {
|
pub(crate) fn url(&self) -> String {
|
||||||
self.url.to_string()
|
self.url.to_string()
|
||||||
}
|
}
|
||||||
|
|
@ -108,7 +102,7 @@ impl RelayProvider for StaticRelayProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn relay_runtime_config_from_env(run_id: u64) -> Result<RelayRuntimeConfig, String> {
|
pub(crate) fn relay_runtime_config_from_env(run_id: u64) -> Result<RelayRuntimeConfig, String> {
|
||||||
let mode = env_optional(MVP_IROH_RELAY_MODE_ENV).map(|value| value.to_ascii_lowercase());
|
let mode = env_optional(MYELIN_IROH_RELAY_MODE_ENV).map(|value| value.to_ascii_lowercase());
|
||||||
let url = selected_relay_url_from_env();
|
let url = selected_relay_url_from_env();
|
||||||
relay_runtime_config_from_settings(run_id, mode.as_deref(), url.as_deref())
|
relay_runtime_config_from_settings(run_id, mode.as_deref(), url.as_deref())
|
||||||
}
|
}
|
||||||
|
|
@ -125,7 +119,7 @@ pub(crate) fn relay_runtime_config_from_settings(
|
||||||
}),
|
}),
|
||||||
None | Some("default") => relay_runtime_config_from_optional_static_provider(run_id, url),
|
None | Some("default") => relay_runtime_config_from_optional_static_provider(run_id, url),
|
||||||
Some(other) => Err(format!(
|
Some(other) => Err(format!(
|
||||||
"unsupported {MVP_IROH_RELAY_MODE_ENV}={other:?}; use disabled or default"
|
"unsupported {MYELIN_IROH_RELAY_MODE_ENV}={other:?}; use disabled or default"
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +132,7 @@ pub(crate) fn relay_mode_env_value(mode: &RelayMode) -> &'static str {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn selected_relay_url_from_env() -> Option<String> {
|
pub(crate) fn selected_relay_url_from_env() -> Option<String> {
|
||||||
env_optional(MVP_IROH_RELAY_URL_ENV).or_else(|| env_optional(SWACTOR_IROH_RELAY_URL_ENV))
|
env_optional(MYELIN_IROH_RELAY_URL_ENV).or_else(|| env_optional(SWACTOR_IROH_RELAY_URL_ENV))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn relay_runtime_config_from_optional_static_provider(
|
fn relay_runtime_config_from_optional_static_provider(
|
||||||
|
|
@ -4,12 +4,13 @@ use std::io::{BufRead, BufReader, Read};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Child, Command, Stdio};
|
use std::process::{Child, Command, Stdio};
|
||||||
use std::sync::{Arc, mpsc};
|
use std::sync::{Arc, mpsc};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::thread::{self, JoinHandle};
|
use std::thread::{self, JoinHandle};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use datastream::DatastreamProducer;
|
use datastream::DatastreamProducer;
|
||||||
use swactor::actor::{ActorAddress, ActorInterface};
|
use swactor::actor::{ActorAddress, ActorInterface};
|
||||||
use swactor::runtime::{Ctx, ExternalSender, Runtime, RuntimeConfig, RuntimeHandle};
|
use swactor::runtime::{Ctx, ExternalSender, Runtime, RuntimeConfig};
|
||||||
use swactor_vastai::{
|
use swactor_vastai::{
|
||||||
CreateInstanceRequest, LifecyclePolicy, Offer, ProvisionRequest, ProvisionedInstance,
|
CreateInstanceRequest, LifecyclePolicy, Offer, ProvisionRequest, ProvisionedInstance,
|
||||||
SelectionPolicy, classify_vastai_error, create_instance,
|
SelectionPolicy, classify_vastai_error, create_instance,
|
||||||
|
|
@ -35,7 +36,7 @@ pub(crate) struct VastAiProvisioningConfig {
|
||||||
impl Default for VastAiProvisioningConfig {
|
impl Default for VastAiProvisioningConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
label_prefix: "mvp".to_owned(),
|
label_prefix: "myelin".to_owned(),
|
||||||
disk_gb: 50,
|
disk_gb: 50,
|
||||||
ssh_user: "root".to_owned(),
|
ssh_user: "root".to_owned(),
|
||||||
selection: SelectionPolicy::default(),
|
selection: SelectionPolicy::default(),
|
||||||
|
|
@ -55,27 +56,46 @@ pub(crate) struct VastAiSshEndpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct VastAiProviderMonitor {
|
pub(crate) struct VastAiProviderMonitor {
|
||||||
runtime: Option<RuntimeHandle>,
|
runtime: Arc<Runtime>,
|
||||||
actor: ActorAddress,
|
actor: ActorAddress,
|
||||||
|
tick_thread: Option<JoinHandle<()>>,
|
||||||
|
stop_flag: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VastAiProviderMonitor {
|
impl VastAiProviderMonitor {
|
||||||
fn new(runtime: RuntimeHandle, actor: ActorAddress) -> Self {
|
fn new(runtime: Runtime, actor: ActorAddress) -> Self {
|
||||||
|
let runtime = Arc::new(runtime);
|
||||||
|
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
|
let rt = Arc::clone(&runtime);
|
||||||
|
let flag = Arc::clone(&stop_flag);
|
||||||
|
let tick_thread = thread::spawn(move || {
|
||||||
|
while !flag.load(Ordering::Relaxed) || rt.has_work() {
|
||||||
|
if rt.has_work() {
|
||||||
|
rt.tick();
|
||||||
|
} else {
|
||||||
|
thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
runtime: Some(runtime),
|
runtime,
|
||||||
actor,
|
actor,
|
||||||
|
tick_thread: Some(tick_thread),
|
||||||
|
stop_flag,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop(&mut self) {
|
fn stop(&mut self) {
|
||||||
let Some(runtime) = self.runtime.take() else {
|
let Some(tick_thread) = self.tick_thread.take() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let _ = runtime
|
let _ = self
|
||||||
.runtime
|
.runtime
|
||||||
.send_to(self.actor, VastAiProviderMonitorMsg::Stop);
|
.send_to(self.actor, VastAiProviderMonitorMsg::Stop);
|
||||||
runtime.shutdown();
|
self.stop_flag.store(true, Ordering::Relaxed);
|
||||||
runtime.join();
|
let _ = tick_thread.join();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -552,7 +572,6 @@ impl VastAiLeaseClient for ToolsVastAiLeaseClient {
|
||||||
sender,
|
sender,
|
||||||
))
|
))
|
||||||
.ok()?;
|
.ok()?;
|
||||||
let runtime = runtime.run().ok()?;
|
|
||||||
Some(VastAiProviderMonitor::new(runtime, actor))
|
Some(VastAiProviderMonitor::new(runtime, actor))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! MVP-local provisioning provider plugins.
|
//! Myelin-local provisioning provider plugins.
|
||||||
//!
|
//!
|
||||||
//! Provider-neutral lifecycle contracts live in the reusable `provisioning`
|
//! Provider-neutral lifecycle contracts live in the reusable `provisioning`
|
||||||
//! crate. This module keeps MVP-local process/Docker plugin implementations
|
//! crate. This module keeps Myelin-local process/Docker plugin implementations
|
||||||
//! that know about bootstrap datastream plumbing and local runtime execution.
|
//! that know about bootstrap datastream plumbing and local runtime execution.
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
@ -118,7 +118,7 @@ fn prepare_docker_file_volume(
|
||||||
&["volume", "create", &volume],
|
&["volume", "create", &volume],
|
||||||
"create cached model docker volume",
|
"create cached model docker volume",
|
||||||
)?;
|
)?;
|
||||||
let loader_name = format!("mvp-cache-load-{}-{volume}", std::process::id());
|
let loader_name = format!("myelin-cache-load-{}-{volume}", std::process::id());
|
||||||
let _ = Command::new("docker")
|
let _ = Command::new("docker")
|
||||||
.args(["rm", "-f", &loader_name])
|
.args(["rm", "-f", &loader_name])
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
|
|
@ -173,7 +173,7 @@ fn docker_file_cache_volume_name(path: &Path) -> Result<String, String> {
|
||||||
.and_then(|name| name.to_str())
|
.and_then(|name| name.to_str())
|
||||||
.map(safe_docker_volume_component)
|
.map(safe_docker_volume_component)
|
||||||
.unwrap_or_else(|| "model".to_owned());
|
.unwrap_or_else(|| "model".to_owned());
|
||||||
Ok(format!("mvp-cache-{file}-{}-{modified}", metadata.len()))
|
Ok(format!("myelin-cache-{file}-{}-{modified}", metadata.len()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn safe_docker_volume_component(value: &str) -> String {
|
fn safe_docker_volume_component(value: &str) -> String {
|
||||||
|
|
@ -455,14 +455,14 @@ impl ProvisionPlugin for LocalDockerPlugin {
|
||||||
let docker_gpus = spec
|
let docker_gpus = spec
|
||||||
.env
|
.env
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(key, _)| key == "MVP_DOCKER_GPUS")
|
.find(|(key, _)| key == "MYELIN_DOCKER_GPUS")
|
||||||
.map(|(_, value)| value.clone())
|
.map(|(_, value)| value.clone())
|
||||||
.or_else(|| std::env::var("MVP_DOCKER_GPUS").ok())
|
.or_else(|| std::env::var("MYELIN_DOCKER_GPUS").ok())
|
||||||
.filter(|value| !value.trim().is_empty());
|
.filter(|value| !value.trim().is_empty());
|
||||||
sink.observe(PluginObservation::DatastreamFrame {
|
sink.observe(PluginObservation::DatastreamFrame {
|
||||||
run_id: spec.run_id,
|
run_id: spec.run_id,
|
||||||
node_id: spec.node_id,
|
node_id: spec.node_id,
|
||||||
channel: "mvp.node.bootstrap".to_owned(),
|
channel: "myelin.provisioning.events".to_owned(),
|
||||||
payload: serde_json::json!({
|
payload: serde_json::json!({
|
||||||
"type":"DockerGpuConfigResolved",
|
"type":"DockerGpuConfigResolved",
|
||||||
"provider":"Docker",
|
"provider":"Docker",
|
||||||
|
|
@ -16,16 +16,6 @@ pub(crate) struct RunPlan {
|
||||||
pub stages: Vec<StageRef>,
|
pub stages: Vec<StageRef>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RunPlan {
|
|
||||||
pub(crate) fn test_linear(run_id: RunId, stages: Vec<StageRef>) -> Self {
|
|
||||||
Self { run_id, stages }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn stage_nodes(&self) -> Vec<NodeId> {
|
|
||||||
self.stages.iter().map(|stage| stage.node_id).collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub(crate) struct RunConfig {
|
pub(crate) struct RunConfig {
|
||||||
pub run_id: RunId,
|
pub run_id: RunId,
|
||||||
|
|
@ -125,10 +115,6 @@ pub(crate) enum RunFaultReason {
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub(crate) enum LifecycleEvent {
|
pub(crate) enum LifecycleEvent {
|
||||||
RunRejected {
|
|
||||||
run_id: RunId,
|
|
||||||
reason: RunFaultReason,
|
|
||||||
},
|
|
||||||
RunFaulted {
|
RunFaulted {
|
||||||
run_id: RunId,
|
run_id: RunId,
|
||||||
reason: RunFaultReason,
|
reason: RunFaultReason,
|
||||||
|
|
@ -175,8 +161,6 @@ pub(crate) enum RunCommand {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type OrchestratorHarness = OrchestratorRun;
|
|
||||||
|
|
||||||
pub(crate) struct OrchestratorRun {
|
pub(crate) struct OrchestratorRun {
|
||||||
config: RunConfig,
|
config: RunConfig,
|
||||||
plan: Option<RunPlan>,
|
plan: Option<RunPlan>,
|
||||||
|
|
@ -165,7 +165,6 @@ pub(crate) enum EdgeKind {
|
||||||
pub(crate) enum ObjectKind {
|
pub(crate) enum ObjectKind {
|
||||||
Token,
|
Token,
|
||||||
Activation,
|
Activation,
|
||||||
Weight,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
3
apps/myelin/src/prompt/mod.rs
Normal file
3
apps/myelin/src/prompt/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
//! Myelin prompt protocol public surface.
|
||||||
|
|
||||||
|
pub(crate) mod rpc;
|
||||||
|
|
@ -56,7 +56,7 @@ impl PromptEvent {
|
||||||
|
|
||||||
impl NetworkMessage for PromptEvent {
|
impl NetworkMessage for PromptEvent {
|
||||||
fn type_tag() -> &'static str {
|
fn type_tag() -> &'static str {
|
||||||
"mvp_system::PromptEvent"
|
"myelin::PromptEvent"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,7 +69,7 @@ pub(crate) enum TokenizerEvent {
|
||||||
|
|
||||||
impl NetworkMessage for TokenizerEvent {
|
impl NetworkMessage for TokenizerEvent {
|
||||||
fn type_tag() -> &'static str {
|
fn type_tag() -> &'static str {
|
||||||
"mvp_system::TokenizerEvent"
|
"myelin::TokenizerEvent"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,12 +19,6 @@ pub(crate) struct DeviceHandle {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeviceHandle {
|
|
||||||
pub(crate) fn new_current(id: u64) -> Self {
|
|
||||||
Self { generation: 1, id }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub(crate) struct LayerRange {
|
pub(crate) struct LayerRange {
|
||||||
pub start: u32,
|
pub start: u32,
|
||||||
|
|
@ -50,14 +44,6 @@ impl WeightSource {
|
||||||
tokenizer,
|
tokenizer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn embedded_gguf(model_id: impl Into<String>, path: impl Into<String>) -> Self {
|
|
||||||
Self::new(
|
|
||||||
model_id,
|
|
||||||
GgufSource::LocalPath(path.into()),
|
|
||||||
TokenizerSource::EmbeddedGguf,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
|
@ -228,9 +214,6 @@ pub(crate) enum StageCommand {
|
||||||
range: LayerRange,
|
range: LayerRange,
|
||||||
shard_plan: Option<StageShardPlan>,
|
shard_plan: Option<StageShardPlan>,
|
||||||
},
|
},
|
||||||
RewireEdge {
|
|
||||||
edge_id: EdgeId,
|
|
||||||
},
|
|
||||||
ExecuteStep(ExecuteStep),
|
ExecuteStep(ExecuteStep),
|
||||||
ReleaseInputHandle {
|
ReleaseInputHandle {
|
||||||
object_id: ObjectId,
|
object_id: ObjectId,
|
||||||
|
|
@ -244,8 +227,6 @@ pub(crate) enum StageCommand {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type StageControllerHarness = StageController;
|
|
||||||
|
|
||||||
pub(crate) struct StageController {
|
pub(crate) struct StageController {
|
||||||
local_node_id: NodeId,
|
local_node_id: NodeId,
|
||||||
provision: Option<ProvisionStage>,
|
provision: Option<ProvisionStage>,
|
||||||
|
|
@ -35,7 +35,7 @@ impl GgufPlanningMetadata {
|
||||||
.map(u64::from)
|
.map(u64::from)
|
||||||
.unwrap_or(DEFAULT_EFFECTIVE_CONTEXT);
|
.unwrap_or(DEFAULT_EFFECTIVE_CONTEXT);
|
||||||
if requested_context == 0 {
|
if requested_context == 0 {
|
||||||
return Err("MVP_MAX_CONTEXT/--max-context must be greater than 0".to_owned());
|
return Err("MYELIN_MAX_CONTEXT/--max-context must be greater than 0".to_owned());
|
||||||
}
|
}
|
||||||
let effective_context = requested_context.min(self.context_length);
|
let effective_context = requested_context.min(self.context_length);
|
||||||
if effective_context == 0 {
|
if effective_context == 0 {
|
||||||
|
|
@ -728,7 +728,7 @@ fn fetch_http_range(url: &str, start: u64, len: u64) -> Result<Vec<u8>, String>
|
||||||
let range = format!("bytes={start}-{end}");
|
let range = format!("bytes={start}-{end}");
|
||||||
let mut request = ureq::get(url)
|
let mut request = ureq::get(url)
|
||||||
.set("Range", &range)
|
.set("Range", &range)
|
||||||
.set("User-Agent", "swactor-mvp-node/0.1");
|
.set("User-Agent", "myelin-node/0.1");
|
||||||
if let Ok(token) = std::env::var("HF_TOKEN")
|
if let Ok(token) = std::env::var("HF_TOKEN")
|
||||||
&& !token.trim().is_empty()
|
&& !token.trim().is_empty()
|
||||||
{
|
{
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
//! MVP stage control, shard planning, and weight lifecycle public surface.
|
//! Myelin stage control, shard planning, and weight lifecycle public surface.
|
||||||
|
|
||||||
pub(crate) mod control;
|
pub(crate) mod control;
|
||||||
pub(crate) mod gguf_metadata;
|
pub(crate) mod gguf_metadata;
|
||||||
50
apps/myelin/src/tests/harness.rs
Normal file
50
apps/myelin/src/tests/harness.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
//! Test-only constructors and harness type aliases for the `tests` tree.
|
||||||
|
//!
|
||||||
|
//! These helpers previously lived in the production modules
|
||||||
|
//! (`orchestration/run_fsm`, `staging::control`) but serve only test code.
|
||||||
|
//! Defining them here keeps them compiled under `#[cfg(test)]` only, so
|
||||||
|
//! production builds stay free of the dead-code warnings they would otherwise
|
||||||
|
//! trigger. Inherent methods resolve through their type, so existing call sites
|
||||||
|
//! such as `fsm::RunPlan::test_linear(..)` and
|
||||||
|
//! `stage::WeightSource::embedded_gguf(..)` work unchanged.
|
||||||
|
|
||||||
|
use crate::run_fsm::{NodeId as FsmNodeId, OrchestratorRun, RunId as FsmRunId, RunPlan, StageRef};
|
||||||
|
use crate::run_plan::{GgufSource, TokenizerSource};
|
||||||
|
use crate::staging::{DeviceHandle, StageController, WeightSource};
|
||||||
|
|
||||||
|
impl RunPlan {
|
||||||
|
/// Build a linear pipeline plan (stage 0 -> stage 1 -> ... -> last stage).
|
||||||
|
/// Test-only convenience over the public `RunPlan` fields.
|
||||||
|
pub(crate) fn test_linear(run_id: FsmRunId, stages: Vec<StageRef>) -> Self {
|
||||||
|
Self { run_id, stages }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Node ids in declared stage order.
|
||||||
|
pub(crate) fn stage_nodes(&self) -> Vec<FsmNodeId> {
|
||||||
|
self.stages.iter().map(|stage| stage.node_id).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceHandle {
|
||||||
|
/// A handle on the worker's current (first) generation.
|
||||||
|
pub(crate) fn new_current(id: u64) -> Self {
|
||||||
|
Self { generation: 1, id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WeightSource {
|
||||||
|
/// Weights carried by an embedded GGUF file with an embedded tokenizer.
|
||||||
|
pub(crate) fn embedded_gguf(model_id: impl Into<String>, path: impl Into<String>) -> Self {
|
||||||
|
Self::new(
|
||||||
|
model_id,
|
||||||
|
GgufSource::LocalPath(path.into()),
|
||||||
|
TokenizerSource::EmbeddedGguf,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test alias for the orchestrator run core, retained for readable test prose.
|
||||||
|
pub(crate) type OrchestratorHarness = OrchestratorRun;
|
||||||
|
|
||||||
|
/// Test alias for the stage controller core, retained for readable test prose.
|
||||||
|
pub(crate) type StageControllerHarness = StageController;
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
//! Local end-to-end behavior guarantee for the MVP system lifecycle.
|
//! Local end-to-end behavior guarantee for the Myelin system lifecycle.
|
||||||
//!
|
//!
|
||||||
//! This test composes the crate's MVP contract harnesses through one in-process
|
//! This test composes the crate's Myelin contract harnesses through one in-process
|
||||||
//! mock environment. It deliberately avoids Docker, real SWIM, real iroh, GGUF,
|
//! mock environment. It deliberately avoids Docker, real SWIM, real iroh, GGUF,
|
||||||
//! and CUDA while still driving the run through planning, provisioning,
|
//! and CUDA while still driving the run through planning, provisioning,
|
||||||
//! readiness, prompt injection, stage execution, completion, and teardown.
|
//! readiness, prompt injection, stage execution, completion, and teardown.
|
||||||
|
|
||||||
use mvp_system::observability::lifecycle as obs;
|
use myelin::observability::lifecycle as obs;
|
||||||
use mvp_system::orchestration::engine_builder as engine;
|
use myelin::orchestration::engine_builder as engine;
|
||||||
|
|
||||||
use super::local_mock::{
|
use super::local_mock::{
|
||||||
LocalMockCluster, LocalMockConfig, LocalMockOutcome, assert_happy_path_lifecycle,
|
LocalMockCluster, LocalMockConfig, LocalMockOutcome, assert_happy_path_lifecycle,
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use mvp_system::observability::lifecycle as obs;
|
use myelin::observability::lifecycle as obs;
|
||||||
|
|
||||||
use super::mock_transport::MockObjectKind;
|
use super::mock_transport::MockObjectKind;
|
||||||
|
|
||||||
|
|
@ -2,10 +2,11 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
use crate::run_fsm as fsm;
|
use crate::run_fsm as fsm;
|
||||||
use crate::run_plan as plan;
|
use crate::run_plan as plan;
|
||||||
|
use crate::tests::harness::OrchestratorHarness;
|
||||||
use data_plane::edge_actor;
|
use data_plane::edge_actor;
|
||||||
use mvp_system::observability::lifecycle as obs;
|
use myelin::observability::lifecycle as obs;
|
||||||
use mvp_system::orchestration::engine_builder as engine;
|
use myelin::orchestration::engine_builder as engine;
|
||||||
use mvp_system::staging as stage;
|
use myelin::staging as stage;
|
||||||
|
|
||||||
use super::mock_node::MockNode;
|
use super::mock_node::MockNode;
|
||||||
use super::mock_transport::{Delivery, MockObject, MockObjectKind, MockTransport};
|
use super::mock_transport::{Delivery, MockObject, MockObjectKind, MockTransport};
|
||||||
|
|
@ -34,7 +35,7 @@ pub struct LocalMockCluster {
|
||||||
engine_events: Vec<engine::EngineEvent>,
|
engine_events: Vec<engine::EngineEvent>,
|
||||||
plan: plan::RunPlan,
|
plan: plan::RunPlan,
|
||||||
nodes: BTreeMap<u32, MockNode>,
|
nodes: BTreeMap<u32, MockNode>,
|
||||||
orchestrator: Option<fsm::OrchestratorHarness>,
|
orchestrator: Option<OrchestratorHarness>,
|
||||||
orchestrator_command_cursor: usize,
|
orchestrator_command_cursor: usize,
|
||||||
orchestrator_event_cursor: usize,
|
orchestrator_event_cursor: usize,
|
||||||
trace: Vec<obs::Event>,
|
trace: Vec<obs::Event>,
|
||||||
|
|
@ -303,7 +304,7 @@ impl LocalMockCluster {
|
||||||
self.orchestrator_command_cursor = 0;
|
self.orchestrator_command_cursor = 0;
|
||||||
self.orchestrator_event_cursor = 0;
|
self.orchestrator_event_cursor = 0;
|
||||||
self.scenario = scenario;
|
self.scenario = scenario;
|
||||||
self.orchestrator = Some(fsm::OrchestratorHarness::new(fsm::RunConfig {
|
self.orchestrator = Some(OrchestratorHarness::new(fsm::RunConfig {
|
||||||
run_id: fsm::RunId(self.run_id.0),
|
run_id: fsm::RunId(self.run_id.0),
|
||||||
max_tokens: u64::from(self.max_tokens),
|
max_tokens: u64::from(self.max_tokens),
|
||||||
prompt: tokenize(prompt),
|
prompt: tokenize(prompt),
|
||||||
|
|
@ -742,7 +743,6 @@ impl LocalMockCluster {
|
||||||
self.push_run(obs::EventKind::RunCompleted, obs::Component::Orchestrator);
|
self.push_run(obs::EventKind::RunCompleted, obs::Component::Orchestrator);
|
||||||
}
|
}
|
||||||
fsm::LifecycleEvent::RunFaulted { .. }
|
fsm::LifecycleEvent::RunFaulted { .. }
|
||||||
| fsm::LifecycleEvent::RunRejected { .. }
|
|
||||||
| fsm::LifecycleEvent::RunOperatorStopped { .. } => {
|
| fsm::LifecycleEvent::RunOperatorStopped { .. } => {
|
||||||
self.push_run(obs::EventKind::RunFaulted, obs::Component::Orchestrator);
|
self.push_run(obs::EventKind::RunFaulted, obs::Component::Orchestrator);
|
||||||
}
|
}
|
||||||
|
|
@ -852,7 +852,7 @@ impl LocalMockCluster {
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn orchestrator_mut(&mut self) -> &mut fsm::OrchestratorHarness {
|
fn orchestrator_mut(&mut self) -> &mut OrchestratorHarness {
|
||||||
self.orchestrator
|
self.orchestrator
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.expect("run_prompt must initialize orchestrator")
|
.expect("run_prompt must initialize orchestrator")
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::run_plan as plan;
|
use crate::run_plan as plan;
|
||||||
use data_plane::edge_actor;
|
use data_plane::edge_actor;
|
||||||
use mvp_system::staging as stage;
|
use myelin::staging as stage;
|
||||||
|
use crate::tests::harness::StageControllerHarness;
|
||||||
|
|
||||||
use super::mock_transport::MockObject;
|
use super::mock_transport::MockObject;
|
||||||
use super::mock_worker::MockWorker;
|
use super::mock_worker::MockWorker;
|
||||||
|
|
@ -16,7 +17,7 @@ pub struct MockNode {
|
||||||
inbound_edge: Option<plan::EdgeId>,
|
inbound_edge: Option<plan::EdgeId>,
|
||||||
outbound_edge: Option<plan::EdgeId>,
|
outbound_edge: Option<plan::EdgeId>,
|
||||||
outbound_object_allocator: Option<edge_actor::ObjectIdAllocator>,
|
outbound_object_allocator: Option<edge_actor::ObjectIdAllocator>,
|
||||||
controller: stage::StageControllerHarness,
|
controller: StageControllerHarness,
|
||||||
worker: MockWorker,
|
worker: MockWorker,
|
||||||
event_cursor: usize,
|
event_cursor: usize,
|
||||||
}
|
}
|
||||||
|
|
@ -34,7 +35,7 @@ impl MockNode {
|
||||||
inbound_edge: None,
|
inbound_edge: None,
|
||||||
outbound_edge: None,
|
outbound_edge: None,
|
||||||
outbound_object_allocator: None,
|
outbound_object_allocator: None,
|
||||||
controller: stage::StageControllerHarness::new(stage::NodeId(node_id.0)),
|
controller: StageControllerHarness::new(stage::NodeId(node_id.0)),
|
||||||
worker: MockWorker::new(stage_index, eos_after_sequence),
|
worker: MockWorker::new(stage_index, eos_after_sequence),
|
||||||
event_cursor: 0,
|
event_cursor: 0,
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::run_plan as plan;
|
use crate::run_plan as plan;
|
||||||
use mvp_system::staging as stage;
|
use myelin::staging as stage;
|
||||||
|
|
||||||
use super::mock_transport::{MockObject, MockObjectKind};
|
use super::mock_transport::{MockObject, MockObjectKind};
|
||||||
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod harness;
|
||||||
mod local_e2e_guarantees;
|
mod local_e2e_guarantees;
|
||||||
mod local_mock;
|
mod local_mock;
|
||||||
mod node_guarantees;
|
mod node_guarantees;
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
use crate::node_actor::{NodeAgentActor, NodeAgentMsg, NodeAgentReport};
|
use crate::node_actor::{NodeAgentActor, NodeAgentMsg, NodeAgentReport};
|
||||||
use crate::orchestration::actor::OrchestratorMsg;
|
use crate::orchestration::actor::OrchestratorMsg;
|
||||||
use iroh::{EndpointAddr, SecretKey};
|
use iroh::{EndpointAddr, SecretKey};
|
||||||
use mvp_system::staging as stage;
|
use myelin::staging as stage;
|
||||||
use swactor::actor::ActorAddress;
|
use swactor::actor::ActorAddress;
|
||||||
use swactor::config::RuntimeConfig;
|
use swactor::config::RuntimeConfig;
|
||||||
use swactor::runtime::Runtime;
|
use swactor::runtime::Runtime;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! Behavior guarantees for the `observability` module.
|
//! Behavior guarantees for the `observability` module.
|
||||||
|
|
||||||
//! Black-box contract tests for the MVP observability surface.
|
//! Black-box contract tests for the Myelin observability surface.
|
||||||
//!
|
//!
|
||||||
//! These tests intentionally know only the public event stream surface:
|
//! These tests intentionally know only the public event stream surface:
|
||||||
//!
|
//!
|
||||||
|
|
@ -8,9 +8,9 @@
|
||||||
//! - event identities, reason enums, and ordering observed by subscribers
|
//! - event identities, reason enums, and ordering observed by subscribers
|
||||||
//!
|
//!
|
||||||
//! They assert the guarantees in
|
//! They assert the guarantees in
|
||||||
//! `specs/mvp_system/observability_surface_contract.md`.
|
//! `specs/myelin_system/observability_surface_contract.md`.
|
||||||
|
|
||||||
use mvp_system::observability::lifecycle as obs;
|
use myelin::observability::lifecycle as obs;
|
||||||
|
|
||||||
// The trace fixture contains one successful run from boot through teardown.
|
// The trace fixture contains one successful run from boot through teardown.
|
||||||
// Tests use structured events only; logs, transport, storage, and batching stay
|
// Tests use structured events only; logs, transport, storage, and batching stay
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Behavior guarantees for the `orchestration` module.
|
//! Behavior guarantees for the `orchestration` module.
|
||||||
|
|
||||||
mod run_plan {
|
mod run_plan {
|
||||||
//! Black-box contract tests for MVP RunPlan formation.
|
//! Black-box contract tests for Myelin RunPlan formation.
|
||||||
//!
|
//!
|
||||||
//! These tests intentionally know only the public planning surface:
|
//! These tests intentionally know only the public planning surface:
|
||||||
//!
|
//!
|
||||||
|
|
@ -205,7 +205,7 @@ mod run_plan {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// This proves every edge has one public producer and one public consumer, and
|
// This proves every edge has one public producer and one public consumer, and
|
||||||
// that the returned edge graph is exactly the MVP linear pipeline.
|
// that the returned edge graph is exactly the Myelin linear pipeline.
|
||||||
#[test]
|
#[test]
|
||||||
fn edge_graph_is_exactly_the_linear_pipeline() {
|
fn edge_graph_is_exactly_the_linear_pipeline() {
|
||||||
// Use four stages so the activation chain has multiple interior edges.
|
// Use four stages so the activation chain has multiple interior edges.
|
||||||
|
|
@ -474,7 +474,7 @@ mod run_plan {
|
||||||
input
|
input
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zero stages cannot form the MVP pipeline. This fixture isolates the invalid
|
// Zero stages cannot form the Myelin pipeline. This fixture isolates the invalid
|
||||||
// stage-count path without adding any other contradictory facts.
|
// stage-count path without adding any other contradictory facts.
|
||||||
fn invalid_zero_stage_count() -> PlannerInput {
|
fn invalid_zero_stage_count() -> PlannerInput {
|
||||||
valid_input(0, 36)
|
valid_input(0, 36)
|
||||||
|
|
@ -491,7 +491,7 @@ mod run_plan {
|
||||||
}
|
}
|
||||||
|
|
||||||
mod run_fsm {
|
mod run_fsm {
|
||||||
//! Black-box contract tests for the MVP orchestrator run FSM.
|
//! Black-box contract tests for the Myelin orchestrator run FSM.
|
||||||
//!
|
//!
|
||||||
//! These tests intentionally know only the public orchestrator surface:
|
//! These tests intentionally know only the public orchestrator surface:
|
||||||
//!
|
//!
|
||||||
|
|
@ -502,6 +502,7 @@ mod run_fsm {
|
||||||
//! `specs/BEHAVIOR_GUARANTEES.md`.
|
//! `specs/BEHAVIOR_GUARANTEES.md`.
|
||||||
|
|
||||||
use crate::run_fsm as fsm;
|
use crate::run_fsm as fsm;
|
||||||
|
use crate::tests::harness::OrchestratorHarness;
|
||||||
|
|
||||||
// A three-stage plan proves multi-stage provisioning and readiness without
|
// A three-stage plan proves multi-stage provisioning and readiness without
|
||||||
// making tests depend on any placement heuristic. The plan is already valid;
|
// making tests depend on any placement heuristic. The plan is already valid;
|
||||||
|
|
@ -529,8 +530,8 @@ mod run_fsm {
|
||||||
// The harness is the black-box public boundary for the run FSM. It accepts
|
// The harness is the black-box public boundary for the run FSM. It accepts
|
||||||
// observable events and records emitted commands/events; tests never inspect an
|
// observable events and records emitted commands/events; tests never inspect an
|
||||||
// internal FSM enum or private readiness counter.
|
// internal FSM enum or private readiness counter.
|
||||||
fn new_run() -> fsm::OrchestratorHarness {
|
fn new_run() -> OrchestratorHarness {
|
||||||
fsm::OrchestratorHarness::new(fsm::RunConfig {
|
OrchestratorHarness::new(fsm::RunConfig {
|
||||||
run_id: fsm::RunId(7),
|
run_id: fsm::RunId(7),
|
||||||
max_tokens: 4,
|
max_tokens: 4,
|
||||||
prompt: vec![101, 102, 103],
|
prompt: vec![101, 102, 103],
|
||||||
|
|
@ -680,7 +681,6 @@ mod run_fsm {
|
||||||
});
|
});
|
||||||
assert!(invalid.events().iter().any(|event| {
|
assert!(invalid.events().iter().any(|event| {
|
||||||
matches!(event, fsm::LifecycleEvent::RunFaulted { .. })
|
matches!(event, fsm::LifecycleEvent::RunFaulted { .. })
|
||||||
|| matches!(event, fsm::LifecycleEvent::RunRejected { .. })
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! Behavior guarantees for the `prompt` module.
|
//! Behavior guarantees for the `prompt` module.
|
||||||
|
|
||||||
use mvp_system::prompt::rpc::{PromptEvent, SubmitPrompt};
|
use myelin::prompt::rpc::{PromptEvent, SubmitPrompt};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn zero_request_limits_take_loop_defaults() {
|
fn zero_request_limits_take_loop_defaults() {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Behavior guarantees for the `staging` module.
|
//! Behavior guarantees for the `staging` module.
|
||||||
|
|
||||||
mod stage_controller {
|
mod stage_controller {
|
||||||
//! Black-box contract tests for MVP StageController behavior.
|
//! Black-box contract tests for Myelin StageController behavior.
|
||||||
//!
|
//!
|
||||||
//! These tests intentionally know only the public stage-controller surface:
|
//! These tests intentionally know only the public stage-controller surface:
|
||||||
//!
|
//!
|
||||||
|
|
@ -11,7 +11,8 @@ mod stage_controller {
|
||||||
//! They assert the guarantees in
|
//! They assert the guarantees in
|
||||||
//! `specs/BEHAVIOR_GUARANTEES.md`.
|
//! `specs/BEHAVIOR_GUARANTEES.md`.
|
||||||
|
|
||||||
use mvp_system::staging as stage;
|
use myelin::staging as stage;
|
||||||
|
use crate::tests::harness::StageControllerHarness;
|
||||||
|
|
||||||
// This provision fixture represents a single middle stage. It has one inbound
|
// This provision fixture represents a single middle stage. It has one inbound
|
||||||
// and one outbound edge so tests can prove the controller uses assigned edges
|
// and one outbound edge so tests can prove the controller uses assigned edges
|
||||||
|
|
@ -37,8 +38,8 @@ mod stage_controller {
|
||||||
// The harness exposes only public messages. Tests intentionally do not inspect
|
// The harness exposes only public messages. Tests intentionally do not inspect
|
||||||
// private controller states such as "Preparing" or "Executing"; they infer
|
// private controller states such as "Preparing" or "Executing"; they infer
|
||||||
// controller behavior from emitted commands and lifecycle events.
|
// controller behavior from emitted commands and lifecycle events.
|
||||||
fn new_controller() -> stage::StageControllerHarness {
|
fn new_controller() -> StageControllerHarness {
|
||||||
stage::StageControllerHarness::new(stage::NodeId(11))
|
StageControllerHarness::new(stage::NodeId(11))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preparation readiness has four independent prerequisites. Listing them as
|
// Preparation readiness has four independent prerequisites. Listing them as
|
||||||
|
|
@ -60,7 +61,7 @@ mod stage_controller {
|
||||||
// This helper provisions and readies a stage through public events. Tests that
|
// This helper provisions and readies a stage through public events. Tests that
|
||||||
// focus on execution use it to avoid duplicating setup while still going through
|
// focus on execution use it to avoid duplicating setup while still going through
|
||||||
// the same observable path as production.
|
// the same observable path as production.
|
||||||
fn ready_stage() -> stage::StageControllerHarness {
|
fn ready_stage() -> StageControllerHarness {
|
||||||
let mut harness = new_controller();
|
let mut harness = new_controller();
|
||||||
harness.observe(stage::StageEvent::ProvisionStage {
|
harness.observe(stage::StageEvent::ProvisionStage {
|
||||||
from: stage::NodeId(99),
|
from: stage::NodeId(99),
|
||||||
|
|
@ -103,15 +104,6 @@ mod stage_controller {
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// The controller must not emit any command that replaces the provisioned
|
|
||||||
// edge ids with a locally chosen edge.
|
|
||||||
assert!(
|
|
||||||
!harness
|
|
||||||
.commands()
|
|
||||||
.iter()
|
|
||||||
.any(|command| { matches!(command, stage::StageCommand::RewireEdge { .. }) })
|
|
||||||
);
|
|
||||||
|
|
||||||
// An unauthorized provision attempt must fault before setup can begin.
|
// An unauthorized provision attempt must fault before setup can begin.
|
||||||
let mut unauthorized = new_controller();
|
let mut unauthorized = new_controller();
|
||||||
unauthorized.observe(stage::StageEvent::ProvisionStage {
|
unauthorized.observe(stage::StageEvent::ProvisionStage {
|
||||||
|
|
@ -209,7 +201,7 @@ mod stage_controller {
|
||||||
assert_eq!(execute_steps[0].outputs[0].sequence, 0);
|
assert_eq!(execute_steps[0].outputs[0].sequence, 0);
|
||||||
|
|
||||||
// A second object while the first step is active must not create another
|
// A second object while the first step is active must not create another
|
||||||
// active ExecuteStep in the MVP.
|
// active ExecuteStep in the Myelin.
|
||||||
harness.observe(stage::StageEvent::ObjectLoaded {
|
harness.observe(stage::StageEvent::ObjectLoaded {
|
||||||
edge_id: stage::EdgeId(7001),
|
edge_id: stage::EdgeId(7001),
|
||||||
object_id: stage::ObjectId(9001),
|
object_id: stage::ObjectId(9001),
|
||||||
|
|
@ -519,7 +511,7 @@ mod stage_controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
mod weight_lifecycle {
|
mod weight_lifecycle {
|
||||||
//! Black-box contract tests for MVP stage-local weight lifecycle.
|
//! Black-box contract tests for Myelin stage-local weight lifecycle.
|
||||||
//!
|
//!
|
||||||
//! These tests intentionally know only the public weight-work surface:
|
//! These tests intentionally know only the public weight-work surface:
|
||||||
//!
|
//!
|
||||||
|
|
@ -530,7 +522,7 @@ mod weight_lifecycle {
|
||||||
//! They assert the guarantees in
|
//! They assert the guarantees in
|
||||||
//! `specs/BEHAVIOR_GUARANTEES.md`.
|
//! `specs/BEHAVIOR_GUARANTEES.md`.
|
||||||
|
|
||||||
use mvp_system::staging::weight_lifecycle as weights;
|
use myelin::staging::weight_lifecycle as weights;
|
||||||
|
|
||||||
// A valid assignment gives the stage exactly one layer range and one source.
|
// A valid assignment gives the stage exactly one layer range and one source.
|
||||||
// Tests vary only source or failure outcome so the assignment contract remains
|
// Tests vary only source or failure outcome so the assignment contract remains
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Tinygrad-backed device bridge probe for Rust MVP bridge tests.
|
"""Tinygrad-backed device bridge probe for Rust Myelin bridge tests.
|
||||||
|
|
||||||
Line-delimited JSON control only. Payload bytes live in the arena file whose
|
Line-delimited JSON control only. Payload bytes live in the arena file whose
|
||||||
path Rust passes during initialize.
|
path Rust passes during initialize.
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
name = "python"
|
name = "python"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "swactor"
|
name = "swactor"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use ::swactor::actor::{
|
||||||
Actor, ActorAddress, ActorInterface, AnyActor, Ctx, Environment, SpawnRequest,
|
Actor, ActorAddress, ActorInterface, AnyActor, Ctx, Environment, SpawnRequest,
|
||||||
};
|
};
|
||||||
use ::swactor::config::RuntimeConfig;
|
use ::swactor::config::RuntimeConfig;
|
||||||
use ::swactor::runtime::{Inbox, Runtime, RuntimeHandle};
|
use ::swactor::runtime::{Inbox, Runtime};
|
||||||
|
|
||||||
// ─── PyMsg newtype ───────────────────────────────────────────────────────────
|
// ─── PyMsg newtype ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -220,8 +220,6 @@ impl PyInbox {
|
||||||
#[pyclass(name = "RuntimeConfig")]
|
#[pyclass(name = "RuntimeConfig")]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PyRuntimeConfig {
|
pub struct PyRuntimeConfig {
|
||||||
#[pyo3(get, set)]
|
|
||||||
num_threads: usize,
|
|
||||||
#[pyo3(get, set)]
|
#[pyo3(get, set)]
|
||||||
max_actors: usize,
|
max_actors: usize,
|
||||||
#[pyo3(get, set)]
|
#[pyo3(get, set)]
|
||||||
|
|
@ -233,13 +231,11 @@ impl PyRuntimeConfig {
|
||||||
#[new]
|
#[new]
|
||||||
#[pyo3(signature = (
|
#[pyo3(signature = (
|
||||||
*,
|
*,
|
||||||
num_threads = 1,
|
|
||||||
max_actors = 1_000,
|
max_actors = 1_000,
|
||||||
channel_buffer_size = 1_000,
|
channel_buffer_size = 1_000,
|
||||||
))]
|
))]
|
||||||
fn new(num_threads: usize, max_actors: usize, channel_buffer_size: usize) -> Self {
|
fn new(max_actors: usize, channel_buffer_size: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
num_threads,
|
|
||||||
max_actors,
|
max_actors,
|
||||||
channel_buffer_size,
|
channel_buffer_size,
|
||||||
}
|
}
|
||||||
|
|
@ -249,7 +245,6 @@ impl PyRuntimeConfig {
|
||||||
impl From<PyRuntimeConfig> for RuntimeConfig {
|
impl From<PyRuntimeConfig> for RuntimeConfig {
|
||||||
fn from(py: PyRuntimeConfig) -> Self {
|
fn from(py: PyRuntimeConfig) -> Self {
|
||||||
RuntimeConfig {
|
RuntimeConfig {
|
||||||
num_threads: py.num_threads,
|
|
||||||
max_actors: py.max_actors,
|
max_actors: py.max_actors,
|
||||||
channel_buffer_size: py.channel_buffer_size,
|
channel_buffer_size: py.channel_buffer_size,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
@ -280,7 +275,7 @@ impl PyRuntime {
|
||||||
|
|
||||||
fn spawn(&self, handler: PyObject) -> PyResult<PyActorAddress> {
|
fn spawn(&self, handler: PyObject) -> PyResult<PyActorAddress> {
|
||||||
let rt = self.inner.as_ref().ok_or_else(|| {
|
let rt = self.inner.as_ref().ok_or_else(|| {
|
||||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
|
pyo3::exceptions::PyRuntimeError::new_err("Runtime not available")
|
||||||
})?;
|
})?;
|
||||||
let actor = PyActor::new(handler);
|
let actor = PyActor::new(handler);
|
||||||
let addr = rt.spawn(actor).map_err(to_py_err)?;
|
let addr = rt.spawn(actor).map_err(to_py_err)?;
|
||||||
|
|
@ -289,14 +284,14 @@ impl PyRuntime {
|
||||||
|
|
||||||
fn send(&self, addr: &PyActorAddress, msg: PyObject) -> PyResult<()> {
|
fn send(&self, addr: &PyActorAddress, msg: PyObject) -> PyResult<()> {
|
||||||
let rt = self.inner.as_ref().ok_or_else(|| {
|
let rt = self.inner.as_ref().ok_or_else(|| {
|
||||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
|
pyo3::exceptions::PyRuntimeError::new_err("Runtime not available")
|
||||||
})?;
|
})?;
|
||||||
rt.send_to(addr.inner, PyMsg(msg)).map_err(to_py_err)
|
rt.send_to(addr.inner, PyMsg(msg)).map_err(to_py_err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inbox(&self) -> PyResult<PyInbox> {
|
fn inbox(&self) -> PyResult<PyInbox> {
|
||||||
let rt = self.inner.as_ref().ok_or_else(|| {
|
let rt = self.inner.as_ref().ok_or_else(|| {
|
||||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
|
pyo3::exceptions::PyRuntimeError::new_err("Runtime not available")
|
||||||
})?;
|
})?;
|
||||||
let inbox: Inbox<PyMsg> = rt.new_inbox().map_err(to_py_err)?;
|
let inbox: Inbox<PyMsg> = rt.new_inbox().map_err(to_py_err)?;
|
||||||
Ok(PyInbox { inner: inbox })
|
Ok(PyInbox { inner: inbox })
|
||||||
|
|
@ -304,97 +299,20 @@ impl PyRuntime {
|
||||||
|
|
||||||
fn tick(&self) -> PyResult<()> {
|
fn tick(&self) -> PyResult<()> {
|
||||||
let rt = self.inner.as_ref().ok_or_else(|| {
|
let rt = self.inner.as_ref().ok_or_else(|| {
|
||||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
|
pyo3::exceptions::PyRuntimeError::new_err("Runtime not available")
|
||||||
})?;
|
})?;
|
||||||
rt.tick();
|
rt.tick();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(&mut self, py: Python<'_>) -> PyResult<PyRuntimeHandle> {
|
|
||||||
let rt = self.inner.take().ok_or_else(|| {
|
|
||||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
|
|
||||||
})?;
|
|
||||||
let handle = py.allow_threads(|| rt.run().map_err(to_py_err))?;
|
|
||||||
Ok(PyRuntimeHandle {
|
|
||||||
inner: Some(handle),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stats(&self) -> PyResult<PyRuntimeStats> {
|
fn stats(&self) -> PyResult<PyRuntimeStats> {
|
||||||
let rt = self.inner.as_ref().ok_or_else(|| {
|
let rt = self.inner.as_ref().ok_or_else(|| {
|
||||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
|
pyo3::exceptions::PyRuntimeError::new_err("Runtime not available")
|
||||||
})?;
|
})?;
|
||||||
Ok(build_stats(rt))
|
Ok(build_stats(rt))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shutdown(&self) -> PyResult<()> {
|
|
||||||
let rt = self.inner.as_ref().ok_or_else(|| {
|
|
||||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
|
|
||||||
})?;
|
|
||||||
rt.shutdown();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── PyRuntimeHandle ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[pyclass(name = "RuntimeHandle")]
|
|
||||||
pub struct PyRuntimeHandle {
|
|
||||||
inner: Option<RuntimeHandle>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pymethods]
|
|
||||||
impl PyRuntimeHandle {
|
|
||||||
fn spawn(&self, handler: PyObject) -> PyResult<PyActorAddress> {
|
|
||||||
let handle = self.inner.as_ref().ok_or_else(|| {
|
|
||||||
pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()")
|
|
||||||
})?;
|
|
||||||
let actor = PyActor::new(handler);
|
|
||||||
let addr = handle.runtime.spawn(actor).map_err(to_py_err)?;
|
|
||||||
Ok(PyActorAddress::from(addr))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send(&self, addr: &PyActorAddress, msg: PyObject) -> PyResult<()> {
|
|
||||||
let handle = self.inner.as_ref().ok_or_else(|| {
|
|
||||||
pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()")
|
|
||||||
})?;
|
|
||||||
handle
|
|
||||||
.runtime
|
|
||||||
.send_to(addr.inner, PyMsg(msg))
|
|
||||||
.map_err(to_py_err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inbox(&self) -> PyResult<PyInbox> {
|
|
||||||
let handle = self.inner.as_ref().ok_or_else(|| {
|
|
||||||
pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()")
|
|
||||||
})?;
|
|
||||||
let inbox: Inbox<PyMsg> = handle.runtime.new_inbox().map_err(to_py_err)?;
|
|
||||||
Ok(PyInbox { inner: inbox })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stats(&self) -> PyResult<PyRuntimeStats> {
|
|
||||||
let handle = self.inner.as_ref().ok_or_else(|| {
|
|
||||||
pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()")
|
|
||||||
})?;
|
|
||||||
Ok(build_stats(&handle.runtime))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shutdown(&self) -> PyResult<()> {
|
|
||||||
let handle = self.inner.as_ref().ok_or_else(|| {
|
|
||||||
pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()")
|
|
||||||
})?;
|
|
||||||
handle.shutdown();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn join(&mut self, py: Python<'_>) -> PyResult<()> {
|
|
||||||
let handle = self.inner.take().ok_or_else(|| {
|
|
||||||
pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()")
|
|
||||||
})?;
|
|
||||||
py.allow_threads(|| handle.join());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── ActorInfo / RuntimeStats ────────────────────────────────────────────────
|
// ─── ActorInfo / RuntimeStats ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -518,7 +436,6 @@ fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||||
m.add_class::<PyInbox>()?;
|
m.add_class::<PyInbox>()?;
|
||||||
m.add_class::<PyRuntimeConfig>()?;
|
m.add_class::<PyRuntimeConfig>()?;
|
||||||
m.add_class::<PyRuntime>()?;
|
m.add_class::<PyRuntime>()?;
|
||||||
m.add_class::<PyRuntimeHandle>()?;
|
|
||||||
m.add_class::<PyActorInfo>()?;
|
m.add_class::<PyActorInfo>()?;
|
||||||
m.add_class::<PyWorkerInfo>()?;
|
m.add_class::<PyWorkerInfo>()?;
|
||||||
m.add_class::<PyRuntimeStats>()?;
|
m.add_class::<PyRuntimeStats>()?;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
name = "wasm-crypto"
|
name = "wasm-crypto"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
name = "wasm-runtime"
|
name = "wasm-runtime"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
|
||||||
|
|
@ -103,10 +103,7 @@ pub struct WasmRuntime {
|
||||||
impl WasmRuntime {
|
impl WasmRuntime {
|
||||||
#[wasm_bindgen(constructor)]
|
#[wasm_bindgen(constructor)]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let rt = Runtime::new(RuntimeConfig {
|
let rt = Runtime::new(RuntimeConfig::default())
|
||||||
num_threads: 1,
|
|
||||||
..RuntimeConfig::default()
|
|
||||||
})
|
|
||||||
.with_extension(Arc::new(StdExtension::new()));
|
.with_extension(Arc::new(StdExtension::new()));
|
||||||
Self { rt }
|
Self { rt }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,4 @@ Keep this crate read-only with respect to observed programs.
|
||||||
- It must not send control signals to observed runtimes.
|
- It must not send control signals to observed runtimes.
|
||||||
- It must not require changes outside `crates/dashboard` for dashboard-only work.
|
- It must not require changes outside `crates/dashboard` for dashboard-only work.
|
||||||
|
|
||||||
Main built-in view: `/view/swactor/workers`, backed by `runtime.stats`, `runtime.workers`, and `runtime.actors` frames when present.
|
Main built-in views: `/view/swactor/workers` (worker-centric) and `/view/swactor/actor-overview` + `/view/swactor/actor-dossier` (actor-centric), all backed by `runtime.stats`, `runtime.workers`, and `runtime.actors` frames when present. Actor views are pure frame consumers and tolerant of publisher shape.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
name = "dashboard"
|
name = "dashboard"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
|
|
@ -10,5 +11,5 @@ swactor = { path = "../..", features = ["serde"] }
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tokio = { version = "1", features = ["net", "rt-multi-thread", "sync"] }
|
tokio.workspace = true
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Read-only HTML/SSE dashboard over incoming datastream frames.
|
Read-only HTML/SSE dashboard over incoming datastream frames.
|
||||||
|
|
||||||
The crate owns the Axum server, bounded raw frame window, and view registry. Component crates can keep their own view implementations beside their code and register them through `DashboardHandle::register_view`. The built-in swactor worker page is hosted here because worker/actor/message processing is universal to swactor programs.
|
The crate owns the Axum server, bounded raw frame window, and view registry. Component crates can keep their own view implementations beside their code and register them through `DashboardHandle::register_view`. The built-in swactor views are hosted here because worker/actor/message processing is universal to swactor programs: the worker page, and the actor overview (fused roster) plus per-actor dossier.
|
||||||
|
|
||||||
## Routes
|
## Routes
|
||||||
|
|
||||||
|
|
@ -16,5 +16,9 @@ The crate owns the Axum server, bounded raw frame window, and view registry. Com
|
||||||
- `GET /api/view/fleet` — fleet and machine telemetry JSON snapshot
|
- `GET /api/view/fleet` — fleet and machine telemetry JSON snapshot
|
||||||
- `GET /view/swactor/workers` — built-in worker page
|
- `GET /view/swactor/workers` — built-in worker page
|
||||||
- `GET /api/view/swactor/workers` — worker page JSON snapshot
|
- `GET /api/view/swactor/workers` — worker page JSON snapshot
|
||||||
|
- `GET /view/swactor/actor-overview` — built-in actor overview + roster page
|
||||||
|
- `GET /api/view/swactor/actor-overview` — actor overview JSON snapshot
|
||||||
|
- `GET /view/swactor/actor-dossier` — built-in per-actor dossier page
|
||||||
|
- `GET /api/view/swactor/actor-dossier` — actor dossier JSON snapshot
|
||||||
|
|
||||||
All state is derived from observed frames. The dashboard sends no control signals back to producers.
|
All state is derived from observed frames. The dashboard sends no control signals back to producers.
|
||||||
|
|
|
||||||
|
|
@ -1,412 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use dashboard::swactor::{RUNTIME_ACTORS, RUNTIME_STATS, RUNTIME_WORKERS};
|
|
||||||
use dashboard::{DashboardConfig, DashboardHandle, FrameEvent, StreamEvent, start_dashboard};
|
|
||||||
use datastream::frame::{ChannelId, Frame, Lifetime, NodeId, Position, StreamId};
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use serde::Serialize;
|
|
||||||
use serde_json::{Value, json};
|
|
||||||
use swactor::actor::{ActorAddress, ActorInterface, Ctx};
|
|
||||||
use swactor::config::RuntimeConfig;
|
|
||||||
use swactor::runtime::Runtime;
|
|
||||||
use swactor::stats::{ActorSnapshot, StatsHook};
|
|
||||||
|
|
||||||
const NODE_ID: &str = "dashboard-swactor-dummy";
|
|
||||||
const WORKER_ACTORS: usize = 12;
|
|
||||||
const PUBLISH_INTERVAL: Duration = Duration::from_millis(250);
|
|
||||||
const PULSE_INTERVAL: Duration = Duration::from_millis(25);
|
|
||||||
const WORK_ITEM_DELAY: Duration = Duration::from_micros(200);
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct PulseTick {
|
|
||||||
seq: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct WorkItem {
|
|
||||||
seq: u64,
|
|
||||||
route: u32,
|
|
||||||
hops_left: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
enum RouterMsg {
|
|
||||||
Configure { workers: Vec<ActorAddress> },
|
|
||||||
Beat { seq: u64 },
|
|
||||||
Complete { worker: u32, seq: u64, route: u32 },
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PulseActor {
|
|
||||||
router: ActorAddress,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActorInterface for PulseActor {
|
|
||||||
type Incoming = PulseTick;
|
|
||||||
type Response = ();
|
|
||||||
|
|
||||||
fn handle(&mut self, ctx: &Ctx, msg: PulseTick) {
|
|
||||||
let _ = ctx.send(self.router, RouterMsg::Beat { seq: msg.seq });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RouterActor {
|
|
||||||
workers: Vec<ActorAddress>,
|
|
||||||
next: usize,
|
|
||||||
completed: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RouterActor {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
workers: Vec::new(),
|
|
||||||
next: 0,
|
|
||||||
completed: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActorInterface for RouterActor {
|
|
||||||
type Incoming = RouterMsg;
|
|
||||||
type Response = ();
|
|
||||||
|
|
||||||
fn handle(&mut self, ctx: &Ctx, msg: RouterMsg) {
|
|
||||||
match msg {
|
|
||||||
RouterMsg::Configure { workers } => {
|
|
||||||
self.workers = workers;
|
|
||||||
self.next = 0;
|
|
||||||
}
|
|
||||||
RouterMsg::Beat { seq } => {
|
|
||||||
if self.workers.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let burst = 48 + (seq as usize % 32);
|
|
||||||
for route in 0..burst {
|
|
||||||
let target = self.workers[self.next % self.workers.len()];
|
|
||||||
self.next = self.next.wrapping_add(1);
|
|
||||||
let _ = ctx.send(
|
|
||||||
target,
|
|
||||||
WorkItem {
|
|
||||||
seq,
|
|
||||||
route: route as u32,
|
|
||||||
hops_left: 1 + ((seq + route as u64) % 3) as u8,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RouterMsg::Complete { worker, seq, route } => {
|
|
||||||
self.completed = self.completed.wrapping_add(1);
|
|
||||||
|
|
||||||
if self.completed % 7 == 0 && !self.workers.is_empty() {
|
|
||||||
let target =
|
|
||||||
self.workers[(worker as usize + route as usize) % self.workers.len()];
|
|
||||||
let _ = ctx.send(
|
|
||||||
target,
|
|
||||||
WorkItem {
|
|
||||||
seq,
|
|
||||||
route: route.wrapping_add(1000),
|
|
||||||
hops_left: 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WorkerActor {
|
|
||||||
id: u32,
|
|
||||||
router: ActorAddress,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActorInterface for WorkerActor {
|
|
||||||
type Incoming = WorkItem;
|
|
||||||
type Response = ();
|
|
||||||
|
|
||||||
fn handle(&mut self, ctx: &Ctx, msg: WorkItem) {
|
|
||||||
if msg.hops_left > 0 {
|
|
||||||
let _ = ctx.send(
|
|
||||||
ctx.self_addr(),
|
|
||||||
WorkItem {
|
|
||||||
seq: msg.seq,
|
|
||||||
route: msg.route,
|
|
||||||
hops_left: msg.hops_left - 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
thread::sleep(WORK_ITEM_DELAY);
|
|
||||||
let _ = ctx.send(
|
|
||||||
self.router,
|
|
||||||
RouterMsg::Complete {
|
|
||||||
worker: self.id,
|
|
||||||
seq: msg.seq,
|
|
||||||
route: msg.route,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct QueuedSinkActor;
|
|
||||||
|
|
||||||
impl ActorInterface for QueuedSinkActor {
|
|
||||||
type Incoming = WorkItem;
|
|
||||||
type Response = ();
|
|
||||||
|
|
||||||
fn handle(&mut self, ctx: &Ctx, _msg: WorkItem) {
|
|
||||||
ctx.suspend_self();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct DashboardStatsHook {
|
|
||||||
actors: Mutex<HashMap<ActorAddress, ActorDetail>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct ActorDetail {
|
|
||||||
worker_id: usize,
|
|
||||||
mailbox_depth: usize,
|
|
||||||
last_msg_type: Option<String>,
|
|
||||||
messages_processed: u64,
|
|
||||||
poisoned: bool,
|
|
||||||
message_type_counts: Vec<(String, u64)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StatsHook for DashboardStatsHook {
|
|
||||||
fn on_tick(&self, worker_id: usize, snapshots: &[ActorSnapshot]) {
|
|
||||||
let mut actors = self.actors.lock();
|
|
||||||
for snapshot in snapshots {
|
|
||||||
actors.insert(
|
|
||||||
snapshot.address,
|
|
||||||
ActorDetail {
|
|
||||||
worker_id,
|
|
||||||
mailbox_depth: snapshot.mailbox_depth,
|
|
||||||
last_msg_type: snapshot.last_msg_type.map(str::to_owned),
|
|
||||||
messages_processed: snapshot.messages_processed,
|
|
||||||
poisoned: snapshot.poisoned,
|
|
||||||
message_type_counts: snapshot
|
|
||||||
.message_type_counts
|
|
||||||
.iter()
|
|
||||||
.map(|(name, count)| ((*name).to_owned(), *count))
|
|
||||||
.collect(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DashboardStatsHook {
|
|
||||||
fn snapshot(
|
|
||||||
&self,
|
|
||||||
live_workers: &[(ActorAddress, usize)],
|
|
||||||
names: &HashMap<ActorAddress, String>,
|
|
||||||
) -> Vec<ActorDetailFrame> {
|
|
||||||
let actors = self.actors.lock();
|
|
||||||
live_workers
|
|
||||||
.iter()
|
|
||||||
.map(|(address, worker_id)| {
|
|
||||||
let detail = actors.get(address);
|
|
||||||
ActorDetailFrame {
|
|
||||||
address: address.to_string(),
|
|
||||||
name: names.get(address).cloned(),
|
|
||||||
worker_id: detail.map_or(*worker_id, |detail| detail.worker_id),
|
|
||||||
mailbox_depth: detail.map_or(0, |detail| detail.mailbox_depth),
|
|
||||||
last_msg_type: detail.and_then(|detail| detail.last_msg_type.clone()),
|
|
||||||
messages_processed: detail.map_or(0, |detail| detail.messages_processed),
|
|
||||||
poisoned: detail.is_some_and(|detail| detail.poisoned),
|
|
||||||
message_type_counts: detail
|
|
||||||
.map(|detail| detail.message_type_counts.clone())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct ActorDetailFrame {
|
|
||||||
address: String,
|
|
||||||
name: Option<String>,
|
|
||||||
worker_id: usize,
|
|
||||||
mailbox_depth: usize,
|
|
||||||
last_msg_type: Option<String>,
|
|
||||||
messages_processed: u64,
|
|
||||||
poisoned: bool,
|
|
||||||
message_type_counts: Vec<(String, u64)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let dashboard = start_dashboard(DashboardConfig::default());
|
|
||||||
dashboard.start_http_standalone();
|
|
||||||
|
|
||||||
let mut runtime = Runtime::new(RuntimeConfig {
|
|
||||||
num_threads: 4,
|
|
||||||
max_actors: 128,
|
|
||||||
channel_buffer_size: 4096,
|
|
||||||
actor_message_budget: 8,
|
|
||||||
});
|
|
||||||
let stats_hook = Arc::new(DashboardStatsHook::default());
|
|
||||||
runtime.set_stats_hook(stats_hook.clone());
|
|
||||||
|
|
||||||
let router = runtime.spawn(RouterActor::new()).expect("spawn router");
|
|
||||||
let mut names = HashMap::new();
|
|
||||||
names.insert(router, "router".to_owned());
|
|
||||||
|
|
||||||
let mut workers = Vec::with_capacity(WORKER_ACTORS);
|
|
||||||
for id in 0..WORKER_ACTORS {
|
|
||||||
let address = runtime
|
|
||||||
.spawn(WorkerActor {
|
|
||||||
id: id as u32,
|
|
||||||
router,
|
|
||||||
})
|
|
||||||
.expect("spawn worker actor");
|
|
||||||
names.insert(address, format!("worker-{id}"));
|
|
||||||
workers.push(address);
|
|
||||||
}
|
|
||||||
|
|
||||||
let pulse = runtime.spawn(PulseActor { router }).expect("spawn pulse");
|
|
||||||
names.insert(pulse, "pulse".to_owned());
|
|
||||||
let queued_sink = runtime.spawn(QueuedSinkActor).expect("spawn queued sink");
|
|
||||||
names.insert(queued_sink, "queued-sink".to_owned());
|
|
||||||
runtime
|
|
||||||
.send_to(
|
|
||||||
router,
|
|
||||||
RouterMsg::Configure {
|
|
||||||
workers: workers.clone(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("configure router");
|
|
||||||
|
|
||||||
let runtime = runtime.run().expect("start swactor runtime");
|
|
||||||
let stream = StreamId::new(NodeId::new(NODE_ID), Lifetime(1));
|
|
||||||
let mut position = 0_u64;
|
|
||||||
let mut seq = 0_u64;
|
|
||||||
let mut ticks_until_publish = 0_u8;
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"dashboard listening at http://127.0.0.1:{}/view/swactor/workers",
|
|
||||||
DashboardConfig::default().port
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"dummy node {NODE_ID} running {} swactor actors",
|
|
||||||
names.len()
|
|
||||||
);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let _ = runtime.runtime.send_to(pulse, PulseTick { seq });
|
|
||||||
if seq % 2 == 0 {
|
|
||||||
let _ = runtime.runtime.send_to(
|
|
||||||
queued_sink,
|
|
||||||
WorkItem {
|
|
||||||
seq,
|
|
||||||
route: u32::MAX,
|
|
||||||
hops_left: 0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
seq = seq.wrapping_add(1);
|
|
||||||
|
|
||||||
if ticks_until_publish == 0 {
|
|
||||||
publish_runtime_snapshot(
|
|
||||||
&dashboard,
|
|
||||||
&stream,
|
|
||||||
&mut position,
|
|
||||||
&runtime.runtime,
|
|
||||||
&stats_hook,
|
|
||||||
&names,
|
|
||||||
);
|
|
||||||
ticks_until_publish = (PUBLISH_INTERVAL.as_millis() / PULSE_INTERVAL.as_millis()) as u8;
|
|
||||||
}
|
|
||||||
ticks_until_publish = ticks_until_publish.saturating_sub(1);
|
|
||||||
|
|
||||||
thread::sleep(PULSE_INTERVAL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn publish_runtime_snapshot(
|
|
||||||
dashboard: &DashboardHandle,
|
|
||||||
stream: &StreamId,
|
|
||||||
position: &mut u64,
|
|
||||||
runtime: &Runtime,
|
|
||||||
stats_hook: &DashboardStatsHook,
|
|
||||||
names: &HashMap<ActorAddress, String>,
|
|
||||||
) {
|
|
||||||
let stats = runtime.stats();
|
|
||||||
let actor_details = stats_hook.snapshot(&stats.actors, names);
|
|
||||||
let actors: Vec<Value> = stats
|
|
||||||
.actors
|
|
||||||
.iter()
|
|
||||||
.map(|(address, worker_id)| json!([address.to_string(), worker_id]))
|
|
||||||
.collect();
|
|
||||||
let workers = serde_json::to_value(&stats.workers).expect("serialize worker stats");
|
|
||||||
let actor_details = serde_json::to_value(actor_details).expect("serialize actor stats");
|
|
||||||
let tick_timings = serde_json::to_value(&stats.tick_timings).expect("serialize tick timings");
|
|
||||||
let total_mailbox_depth: usize = stats
|
|
||||||
.workers
|
|
||||||
.iter()
|
|
||||||
.map(|worker| worker.mailbox_depth)
|
|
||||||
.sum();
|
|
||||||
|
|
||||||
publish_json(
|
|
||||||
dashboard,
|
|
||||||
stream,
|
|
||||||
position,
|
|
||||||
RUNTIME_STATS,
|
|
||||||
json!({
|
|
||||||
"num_workers": stats.num_workers,
|
|
||||||
"uptime_ms": stats.uptime_ms,
|
|
||||||
"actors_live": stats.actors.len(),
|
|
||||||
"mailbox_depth": total_mailbox_depth,
|
|
||||||
"actors": actors,
|
|
||||||
"workers": workers,
|
|
||||||
"actor_details": actor_details,
|
|
||||||
"tick_timings": tick_timings,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
publish_json(
|
|
||||||
dashboard,
|
|
||||||
stream,
|
|
||||||
position,
|
|
||||||
RUNTIME_WORKERS,
|
|
||||||
json!({ "workers": stats.workers }),
|
|
||||||
);
|
|
||||||
|
|
||||||
publish_json(
|
|
||||||
dashboard,
|
|
||||||
stream,
|
|
||||||
position,
|
|
||||||
RUNTIME_ACTORS,
|
|
||||||
json!({ "actors": actor_details }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn publish_json(
|
|
||||||
dashboard: &DashboardHandle,
|
|
||||||
stream: &StreamId,
|
|
||||||
position: &mut u64,
|
|
||||||
channel: &str,
|
|
||||||
value: Value,
|
|
||||||
) {
|
|
||||||
let payload = serde_json::to_vec(&value).expect("serialize dashboard frame");
|
|
||||||
let frame = Frame::new(
|
|
||||||
ChannelId(*position as u32 + 1),
|
|
||||||
Position(*position),
|
|
||||||
payload,
|
|
||||||
);
|
|
||||||
dashboard.publish(FrameEvent {
|
|
||||||
stream: StreamEvent {
|
|
||||||
node: stream.node.as_str().to_string(),
|
|
||||||
life: stream.life.0,
|
|
||||||
},
|
|
||||||
channel: channel.to_owned(),
|
|
||||||
position: *position,
|
|
||||||
payload: frame.payload.clone(),
|
|
||||||
});
|
|
||||||
*position = position.wrapping_add(1);
|
|
||||||
}
|
|
||||||
|
|
@ -97,6 +97,8 @@ impl DashboardHandle {
|
||||||
views.register(Arc::new(live_explorer::LiveDatastreamExplorer::default()));
|
views.register(Arc::new(live_explorer::LiveDatastreamExplorer::default()));
|
||||||
views.register(Arc::new(hardware_view::HardwareDashboardView::default()));
|
views.register(Arc::new(hardware_view::HardwareDashboardView::default()));
|
||||||
views.register(swactor::worker_view());
|
views.register(swactor::worker_view());
|
||||||
|
views.register(swactor::actor_overview_view());
|
||||||
|
views.register(swactor::actor_dossier_view());
|
||||||
let store = Arc::new(DashboardStore::new(
|
let store = Arc::new(DashboardStore::new(
|
||||||
config.raw_frame_history,
|
config.raw_frame_history,
|
||||||
Arc::clone(&views),
|
Arc::clone(&views),
|
||||||
|
|
|
||||||
185
crates/dashboard/src/swactor/actor_dossier.html
Normal file
185
crates/dashboard/src/swactor/actor_dossier.html
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Actor dossier — swactor</title>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#0f172a; --panel:#1e293b; --panel2:#172033; --border:#334155;
|
||||||
|
--text:#e2e8f0; --muted:#94a3b8; --accent:#60a5fa;
|
||||||
|
--running:#34d399; --poisoned:#f87171;
|
||||||
|
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{margin:0;padding:0;background:var(--bg);color:var(--text);}
|
||||||
|
.mono,.table,th,td,.value,.num{font-variant-numeric:tabular-nums;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}
|
||||||
|
header.bar{display:flex;align-items:center;gap:16px;padding:14px 20px;border-bottom:1px solid var(--border);background:var(--panel);position:sticky;top:0;z-index:5;}
|
||||||
|
header.bar h1{font-size:18px;margin:0;font-weight:600;letter-spacing:.01em;}
|
||||||
|
nav.tabs{display:flex;gap:4px;margin-left:auto;}
|
||||||
|
nav.tabs a{padding:6px 12px;border-radius:8px;color:var(--muted);text-decoration:none;font-size:13px;border:1px solid transparent;}
|
||||||
|
nav.tabs a:hover{color:var(--text);background:var(--panel2);}
|
||||||
|
nav.tabs a.cur{color:var(--accent);background:var(--panel2);border-color:var(--border);}
|
||||||
|
select{background:var(--panel2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 8px;font-size:13px;}
|
||||||
|
.wrap{padding:20px;max-width:1100px;margin:0 auto;}
|
||||||
|
.picker{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:18px;}
|
||||||
|
.picker label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;}
|
||||||
|
#pick{min-width:300px;max-width:560px;flex:1;}
|
||||||
|
.back-link{font-size:13px;}
|
||||||
|
.badge{display:inline-block;padding:2px 8px;border-radius:99px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;font-weight:600;}
|
||||||
|
.empty{padding:28px;border:1px dashed #475569;border-radius:12px;color:var(--muted);text-align:center;}
|
||||||
|
.toast{position:fixed;left:50%;bottom:20px;transform:translateX(-50%);background:var(--panel2);border:1px solid var(--border);color:var(--text);padding:10px 16px;border-radius:10px;font-size:13px;max-width:min(90vw,560px);text-align:center;z-index:50;box-shadow:0 8px 24px rgba(0,0,0,.45);}
|
||||||
|
.toast.err{border-color:var(--poisoned);color:var(--poisoned);}
|
||||||
|
.kvs{display:grid;grid-template-columns:max-content 1fr;gap:6px 14px;}
|
||||||
|
.kvs .k{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.05em;}
|
||||||
|
.kvs .v{font-size:13px;word-break:break-word;}
|
||||||
|
.section{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:16px 18px;margin-bottom:16px;}
|
||||||
|
h2{font-size:14px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin:0 0 12px;}
|
||||||
|
.mb-dyn{display:flex;gap:24px;align-items:center;flex-wrap:wrap;}
|
||||||
|
.mb-spark{flex:1 1 320px;min-width:0;}
|
||||||
|
.mb-spark .spark{display:block;width:100%;height:120px;}
|
||||||
|
.mb-vals{display:flex;flex-direction:column;gap:16px;min-width:150px;}
|
||||||
|
.mb-val .label{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.08em;}
|
||||||
|
.mb-val .value{font-size:28px;margin-top:4px;font-weight:600;line-height:1;}
|
||||||
|
.bar-row{display:flex;align-items:center;gap:8px;margin:5px 0;}
|
||||||
|
.bar-row .name{width:48%;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||||
|
.bar-track{flex:1;height:10px;background:var(--panel2);border-radius:99px;overflow:hidden;}
|
||||||
|
.bar-fill{height:100%;background:var(--accent);border-radius:99px;}
|
||||||
|
.bar-row .num{width:90px;text-align:right;color:var(--muted);}
|
||||||
|
.bar-row.last .bar-fill{background:var(--running);}
|
||||||
|
.bar-row.last{font-weight:600;}
|
||||||
|
a{color:var(--accent);text-decoration:none;}
|
||||||
|
.stale{opacity:.55;}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="bar">
|
||||||
|
<h1>Actor dossier</h1>
|
||||||
|
<select id="runtime"></select>
|
||||||
|
<nav class="tabs">
|
||||||
|
<a href="/view/swactor/actor-overview">Overview</a>
|
||||||
|
<a href="/view/swactor/actor-dossier" class="cur">Dossier</a>
|
||||||
|
<a href="/view/swactor/workers">Workers</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="picker">
|
||||||
|
<label for="pick">Actor</label>
|
||||||
|
<select id="pick"></select>
|
||||||
|
<a class="back-link" href="/view/swactor/actor-overview">← back to overview</a>
|
||||||
|
</div>
|
||||||
|
<div id="empty" class="toast" hidden>No runtime actor frames yet.</div>
|
||||||
|
<div id="content"></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const POLL_MS=750;
|
||||||
|
const STATE_COLOR={running:'#34d399',poisoned:'#f87171'};
|
||||||
|
const elRuntime=document.getElementById('runtime');
|
||||||
|
const elPick=document.getElementById('pick');
|
||||||
|
const elEmpty=document.getElementById('empty');
|
||||||
|
const elContent=document.getElementById('content');
|
||||||
|
let runtimes=[],runtimeKey='',selectedAddr='';
|
||||||
|
|
||||||
|
function fmt(n,d=0){if(n==null||n===undefined)return'\u2014';if(typeof n!=='number')return String(n);return n.toLocaleString(undefined,{maximumFractionDigits:d});}
|
||||||
|
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c];});}
|
||||||
|
function shortAddr(a){a=String(a||'');return a.length<=12?a:a.slice(0,6)+'\u2026'+a.slice(-4);}
|
||||||
|
function urlActor(){try{return new URLSearchParams(location.search).get('actor');}catch(e){return null;}}
|
||||||
|
function stateBadge(a){const s=a.poisoned?'poisoned':'running';const c=STATE_COLOR[s];return '<span class="badge" style="color:'+c+';background:'+c+'22">'+s+'</span>';}
|
||||||
|
function growthColor(g){if(g>1.5)return '#f87171';if(g>0.3)return '#fbbf24';return '#34d399';}
|
||||||
|
function areaChart(data,w,h,color){
|
||||||
|
if(!data||data.length<2)return '<svg class="spark" viewBox="0 0 '+w+' '+h+'" width="100%" height="'+h+'" preserveAspectRatio="none"></svg>';
|
||||||
|
const max=Math.max(1,...data);const n=data.length;
|
||||||
|
let line='',area='0,'+h+' ';
|
||||||
|
for(let i=0;i<n;i++){const x=(i/(n-1)*w).toFixed(1);const y=(h-(data[i]/max)*(h-2)-1).toFixed(1);line+=x+','+y+' ';area+=x+','+y+' ';}
|
||||||
|
area+=' '+w+','+h;
|
||||||
|
return '<svg class="spark" viewBox="0 0 '+w+' '+h+'" width="100%" height="'+h+'" preserveAspectRatio="none">'+
|
||||||
|
'<polygon points="'+area.trim()+'" fill="'+(color||'#60a5fa')+'22"/>'+
|
||||||
|
'<polyline points="'+line.trim()+'" fill="none" stroke="'+(color||'#60a5fa')+'" stroke-width="1.5"/></svg>';
|
||||||
|
}
|
||||||
|
function kv(k,vhtml){return '<div class="k">'+esc(k)+'</div><div class="v">'+vhtml+'</div>';}
|
||||||
|
function typeShort(t){t=String(t||'');const i=t.lastIndexOf('::');return i>=0?t.slice(i+2):t;}
|
||||||
|
function pickRuntime(list){
|
||||||
|
if(!runtimeKey||!list.some(r=>r.stream.key===runtimeKey)){
|
||||||
|
const live=list.find(r=>r.live)||list[0];runtimeKey=live?live.stream.key:'';
|
||||||
|
}
|
||||||
|
return list.find(r=>r.stream.key===runtimeKey)||null;
|
||||||
|
}
|
||||||
|
function currentActors(rt){return rt?(rt.actors||[]):[];}
|
||||||
|
function findActor(rt){
|
||||||
|
const actors=currentActors(rt);
|
||||||
|
if(selectedAddr){const hit=actors.find(a=>a.address===selectedAddr);if(hit)return hit;}
|
||||||
|
return actors[0]||null;
|
||||||
|
}
|
||||||
|
function renderPicker(rt){
|
||||||
|
const actors=currentActors(rt);
|
||||||
|
elPick.innerHTML=actors.map(a=>
|
||||||
|
'<option value="'+esc(a.address)+'">'+esc(shortAddr(a.address))+(a.name?' \u00b7 '+esc(a.name):'')+'</option>'
|
||||||
|
).join('');
|
||||||
|
const cur=findActor(rt);
|
||||||
|
if(cur){selectedAddr=cur.address;elPick.value=selectedAddr;}
|
||||||
|
}
|
||||||
|
function renderRuntimeSelector(){
|
||||||
|
elRuntime.innerHTML=runtimes.map(r=>
|
||||||
|
'<option value="'+esc(r.stream.key)+'">'+esc(r.stream.node)+'#'+r.stream.life+(r.live?'':' (stale)')+'</option>'
|
||||||
|
).join('');
|
||||||
|
if(runtimeKey)elRuntime.value=runtimeKey;
|
||||||
|
}
|
||||||
|
function render(a,rt){
|
||||||
|
if(!a){elContent.innerHTML='<div class="empty">Select an actor. None are present on the current frame.</div>';return;}
|
||||||
|
const lastType=a.last_msg_type||'\u2014';
|
||||||
|
const diet=(a.message_type_counts||[]).slice();
|
||||||
|
diet.sort((x,y)=>y.count-x.count);
|
||||||
|
const dmax=Math.max(1,...diet.map(d=>d.count));
|
||||||
|
const lastName=a.last_msg_type;
|
||||||
|
const dietHtml=diet.length?diet.map(d=>{
|
||||||
|
const cls=d.message_type===lastName?'bar-row last':'bar-row';
|
||||||
|
return '<div class="'+cls+'"><span class="name">'+esc(d.message_type)+'</span>'+
|
||||||
|
'<div class="bar-track"><div class="bar-fill" style="width:'+(d.count/dmax*100).toFixed(1)+'%"></div></div>'+
|
||||||
|
'<span class="num">'+fmt(d.count)+'</span></div>';
|
||||||
|
}).join(''):'<div class="empty">no message-type counts on frame</div>';
|
||||||
|
const hist=(a.history||[]).map(h=>h.mailbox_depth);
|
||||||
|
const g=a.mailbox_growth||0;
|
||||||
|
elContent.innerHTML=
|
||||||
|
'<div class="section"><h2>Identity & lifecycle</h2><div class="kvs">'+
|
||||||
|
kv('address','<span class="mono">'+esc(a.address)+'</span>')+
|
||||||
|
(a.name?kv('name',esc(a.name)):'')+
|
||||||
|
(a.actor_type?kv('actor type','<span class="mono" title="'+esc(a.actor_type)+'">'+esc(typeShort(a.actor_type))+'</span>'):'')+
|
||||||
|
(a.message_type?kv('message type','<span class="mono" title="'+esc(a.message_type)+'">'+esc(typeShort(a.message_type))+'</span>'):'')+
|
||||||
|
kv('state',stateBadge(a))+
|
||||||
|
kv('worker',a.worker_id==null?'\u2014':esc(a.worker_id))+
|
||||||
|
kv('last message',esc(lastType))+
|
||||||
|
kv('last seen',a.last_seen_ms_ago!=null?(a.last_seen_ms_ago/1000).toFixed(1)+'s ago':'\u2014')+
|
||||||
|
'</div></div>'+
|
||||||
|
'<div class="section"><h2>Mailbox dynamics</h2><div class="mb-dyn">'+
|
||||||
|
'<div class="mb-spark">'+areaChart(hist,720,120,growthColor(g))+'</div>'+
|
||||||
|
'<div class="mb-vals">'+
|
||||||
|
'<div class="mb-val"><div class="label">depth</div><div class="value" style="color:'+growthColor(g)+'">'+fmt(a.mailbox_depth)+'</div></div>'+
|
||||||
|
'<div class="mb-val"><div class="label">growth</div><div class="value">'+fmt(g,1)+'/s</div></div>'+
|
||||||
|
'<div class="mb-val"><div class="label">msg/s</div><div class="value">'+fmt(a.msg_per_sec,1)+'</div></div>'+
|
||||||
|
'</div>'+
|
||||||
|
'</div></div>'+
|
||||||
|
'<div class="section"><h2>Message diet</h2>'+dietHtml+'</div>';
|
||||||
|
}
|
||||||
|
async function refresh(){
|
||||||
|
try{
|
||||||
|
const res=await fetch('/api/view/swactor/actor-dossier',{cache:'no-store'});
|
||||||
|
if(!res.ok)throw new Error('HTTP '+res.status);
|
||||||
|
const data=await res.json();
|
||||||
|
runtimes=data.runtimes||[];
|
||||||
|
renderRuntimeSelector();
|
||||||
|
const rt=pickRuntime(runtimes);
|
||||||
|
if(rt&¤tActors(rt).length>0){elEmpty.hidden=true;}else{elEmpty.className='toast';elEmpty.textContent='No runtime actor frames yet.';elEmpty.hidden=false;}
|
||||||
|
renderPicker(rt);
|
||||||
|
render(findActor(rt),rt);
|
||||||
|
}catch(e){
|
||||||
|
elEmpty.className='toast err';elEmpty.hidden=false;elEmpty.textContent='Failed to load actor frames: '+e.message;elContent.innerHTML='';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elRuntime.addEventListener('change',e=>{runtimeKey=e.target.value;const rt=pickRuntime(runtimes);renderPicker(rt);render(findActor(rt),rt);});
|
||||||
|
elPick.addEventListener('change',e=>{selectedAddr=e.target.value;const rt=pickRuntime(runtimes);render(findActor(rt),rt);});
|
||||||
|
const q=urlActor();if(q)selectedAddr=q;
|
||||||
|
refresh();
|
||||||
|
setInterval(refresh,POLL_MS);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
312
crates/dashboard/src/swactor/actor_overview.html
Normal file
312
crates/dashboard/src/swactor/actor_overview.html
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Actor overview — swactor</title>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#0f172a; --panel:#1e293b; --panel2:#172033; --border:#334155;
|
||||||
|
--text:#e2e8f0; --muted:#94a3b8; --accent:#60a5fa;
|
||||||
|
--running:#34d399; --poisoned:#f87171;
|
||||||
|
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{margin:0;padding:0;background:var(--bg);color:var(--text);}
|
||||||
|
.mono,.table,th,td,.value,.num{font-variant-numeric:tabular-nums;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}
|
||||||
|
header.bar{display:flex;align-items:center;gap:16px;padding:14px 20px;border-bottom:1px solid var(--border);background:var(--panel);position:sticky;top:0;z-index:5;}
|
||||||
|
header.bar h1{font-size:18px;margin:0;font-weight:600;letter-spacing:.01em;}
|
||||||
|
nav.tabs{display:flex;gap:4px;margin-left:auto;}
|
||||||
|
nav.tabs a{padding:6px 12px;border-radius:8px;color:var(--muted);text-decoration:none;font-size:13px;border:1px solid transparent;}
|
||||||
|
nav.tabs a:hover{color:var(--text);background:var(--panel2);}
|
||||||
|
nav.tabs a.cur{color:var(--accent);background:var(--panel2);border-color:var(--border);}
|
||||||
|
select{background:var(--panel2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 8px;font-size:13px;}
|
||||||
|
.wrap{padding:20px;max-width:1400px;margin:0 auto;}
|
||||||
|
.grid{display:grid;gap:12px;}
|
||||||
|
.cards{grid-template-columns:repeat(auto-fit,minmax(150px,1fr));margin-bottom:16px;}
|
||||||
|
.card{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:14px;}
|
||||||
|
.card .label{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.08em;}
|
||||||
|
.card .value{font-size:26px;margin-top:6px;font-weight:600;}
|
||||||
|
.card .value.bad{color:var(--poisoned);}
|
||||||
|
.badge{display:inline-block;padding:2px 8px;border-radius:99px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;font-weight:600;}
|
||||||
|
.table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden;}
|
||||||
|
.table th,.table td{padding:8px 10px;border-bottom:1px solid #1f2937;text-align:left;white-space:nowrap;}
|
||||||
|
.table th{color:#93c5fd;background:var(--panel2);cursor:pointer;user-select:none;font-size:12px;text-transform:uppercase;letter-spacing:.05em;}
|
||||||
|
.table th:hover{color:#fff;}
|
||||||
|
.table tr{cursor:pointer;}
|
||||||
|
.table tbody tr:hover{background:#1d4ed822;}
|
||||||
|
.table td.muted,.muted{color:var(--muted);}
|
||||||
|
.spark{display:block;}
|
||||||
|
.bar-row{display:flex;align-items:center;gap:8px;margin:5px 0;}
|
||||||
|
.bar-row .name{width:38%;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||||
|
.bar-track{flex:1;height:10px;background:var(--panel2);border-radius:99px;overflow:hidden;}
|
||||||
|
.bar-fill{height:100%;background:var(--accent);border-radius:99px;}
|
||||||
|
.bar-row .num{width:90px;text-align:right;color:var(--muted);}
|
||||||
|
input{background:var(--panel2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 8px;font-size:13px;}
|
||||||
|
.filters{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:14px;}
|
||||||
|
.filters label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;}
|
||||||
|
h2{font-size:14px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin:22px 0 10px;}
|
||||||
|
.empty{padding:28px;border:1px dashed #475569;border-radius:12px;color:var(--muted);text-align:center;}
|
||||||
|
.toast{position:fixed;left:50%;bottom:20px;transform:translateX(-50%);background:var(--panel2);border:1px solid var(--border);color:var(--text);padding:10px 16px;border-radius:10px;font-size:13px;max-width:min(90vw,560px);text-align:center;z-index:50;box-shadow:0 8px 24px rgba(0,0,0,.45);}
|
||||||
|
.toast.err{border-color:var(--poisoned);color:var(--poisoned);}
|
||||||
|
.stacked{display:flex;height:18px;border-radius:6px;overflow:hidden;border:1px solid var(--border);}
|
||||||
|
.legend{display:flex;gap:14px;flex-wrap:wrap;margin-top:8px;font-size:12px;color:var(--muted);}
|
||||||
|
.legend i{display:inline-block;width:12px;height:12px;margin-right:5px;border-radius:3px;vertical-align:middle;}
|
||||||
|
a{color:var(--accent);text-decoration:none;}
|
||||||
|
.roster-box{max-height:520px;overflow:auto;border:1px solid var(--border);border-radius:12px;}
|
||||||
|
.roster-box .table{border-radius:0;border:none;}
|
||||||
|
.stale{opacity:.55;}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="bar">
|
||||||
|
<h1>Actor overview</h1>
|
||||||
|
<select id="runtime"></select>
|
||||||
|
<nav class="tabs">
|
||||||
|
<a href="/view/swactor/actor-overview" class="cur">Overview</a>
|
||||||
|
<a href="/view/swactor/actor-dossier">Dossier</a>
|
||||||
|
<a href="/view/swactor/workers">Workers</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<div class="wrap">
|
||||||
|
<div id="empty" class="toast" hidden>No runtime actor frames yet. A live swactor runtime publishing <code>runtime.actors</code> will populate this page.</div>
|
||||||
|
<div id="cards" class="grid cards"></div>
|
||||||
|
|
||||||
|
<h2>State distribution</h2>
|
||||||
|
<div class="stacked" id="dist"></div>
|
||||||
|
<div class="legend" id="distLegend"></div>
|
||||||
|
|
||||||
|
<h2>Throughput</h2>
|
||||||
|
<div id="throughput"></div>
|
||||||
|
|
||||||
|
<h2>Worker balance</h2>
|
||||||
|
<div id="workers"></div>
|
||||||
|
|
||||||
|
<h2>Hot actors — mailbox growth</h2>
|
||||||
|
<div id="hot"></div>
|
||||||
|
|
||||||
|
<h2>Alerts</h2>
|
||||||
|
<div id="alerts"></div>
|
||||||
|
|
||||||
|
<h2>Roster</h2>
|
||||||
|
<div class="filters">
|
||||||
|
<label for="f-search">search</label>
|
||||||
|
<input id="f-search" type="search" placeholder="name · addr · worker" autocomplete="off" spellcheck="false">
|
||||||
|
<label for="f-state">state</label>
|
||||||
|
<select id="f-state">
|
||||||
|
<option value="all">all states</option>
|
||||||
|
<option value="running">running</option>
|
||||||
|
<option value="poisoned">poisoned</option>
|
||||||
|
</select>
|
||||||
|
<label for="f-worker">worker</label>
|
||||||
|
<select id="f-worker"></select>
|
||||||
|
</div>
|
||||||
|
<div class="roster-box">
|
||||||
|
<table class="table">
|
||||||
|
<thead><tr id="head"></tr></thead>
|
||||||
|
<tbody id="rows"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const POLL_MS=750;
|
||||||
|
const elRuntime=document.getElementById('runtime');
|
||||||
|
const elEmpty=document.getElementById('empty');
|
||||||
|
const elCards=document.getElementById('cards');
|
||||||
|
const elDist=document.getElementById('dist');
|
||||||
|
const elDistLegend=document.getElementById('distLegend');
|
||||||
|
const elThroughput=document.getElementById('throughput');
|
||||||
|
const elWorkers=document.getElementById('workers');
|
||||||
|
const elHot=document.getElementById('hot');
|
||||||
|
const elAlerts=document.getElementById('alerts');
|
||||||
|
const elHead=document.getElementById('head');
|
||||||
|
const elRows=document.getElementById('rows');
|
||||||
|
const elSearch=document.getElementById('f-search');
|
||||||
|
const elState=document.getElementById('f-state');
|
||||||
|
const elWorker=document.getElementById('f-worker');
|
||||||
|
|
||||||
|
const STATE_COLOR={running:'#34d399',poisoned:'#f87171'};
|
||||||
|
const COLS=[['actor','Actor'],['type','Type'],['state','State'],['mailbox','Mailbox'],['rate','Msg/s'],
|
||||||
|
['processed','Processed'],['last','Last msg'],['worker','Worker']];
|
||||||
|
const DEFDIR={actor:'asc',type:'asc',state:'asc',mailbox:'desc',rate:'desc',processed:'desc',last:'asc',worker:'asc'};
|
||||||
|
let sortKey='mailbox',sortDir='desc';
|
||||||
|
let fSearch='',fState='all',fWorker='all';
|
||||||
|
let runtimes=[],runtimeKey='';
|
||||||
|
|
||||||
|
function fmt(n,d=0){if(n==null||n===undefined)return'\u2014';if(typeof n!=='number')return String(n);return n.toLocaleString(undefined,{maximumFractionDigits:d});}
|
||||||
|
function ago(sec){sec=Math.max(0,Math.round(sec));if(sec<60)return sec+'s';const m=Math.floor(sec/60);if(m<60)return m+'m';return Math.floor(m/60)+'h';}
|
||||||
|
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c];});}
|
||||||
|
function shortAddr(a){a=String(a||'');return a.length<=12?a:a.slice(0,6)+'\u2026'+a.slice(-4);}
|
||||||
|
function typeShort(t){t=String(t||'');const i=t.lastIndexOf('::');return i>=0?t.slice(i+2):t;}
|
||||||
|
function stateBadge(a){const s=a.poisoned?'poisoned':'running';const c=STATE_COLOR[s];return '<span class="badge" style="color:'+c+';background:'+c+'22">'+s+'</span>';}
|
||||||
|
function growthColor(g){if(g>1.5)return '#f87171';if(g>0.3)return '#fbbf24';return '#34d399';}
|
||||||
|
function sparkline(data,w,h,color){
|
||||||
|
if(!data||data.length<2)return '<svg class="spark" viewBox="0 0 '+w+' '+h+'" width="'+w+'" height="'+h+'"></svg>';
|
||||||
|
const max=Math.max(1,...data);const n=data.length;
|
||||||
|
let pts='';for(let i=0;i<n;i++)pts+=(i/(n-1)*w).toFixed(1)+','+(h-(data[i]/max)*(h-2)-1).toFixed(1)+' ';
|
||||||
|
return '<svg class="spark" viewBox="0 0 '+w+' '+h+'" width="'+w+'" height="'+h+'" preserveAspectRatio="none"><polyline points="'+pts.trim()+'" fill="none" stroke="'+(color||'#60a5fa')+'" stroke-width="1.5"/></svg>';
|
||||||
|
}
|
||||||
|
function pickRuntime(list){
|
||||||
|
if(!runtimeKey||!list.some(r=>r.stream.key===runtimeKey)){
|
||||||
|
const live=list.find(r=>r.live)||list[0];
|
||||||
|
runtimeKey=live?live.stream.key:'';
|
||||||
|
}
|
||||||
|
return list.find(r=>r.stream.key===runtimeKey)||null;
|
||||||
|
}
|
||||||
|
function renderRuntimeSelector(){
|
||||||
|
elRuntime.innerHTML=runtimes.map(r=>
|
||||||
|
'<option value="'+esc(r.stream.key)+'">'+esc(r.stream.node)+'#'+r.stream.life+(r.live?'':' (stale)')+'</option>'
|
||||||
|
).join('');
|
||||||
|
if(runtimeKey)elRuntime.value=runtimeKey;
|
||||||
|
}
|
||||||
|
function renderCards(rt){
|
||||||
|
const s=rt.summary||{};
|
||||||
|
const uptime=s.uptime_ms!=null?ago(s.uptime_ms/1000):'\u2014';
|
||||||
|
elCards.innerHTML=
|
||||||
|
'<div class="card"><div class="label">Actors</div><div class="value">'+fmt(s.actors)+'</div></div>'+
|
||||||
|
'<div class="card"><div class="label">msg/s</div><div class="value">'+fmt(s.msg_per_sec,1)+'</div></div>'+
|
||||||
|
'<div class="card"><div class="label">Mailbox</div><div class="value">'+fmt(s.mailbox_depth)+'</div></div>'+
|
||||||
|
'<div class="card"><div class="label">Poisoned</div><div class="value'+(s.poisoned>0?' bad':'')+'">'+fmt(s.poisoned)+'</div></div>'+
|
||||||
|
'<div class="card"><div class="label">Uptime</div><div class="value">'+uptime+'</div></div>';
|
||||||
|
}
|
||||||
|
function renderDist(rt){
|
||||||
|
const actors=rt.actors||[];const n=actors.length||1;
|
||||||
|
let running=0,poisoned=0;
|
||||||
|
actors.forEach(a=>{if(a.poisoned)poisoned++;else running++;});
|
||||||
|
let segs='';
|
||||||
|
if(running)segs+='<div style="width:'+(running/n*100).toFixed(2)+'%;background:'+STATE_COLOR.running+';"></div>';
|
||||||
|
if(poisoned)segs+='<div style="width:'+(poisoned/n*100).toFixed(2)+'%;background:'+STATE_COLOR.poisoned+';"></div>';
|
||||||
|
elDist.innerHTML=segs||'<div style="width:100%;background:var(--panel2);"></div>';
|
||||||
|
elDistLegend.innerHTML='<span><i style="background:'+STATE_COLOR.running+'"></i>running: '+running+'</span>'+
|
||||||
|
'<span><i style="background:'+STATE_COLOR.poisoned+'"></i>poisoned: '+poisoned+'</span>';
|
||||||
|
}
|
||||||
|
function renderThroughput(rt){
|
||||||
|
const h=(rt.history||[]).map(x=>x.msg_per_sec);
|
||||||
|
const mb=(rt.history||[]).map(x=>x.mailbox_depth);
|
||||||
|
elThroughput.innerHTML=
|
||||||
|
sparkline(h,760,90,'#60a5fa')+'<div style="height:6px"></div>'+
|
||||||
|
sparkline(mb,760,90,'#fbbf24')+
|
||||||
|
'<div class="legend"><span><i style="background:#60a5fa"></i>msg/s</span><span><i style="background:#fbbf24"></i>mailbox</span></div>';
|
||||||
|
}
|
||||||
|
function renderWorkers(rt){
|
||||||
|
const counts={};
|
||||||
|
(rt.actors||[]).forEach(a=>{const w=a.worker_id==null?'?':a.worker_id;counts[w]=(counts[w]||0)+1;});
|
||||||
|
const keys=Object.keys(counts).sort();
|
||||||
|
const max=Math.max(1,...Object.values(counts));
|
||||||
|
elWorkers.innerHTML=keys.map(w=>{
|
||||||
|
const c=counts[w];
|
||||||
|
return '<div class="bar-row"><span class="name">worker '+esc(w)+'</span>'+
|
||||||
|
'<div class="bar-track"><div class="bar-fill" style="width:'+(c/max*100).toFixed(1)+'%"></div></div>'+
|
||||||
|
'<span class="num">'+c+'</span></div>';
|
||||||
|
}).join('')||'<div class="empty">no worker placement on frame</div>';
|
||||||
|
}
|
||||||
|
function renderHot(rt){
|
||||||
|
const hot=(rt.actors||[]).slice().sort((a,b)=>(b.mailbox_growth||0)-(a.mailbox_growth||0)).slice(0,8);
|
||||||
|
const gmax=Math.max(1,...hot.map(a=>Math.max(0,a.mailbox_growth||0)));
|
||||||
|
elHot.innerHTML=hot.map(a=>{
|
||||||
|
const g=a.mailbox_growth||0;const gc=growthColor(g);
|
||||||
|
return '<div class="bar-row" data-addr="'+esc(a.address)+'">'+
|
||||||
|
'<span class="name">'+shortAddr(a.address)+(a.name?' '+esc(a.name):'')+'</span>'+
|
||||||
|
'<div class="bar-track"><div class="bar-fill" style="width:'+(Math.max(0,g)/gmax*100).toFixed(1)+'%;background:'+gc+'"></div></div>'+
|
||||||
|
'<span class="num">'+fmt(g,1)+'/s</span></div>';
|
||||||
|
}).join('')||'<div class="empty">no actors</div>';
|
||||||
|
}
|
||||||
|
function renderAlerts(rt){
|
||||||
|
const alerted=(rt.actors||[]).filter(a=>a.poisoned||(a.mailbox_growth||0)>1.5);
|
||||||
|
if(!alerted.length){elAlerts.innerHTML='<div class="empty">fleet healthy</div>';return;}
|
||||||
|
alerted.sort((a,b)=>{const ap=a.poisoned?0:1,bp=b.poisoned?0:1;if(ap!==bp)return ap-bp;return (b.mailbox_growth||0)-(a.mailbox_growth||0);});
|
||||||
|
elAlerts.innerHTML='<table class="table"><tbody>'+
|
||||||
|
alerted.map(a=>{
|
||||||
|
const reason=a.poisoned?'poisoned':'saturating';
|
||||||
|
return '<tr data-addr="'+esc(a.address)+'"><td>'+shortAddr(a.address)+(a.name?' '+esc(a.name):'')+' '+stateBadge(a)+'</td>'+
|
||||||
|
'<td class="muted">'+reason+'</td><td style="text-align:right;color:var(--muted);width:1%;">\u2192</td></tr>';
|
||||||
|
}).join('')+'</tbody></table>';
|
||||||
|
}
|
||||||
|
function colValue(a,key){
|
||||||
|
switch(key){
|
||||||
|
case 'actor':return (a.name||a.address).toLowerCase();
|
||||||
|
case 'type':return typeShort(a.actor_type).toLowerCase();
|
||||||
|
case 'state':return a.poisoned?'poisoned':'running';
|
||||||
|
case 'mailbox':return a.mailbox_depth||0;
|
||||||
|
case 'rate':return a.msg_per_sec||0;
|
||||||
|
case 'processed':return a.messages_processed||0;
|
||||||
|
case 'last':return a.last_msg_type||'';
|
||||||
|
case 'worker':return a.worker_id==null?-1:a.worker_id;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
function visible(rt){
|
||||||
|
let list=rt.actors||[];
|
||||||
|
if(fSearch){const q=fSearch.toLowerCase();list=list.filter(a=>((a.name?a.name+' ':'')+(a.actor_type?typeShort(a.actor_type)+' '+a.actor_type+' ':'')+a.address+' '+(a.worker_id==null?'':a.worker_id)).toLowerCase().indexOf(q)>=0);}
|
||||||
|
if(fState!=='all')list=list.filter(a=>(a.poisoned?'poisoned':'running')===fState);
|
||||||
|
if(fWorker!=='all')list=list.filter(a=>String(a.worker_id)===fWorker);
|
||||||
|
const k=sortKey,dir=sortDir==='asc'?1:-1;
|
||||||
|
return list.slice().sort((x,y)=>{const vx=colValue(x,k),vy=colValue(y,k);let r;if(typeof vx==='number'&&typeof vy==='number')r=vx-vy;else r=String(vx).localeCompare(String(vy));return r*dir;});
|
||||||
|
}
|
||||||
|
function renderHead(){
|
||||||
|
elHead.innerHTML=COLS.map(c=>{
|
||||||
|
const key=c[0],label=c[1];
|
||||||
|
const arrow=sortKey===key?(sortDir==='asc'?' \u25B2':' \u25BC'):'';
|
||||||
|
const order=sortKey===key?(sortDir==='asc'?'ascending':'descending'):'none';
|
||||||
|
return '<th data-key="'+key+'" scope="col" aria-sort="'+order+'">'+label+arrow+'</th>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
function populateWorkerFilter(rt){
|
||||||
|
const seen={};(rt.actors||[]).forEach(a=>{if(a.worker_id!=null)seen[a.worker_id]=1;});
|
||||||
|
const keys=Object.keys(seen).sort((a,b)=>a-b);
|
||||||
|
elWorker.innerHTML='<option value="all">all workers</option>'+keys.map(w=>'<option value="'+esc(w)+'">'+esc(w)+'</option>').join('');
|
||||||
|
if(fWorker!=='all'&&!keys.includes(fWorker))fWorker='all';
|
||||||
|
elWorker.value=fWorker;
|
||||||
|
}
|
||||||
|
function renderRows(rt){
|
||||||
|
const list=visible(rt);
|
||||||
|
if(!list.length){elRows.innerHTML='<tr><td colspan="'+COLS.length+'"><div class="empty">No actors match the current filters.</div></td></tr>';return;}
|
||||||
|
elRows.innerHTML=list.map(a=>{
|
||||||
|
const hist=(a.history||[]).map(h=>h.mailbox_depth);
|
||||||
|
const gc=growthColor(a.mailbox_growth||0);
|
||||||
|
const mbox='<span style="display:inline-flex;align-items:center;gap:8px">'+
|
||||||
|
'<span style="color:'+gc+';font-weight:600">'+fmt(a.mailbox_depth)+'</span>'+sparkline(hist,60,16,gc)+'</span>';
|
||||||
|
const actorCell=shortAddr(a.address)+(a.name?'<br><span class="muted" style="font-size:11px">'+esc(a.name)+'</span>':'');
|
||||||
|
const typeCell=a.actor_type?'<span class="muted" title="'+esc(a.actor_type)+'">'+esc(typeShort(a.actor_type))+'</span>':'<span class="muted">\u2014</span>';
|
||||||
|
return '<tr data-addr="'+esc(a.address)+'">'+
|
||||||
|
'<td>'+actorCell+'</td><td>'+typeCell+'</td><td>'+stateBadge(a)+'</td><td>'+mbox+'</td>'+
|
||||||
|
'<td>'+fmt(a.msg_per_sec,1)+'</td><td>'+fmt(a.messages_processed)+'</td>'+
|
||||||
|
'<td><span class="muted" style="display:inline-block;max-width:240px;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom">'+esc(a.last_msg_type)+'</span></td>'+
|
||||||
|
'<td>'+(a.worker_id==null?'<span class="muted">\u2014</span>':a.worker_id)+'</td></tr>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
function render(rt){
|
||||||
|
if(!rt){[elCards,elDist,elThroughput,elWorkers,elHot,elAlerts,elRows].forEach(e=>e.innerHTML='');return;}
|
||||||
|
elEmpty.hidden=true;
|
||||||
|
renderCards(rt);renderDist(rt);renderThroughput(rt);renderWorkers(rt);renderHot(rt);renderAlerts(rt);
|
||||||
|
populateWorkerFilter(rt);renderHead();renderRows(rt);
|
||||||
|
}
|
||||||
|
async function refresh(){
|
||||||
|
try{
|
||||||
|
const res=await fetch('/api/view/swactor/actor-overview',{cache:'no-store'});
|
||||||
|
if(!res.ok)throw new Error('HTTP '+res.status);
|
||||||
|
const data=await res.json();
|
||||||
|
runtimes=data.runtimes||[];
|
||||||
|
renderRuntimeSelector();
|
||||||
|
const rt=pickRuntime(runtimes);
|
||||||
|
if(rt){elEmpty.hidden=true;}else{elEmpty.className='toast';elEmpty.textContent='No runtime actor frames yet. A live swactor runtime publishing runtime.actors will populate this page.';elEmpty.hidden=false;}
|
||||||
|
render(rt);
|
||||||
|
}catch(e){
|
||||||
|
elEmpty.className='toast err';
|
||||||
|
elEmpty.textContent='Failed to load actor frames: '+e.message;
|
||||||
|
elEmpty.hidden=false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function setSort(key){if(sortKey===key)sortDir=sortDir==='asc'?'desc':'asc';else{sortKey=key;sortDir=DEFDIR[key]||'asc';}renderHead();renderRows(pickRuntime(runtimes));}
|
||||||
|
elRuntime.addEventListener('change',e=>{runtimeKey=e.target.value;render(pickRuntime(runtimes));});
|
||||||
|
elSearch.addEventListener('input',e=>{fSearch=e.target.value.toLowerCase();renderRows(pickRuntime(runtimes));});
|
||||||
|
elState.addEventListener('change',e=>{fState=e.target.value;renderRows(pickRuntime(runtimes));});
|
||||||
|
elWorker.addEventListener('change',e=>{fWorker=e.target.value;renderRows(pickRuntime(runtimes));});
|
||||||
|
elHead.addEventListener('click',e=>{const th=e.target.closest('th[data-key]');if(th)setSort(th.getAttribute('data-key'));});
|
||||||
|
document.addEventListener('click',e=>{const el=e.target.closest('[data-addr]');if(el)location.href='/view/swactor/actor-dossier?actor='+encodeURIComponent(el.getAttribute('data-addr'));});
|
||||||
|
renderHead();
|
||||||
|
refresh();
|
||||||
|
setInterval(refresh,POLL_MS);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
635
crates/dashboard/src/swactor/actor_view.rs
Normal file
635
crates/dashboard/src/swactor/actor_view.rs
Normal file
|
|
@ -0,0 +1,635 @@
|
||||||
|
//! Actor-centric read-only view over `runtime.actors` / `runtime.stats` frames.
|
||||||
|
//!
|
||||||
|
//! Pure frame consumer: it folds incoming per-actor snapshots into local state
|
||||||
|
//! and exposes JSON + HTML. It sends nothing back to observed runtimes. Parsing
|
||||||
|
//! is intentionally tolerant of publisher shape — a frame is a flat record that
|
||||||
|
//! may come from the per-worker `DatastreamStatsHook` envelope
|
||||||
|
//! (`{worker_id, actors:[...]}`) or from a process that publishes a merged
|
||||||
|
//! `{actors:[...]}` payload (the dashboard dummy node, app runtimes). Fields the
|
||||||
|
//! publisher includes (name, worker_id, lifecycle flags) are displayed; ones it
|
||||||
|
//! omits are left blank rather than fabricated.
|
||||||
|
//!
|
||||||
|
//! Reliably on the frame today: address, mailbox depth, throughput, last
|
||||||
|
//! message, poisoned flag, and the message-type diet. `worker_id` and `name`
|
||||||
|
//! appear when the publisher sends them. Finer lifecycle granularity
|
||||||
|
//! (new/suspended/stopping), actor Rust type, and spawn age are not on the
|
||||||
|
//! current frame and are therefore not shown — enriching the feed is a separate
|
||||||
|
//! core concern, not a dashboard one.
|
||||||
|
//!
|
||||||
|
//! Two pages share one data model: the fused overview+roster
|
||||||
|
//! (`/view/swactor/actor-overview`) and the per-actor dossier
|
||||||
|
//! (`/view/swactor/actor-dossier`). Each is a self-contained `DashboardView`
|
||||||
|
//! instance ingesting the same channels.
|
||||||
|
|
||||||
|
use std::collections::{BTreeMap, VecDeque};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use datastream::frame::{Frame, StreamId};
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
use crate::swactor::{RUNTIME_ACTORS, RUNTIME_STATS};
|
||||||
|
use crate::view::DashboardView;
|
||||||
|
use crate::{FrameEvent, StreamEvent};
|
||||||
|
|
||||||
|
const CHANNELS: &[&str] = &[RUNTIME_ACTORS, RUNTIME_STATS];
|
||||||
|
const HISTORY_CAP: usize = 512;
|
||||||
|
const HISTORY_MIN_INTERVAL: Duration = Duration::from_millis(250);
|
||||||
|
const PER_ACTOR_HISTORY_CAP: usize = 120;
|
||||||
|
const GROWTH_WINDOW: usize = 12;
|
||||||
|
const LIVE_TTL: Duration = Duration::from_secs(8);
|
||||||
|
|
||||||
|
/// Read-only actor panel. One instance per served page so each page owns its
|
||||||
|
/// state independently; both ingest the same frames.
|
||||||
|
pub struct ActorPanelView {
|
||||||
|
id: &'static str,
|
||||||
|
title: &'static str,
|
||||||
|
path: &'static str,
|
||||||
|
html: &'static str,
|
||||||
|
state: RwLock<PanelState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActorPanelView {
|
||||||
|
/// Fused overview + roster at `/view/swactor/actor-overview`.
|
||||||
|
pub fn overview() -> Self {
|
||||||
|
Self::new(
|
||||||
|
"swactor-actor-overview",
|
||||||
|
"Actor overview",
|
||||||
|
"swactor/actor-overview",
|
||||||
|
include_str!("actor_overview.html"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-actor dossier at `/view/swactor/actor-dossier`.
|
||||||
|
pub fn dossier() -> Self {
|
||||||
|
Self::new(
|
||||||
|
"swactor-actor-dossier",
|
||||||
|
"Actor dossier",
|
||||||
|
"swactor/actor-dossier",
|
||||||
|
include_str!("actor_dossier.html"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(id: &'static str, title: &'static str, path: &'static str, html: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
path,
|
||||||
|
html,
|
||||||
|
state: RwLock::new(PanelState::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct PanelState {
|
||||||
|
runtimes: BTreeMap<String, RuntimeState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RuntimeState {
|
||||||
|
stream: StreamEvent,
|
||||||
|
last_seen: Instant,
|
||||||
|
num_workers: Option<u32>,
|
||||||
|
uptime_ms: Option<u64>,
|
||||||
|
actors: BTreeMap<String, ActorState>,
|
||||||
|
history: VecDeque<HistorySample>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RuntimeState {
|
||||||
|
fn new(stream: StreamEvent, now: Instant) -> Self {
|
||||||
|
Self {
|
||||||
|
stream,
|
||||||
|
last_seen: now,
|
||||||
|
num_workers: None,
|
||||||
|
uptime_ms: None,
|
||||||
|
actors: BTreeMap::new(),
|
||||||
|
history: VecDeque::with_capacity(HISTORY_CAP),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, channel: &str, payload: &[u8], now: Instant) {
|
||||||
|
self.last_seen = now;
|
||||||
|
let Ok(value) = serde_json::from_slice::<Value>(payload) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match channel {
|
||||||
|
RUNTIME_ACTORS => self.apply_actors(&value, now),
|
||||||
|
RUNTIME_STATS => self.apply_stats(&value, now),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
self.push_history(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_actors(&mut self, value: &Value, now: Instant) {
|
||||||
|
// Per-worker `worker_id` wrapper (DatastreamStatsHook shape) is the
|
||||||
|
// default placement for actors that do not carry one inline.
|
||||||
|
let wrapper_worker = u32_field(value, &["worker_id", "worker"]);
|
||||||
|
if let Some(actors) = value.get("actors").and_then(Value::as_array) {
|
||||||
|
for actor in actors {
|
||||||
|
self.apply_actor(actor, now, wrapper_worker);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A bare actor object per frame (no envelope).
|
||||||
|
self.apply_actor(value, now, wrapper_worker);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_actor(&mut self, value: &Value, now: Instant, default_worker: Option<u32>) {
|
||||||
|
let Some(address) = string_field(value, &["address", "addr", "actor_addr"]) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let actor = self
|
||||||
|
.actors
|
||||||
|
.entry(address.clone())
|
||||||
|
.or_insert_with(|| ActorState::new(address));
|
||||||
|
actor.apply_json(value, now, default_worker);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_stats(&mut self, value: &Value, now: Instant) {
|
||||||
|
if let Some(num_workers) = u32_field(value, &["num_workers", "workers_live"]) {
|
||||||
|
self.num_workers = Some(num_workers);
|
||||||
|
}
|
||||||
|
if let Some(uptime_ms) = u64_field(value, &["uptime_ms"]) {
|
||||||
|
self.uptime_ms = Some(uptime_ms);
|
||||||
|
}
|
||||||
|
// address -> worker_id mapping; runtime.stats `actors` is [[addr, wid]].
|
||||||
|
if let Some(actors) = value.get("actors").and_then(Value::as_array) {
|
||||||
|
for entry in actors {
|
||||||
|
if let Some(items) = entry.as_array()
|
||||||
|
&& items.len() >= 2
|
||||||
|
&& let (Some(address), Some(worker_id)) =
|
||||||
|
(value_to_string(&items[0]), value_to_u32(&items[1]))
|
||||||
|
{
|
||||||
|
self.actors
|
||||||
|
.entry(address.clone())
|
||||||
|
.or_insert_with(|| ActorState::new(address))
|
||||||
|
.worker_id = Some(worker_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Some publishers carry full per-actor detail under `actor_details`.
|
||||||
|
if let Some(details) = value.get("actor_details").and_then(Value::as_array) {
|
||||||
|
for actor in details {
|
||||||
|
self.apply_actor(actor, now, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_history(&mut self, now: Instant) {
|
||||||
|
let totals = self.totals();
|
||||||
|
if let Some(last) = self.history.back_mut()
|
||||||
|
&& now.duration_since(last.at) < HISTORY_MIN_INTERVAL
|
||||||
|
{
|
||||||
|
last.mailbox_depth = totals.mailbox_depth;
|
||||||
|
last.msg_per_sec = totals.msg_per_sec;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.history.len() == HISTORY_CAP {
|
||||||
|
self.history.pop_front();
|
||||||
|
}
|
||||||
|
self.history.push_back(HistorySample {
|
||||||
|
at: now,
|
||||||
|
mailbox_depth: totals.mailbox_depth,
|
||||||
|
msg_per_sec: totals.msg_per_sec,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn totals(&self) -> Totals {
|
||||||
|
let mut totals = Totals::default();
|
||||||
|
totals.actors = self.actors.len().min(u32::MAX as usize) as u32;
|
||||||
|
for actor in self.actors.values() {
|
||||||
|
totals.mailbox_depth = totals.mailbox_depth.saturating_add(actor.mailbox_depth);
|
||||||
|
totals.msg_per_sec += actor.msg_per_sec;
|
||||||
|
if actor.poisoned {
|
||||||
|
totals.poisoned = totals.poisoned.saturating_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct ActorState {
|
||||||
|
address: String,
|
||||||
|
name: Option<String>,
|
||||||
|
actor_type: Option<String>,
|
||||||
|
message_type: Option<String>,
|
||||||
|
worker_id: Option<u32>,
|
||||||
|
mailbox_depth: u32,
|
||||||
|
mailbox_growth: f64,
|
||||||
|
messages_processed: u64,
|
||||||
|
msg_per_sec: f64,
|
||||||
|
last_msg_type: Option<String>,
|
||||||
|
poisoned: bool,
|
||||||
|
message_type_counts: Vec<(String, u64)>,
|
||||||
|
history: VecDeque<ActorHistorySample>,
|
||||||
|
last_update: Option<Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActorState {
|
||||||
|
fn new(address: String) -> Self {
|
||||||
|
Self {
|
||||||
|
address,
|
||||||
|
name: None,
|
||||||
|
actor_type: None,
|
||||||
|
message_type: None,
|
||||||
|
worker_id: None,
|
||||||
|
mailbox_depth: 0,
|
||||||
|
mailbox_growth: 0.0,
|
||||||
|
messages_processed: 0,
|
||||||
|
msg_per_sec: 0.0,
|
||||||
|
last_msg_type: None,
|
||||||
|
poisoned: false,
|
||||||
|
message_type_counts: Vec::new(),
|
||||||
|
history: VecDeque::new(),
|
||||||
|
last_update: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_json(&mut self, value: &Value, now: Instant, default_worker: Option<u32>) {
|
||||||
|
let elapsed = self
|
||||||
|
.last_update
|
||||||
|
.map(|then| now.duration_since(then).as_secs_f64())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
if let Some(name) = string_field(value, &["name"]).filter(|name| !name.is_empty()) {
|
||||||
|
self.name = Some(name);
|
||||||
|
}
|
||||||
|
if let Some(actor_type) = string_field(value, &["actor_type"]).filter(|t| !t.is_empty()) {
|
||||||
|
self.actor_type = Some(actor_type);
|
||||||
|
}
|
||||||
|
if let Some(message_type) = string_field(value, &["message_type"]).filter(|t| !t.is_empty()) {
|
||||||
|
self.message_type = Some(message_type);
|
||||||
|
}
|
||||||
|
if let Some(worker_id) = u32_field(value, &["worker_id", "worker"]) {
|
||||||
|
self.worker_id = Some(worker_id);
|
||||||
|
} else if self.worker_id.is_none() {
|
||||||
|
self.worker_id = default_worker;
|
||||||
|
}
|
||||||
|
assign_u32(&mut self.mailbox_depth, value, &["mailbox_depth", "queued"]);
|
||||||
|
assign_u64_rate(
|
||||||
|
&mut self.messages_processed,
|
||||||
|
&mut self.msg_per_sec,
|
||||||
|
value,
|
||||||
|
&["messages_processed", "processed", "messages_handled"],
|
||||||
|
elapsed,
|
||||||
|
);
|
||||||
|
if let Some(last) = string_field(
|
||||||
|
value,
|
||||||
|
&["last_msg_type", "last_message", "last_message_type"],
|
||||||
|
)
|
||||||
|
.filter(|last| !last.is_empty())
|
||||||
|
{
|
||||||
|
self.last_msg_type = Some(last);
|
||||||
|
}
|
||||||
|
if let Some(poisoned) = value.get("poisoned").and_then(Value::as_bool) {
|
||||||
|
self.poisoned = poisoned;
|
||||||
|
}
|
||||||
|
if let Some(counts) = parse_message_type_counts(value.get("message_type_counts")) {
|
||||||
|
self.message_type_counts = counts;
|
||||||
|
}
|
||||||
|
self.last_update = Some(now);
|
||||||
|
self.push_history(now);
|
||||||
|
self.recompute_growth();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_history(&mut self, now: Instant) {
|
||||||
|
if let Some(last) = self.history.back_mut()
|
||||||
|
&& now.duration_since(last.at) < HISTORY_MIN_INTERVAL
|
||||||
|
{
|
||||||
|
last.mailbox_depth = self.mailbox_depth;
|
||||||
|
last.msg_per_sec = self.msg_per_sec;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.history.len() == PER_ACTOR_HISTORY_CAP {
|
||||||
|
self.history.pop_front();
|
||||||
|
}
|
||||||
|
self.history.push_back(ActorHistorySample {
|
||||||
|
at: now,
|
||||||
|
mailbox_depth: self.mailbox_depth,
|
||||||
|
msg_per_sec: self.msg_per_sec,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mailbox depth/s over the recent window, for trend colouring.
|
||||||
|
fn recompute_growth(&mut self) {
|
||||||
|
let n = self.history.len();
|
||||||
|
if n < 2 {
|
||||||
|
self.mailbox_growth = 0.0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let start = n - n.min(GROWTH_WINDOW);
|
||||||
|
let first = &self.history[start];
|
||||||
|
let last = &self.history[n - 1];
|
||||||
|
let span = last.at.duration_since(first.at).as_secs_f64();
|
||||||
|
self.mailbox_growth = if span > 0.0 {
|
||||||
|
(last.mailbox_depth as f64 - first.mailbox_depth as f64) / span
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct HistorySample {
|
||||||
|
at: Instant,
|
||||||
|
mailbox_depth: u32,
|
||||||
|
msg_per_sec: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct ActorHistorySample {
|
||||||
|
at: Instant,
|
||||||
|
mailbox_depth: u32,
|
||||||
|
msg_per_sec: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Totals {
|
||||||
|
actors: u32,
|
||||||
|
mailbox_depth: u32,
|
||||||
|
msg_per_sec: f64,
|
||||||
|
poisoned: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct PanelSnapshot {
|
||||||
|
runtimes: Vec<RuntimeSnapshot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct RuntimeSnapshot {
|
||||||
|
stream: StreamSnapshot,
|
||||||
|
live: bool,
|
||||||
|
last_seen_ms_ago: u64,
|
||||||
|
summary: SummarySnapshot,
|
||||||
|
actors: Vec<ActorSnapshot>,
|
||||||
|
history: Vec<HistorySnapshot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct StreamSnapshot {
|
||||||
|
key: String,
|
||||||
|
node: String,
|
||||||
|
life: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SummarySnapshot {
|
||||||
|
actors: u32,
|
||||||
|
msg_per_sec: f64,
|
||||||
|
mailbox_depth: u32,
|
||||||
|
poisoned: u32,
|
||||||
|
uptime_ms: Option<u64>,
|
||||||
|
num_workers: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ActorSnapshot {
|
||||||
|
address: String,
|
||||||
|
name: Option<String>,
|
||||||
|
actor_type: Option<String>,
|
||||||
|
message_type: Option<String>,
|
||||||
|
worker_id: Option<u32>,
|
||||||
|
/// Single derived display state. The frame carries only `poisoned`, so the
|
||||||
|
/// granularity is poisoned | running until the feed is enriched.
|
||||||
|
state: &'static str,
|
||||||
|
mailbox_depth: u32,
|
||||||
|
mailbox_growth: f64,
|
||||||
|
messages_processed: u64,
|
||||||
|
msg_per_sec: f64,
|
||||||
|
last_msg_type: Option<String>,
|
||||||
|
poisoned: bool,
|
||||||
|
message_type_counts: Vec<MessageTypeCountSnapshot>,
|
||||||
|
history: Vec<ActorHistorySnapshot>,
|
||||||
|
last_seen_ms_ago: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct MessageTypeCountSnapshot {
|
||||||
|
message_type: String,
|
||||||
|
count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct HistorySnapshot {
|
||||||
|
ms_ago: u64,
|
||||||
|
msg_per_sec: f64,
|
||||||
|
mailbox_depth: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ActorHistorySnapshot {
|
||||||
|
ms_ago: u64,
|
||||||
|
mailbox_depth: u32,
|
||||||
|
msg_per_sec: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DashboardView for ActorPanelView {
|
||||||
|
fn id(&self) -> &'static str {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&self) -> &'static str {
|
||||||
|
self.title
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self) -> &'static str {
|
||||||
|
self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn channels(&self) -> &'static [&'static str] {
|
||||||
|
CHANNELS
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ingest(&self, _stream: &StreamId, _frame: &Frame, event: &FrameEvent) {
|
||||||
|
let now = Instant::now();
|
||||||
|
let mut state = self.state.write();
|
||||||
|
let key = stream_key(&event.stream);
|
||||||
|
state
|
||||||
|
.runtimes
|
||||||
|
.entry(key)
|
||||||
|
.or_insert_with(|| RuntimeState::new(event.stream.clone(), now))
|
||||||
|
.update(&event.channel, &event.payload, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot_json(&self) -> Value {
|
||||||
|
let now = Instant::now();
|
||||||
|
let snapshot = PanelSnapshot {
|
||||||
|
runtimes: self
|
||||||
|
.state
|
||||||
|
.read()
|
||||||
|
.runtimes
|
||||||
|
.values()
|
||||||
|
.map(|runtime| runtime_snapshot(runtime, now))
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
serde_json::to_value(snapshot).unwrap_or_else(|_| json!({ "runtimes": [] }))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn html(&self) -> Option<&'static str> {
|
||||||
|
Some(self.html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runtime_snapshot(runtime: &RuntimeState, now: Instant) -> RuntimeSnapshot {
|
||||||
|
let totals = runtime.totals();
|
||||||
|
RuntimeSnapshot {
|
||||||
|
stream: StreamSnapshot {
|
||||||
|
key: stream_key(&runtime.stream),
|
||||||
|
node: runtime.stream.node.clone(),
|
||||||
|
life: runtime.stream.life,
|
||||||
|
},
|
||||||
|
live: now.duration_since(runtime.last_seen) <= LIVE_TTL,
|
||||||
|
last_seen_ms_ago: now.duration_since(runtime.last_seen).as_millis() as u64,
|
||||||
|
summary: SummarySnapshot {
|
||||||
|
actors: totals.actors,
|
||||||
|
msg_per_sec: totals.msg_per_sec,
|
||||||
|
mailbox_depth: totals.mailbox_depth,
|
||||||
|
poisoned: totals.poisoned,
|
||||||
|
uptime_ms: runtime.uptime_ms,
|
||||||
|
num_workers: runtime.num_workers,
|
||||||
|
},
|
||||||
|
actors: runtime
|
||||||
|
.actors
|
||||||
|
.values()
|
||||||
|
.map(|actor| ActorSnapshot {
|
||||||
|
address: actor.address.clone(),
|
||||||
|
name: actor.name.clone(),
|
||||||
|
actor_type: actor.actor_type.clone(),
|
||||||
|
message_type: actor.message_type.clone(),
|
||||||
|
worker_id: actor.worker_id,
|
||||||
|
state: if actor.poisoned { "poisoned" } else { "running" },
|
||||||
|
mailbox_depth: actor.mailbox_depth,
|
||||||
|
mailbox_growth: actor.mailbox_growth,
|
||||||
|
messages_processed: actor.messages_processed,
|
||||||
|
msg_per_sec: actor.msg_per_sec,
|
||||||
|
last_msg_type: actor.last_msg_type.clone(),
|
||||||
|
poisoned: actor.poisoned,
|
||||||
|
message_type_counts: actor
|
||||||
|
.message_type_counts
|
||||||
|
.iter()
|
||||||
|
.map(|(message_type, count)| MessageTypeCountSnapshot {
|
||||||
|
message_type: message_type.clone(),
|
||||||
|
count: *count,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
history: actor
|
||||||
|
.history
|
||||||
|
.iter()
|
||||||
|
.map(|sample| ActorHistorySnapshot {
|
||||||
|
ms_ago: now.duration_since(sample.at).as_millis() as u64,
|
||||||
|
mailbox_depth: sample.mailbox_depth,
|
||||||
|
msg_per_sec: sample.msg_per_sec,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
last_seen_ms_ago: actor
|
||||||
|
.last_update
|
||||||
|
.map(|then| now.duration_since(then).as_millis() as u64)
|
||||||
|
.unwrap_or(0),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
history: runtime
|
||||||
|
.history
|
||||||
|
.iter()
|
||||||
|
.map(|sample| HistorySnapshot {
|
||||||
|
ms_ago: now.duration_since(sample.at).as_millis() as u64,
|
||||||
|
msg_per_sec: sample.msg_per_sec,
|
||||||
|
mailbox_depth: sample.mailbox_depth,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- tolerant JSON helpers (publisher-shape-agnostic readers) ---------------
|
||||||
|
|
||||||
|
fn assign_u32(slot: &mut u32, value: &Value, names: &[&str]) {
|
||||||
|
if let Some(v) = u32_field(value, names) {
|
||||||
|
*slot = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assign_u64_rate(slot: &mut u64, rate: &mut f64, value: &Value, names: &[&str], elapsed: f64) {
|
||||||
|
if let Some(next) = u64_field(value, names) {
|
||||||
|
if elapsed > 0.0 && next > *slot {
|
||||||
|
*rate = (next - *slot) as f64 / elapsed;
|
||||||
|
} else if next < *slot {
|
||||||
|
*rate = 0.0;
|
||||||
|
}
|
||||||
|
*slot = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn u32_field(value: &Value, names: &[&str]) -> Option<u32> {
|
||||||
|
u64_field(value, names).and_then(|v| u32::try_from(v).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn u64_field(value: &Value, names: &[&str]) -> Option<u64> {
|
||||||
|
names
|
||||||
|
.iter()
|
||||||
|
.find_map(|name| value.get(*name).and_then(value_to_u64))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string_field(value: &Value, names: &[&str]) -> Option<String> {
|
||||||
|
names
|
||||||
|
.iter()
|
||||||
|
.find_map(|name| value.get(*name).and_then(value_to_string))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_to_u32(value: &Value) -> Option<u32> {
|
||||||
|
value_to_u64(value).and_then(|v| u32::try_from(v).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_to_u64(value: &Value) -> Option<u64> {
|
||||||
|
value
|
||||||
|
.as_u64()
|
||||||
|
.or_else(|| value.as_str().and_then(|s| s.parse::<u64>().ok()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_to_string(value: &Value) -> Option<String> {
|
||||||
|
value.as_str().map(ToOwned::to_owned).or_else(|| {
|
||||||
|
if value.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(value.to_string())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accepts pair form `["Type", N]`, object form `{"ty": "Type", "count": N}`,
|
||||||
|
/// and map form `{"Type": N}`.
|
||||||
|
fn parse_message_type_counts(value: Option<&Value>) -> Option<Vec<(String, u64)>> {
|
||||||
|
let value = value?;
|
||||||
|
if let Some(items) = value.as_array() {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for item in items {
|
||||||
|
if let Some(pair) = item.as_array()
|
||||||
|
&& pair.len() >= 2
|
||||||
|
&& let (Some(name), Some(count)) =
|
||||||
|
(value_to_string(&pair[0]), value_to_u64(&pair[1]))
|
||||||
|
{
|
||||||
|
out.push((name, count));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(name) = string_field(item, &["ty", "message_type", "type", "name"])
|
||||||
|
&& let Some(count) = u64_field(item, &["count"])
|
||||||
|
{
|
||||||
|
out.push((name, count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Some(out);
|
||||||
|
}
|
||||||
|
if let Some(map) = value.as_object() {
|
||||||
|
let mut out: Vec<(String, u64)> = map
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(name, count)| value_to_u64(count).map(|count| (name.clone(), count)))
|
||||||
|
.collect();
|
||||||
|
out.sort_by(|a, b| b.1.cmp(&a.1));
|
||||||
|
return Some(out);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stream_key(stream: &StreamEvent) -> String {
|
||||||
|
format!("{}#{}", stream.node, stream.life)
|
||||||
|
}
|
||||||
|
|
@ -2,10 +2,12 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use crate::view::DashboardView;
|
use crate::view::DashboardView;
|
||||||
|
|
||||||
|
mod actor_view;
|
||||||
mod worker_page;
|
mod worker_page;
|
||||||
mod worker_view;
|
mod worker_view;
|
||||||
|
|
||||||
pub use worker_view::SwactorWorkerView;
|
pub use worker_view::SwactorWorkerView;
|
||||||
|
pub use actor_view::ActorPanelView;
|
||||||
|
|
||||||
pub const RUNTIME_STATS: &str = "runtime.stats";
|
pub const RUNTIME_STATS: &str = "runtime.stats";
|
||||||
pub const RUNTIME_WORKERS: &str = "runtime.workers";
|
pub const RUNTIME_WORKERS: &str = "runtime.workers";
|
||||||
|
|
@ -14,3 +16,13 @@ pub const RUNTIME_ACTORS: &str = "runtime.actors";
|
||||||
pub fn worker_view() -> Arc<dyn DashboardView> {
|
pub fn worker_view() -> Arc<dyn DashboardView> {
|
||||||
Arc::new(SwactorWorkerView::default())
|
Arc::new(SwactorWorkerView::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Built-in actor overview + roster view (`/view/swactor/actor-overview`).
|
||||||
|
pub fn actor_overview_view() -> Arc<dyn DashboardView> {
|
||||||
|
Arc::new(ActorPanelView::overview())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Built-in actor dossier view (`/view/swactor/actor-dossier`).
|
||||||
|
pub fn actor_dossier_view() -> Arc<dyn DashboardView> {
|
||||||
|
Arc::new(ActorPanelView::dossier())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
name = "data-plane"
|
name = "data-plane"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
||||||
|
|
@ -1,523 +0,0 @@
|
||||||
# Data Plane Actor Architecture Specification
|
|
||||||
|
|
||||||
**Status:** implemented architecture contract for the `data-plane` crate.
|
|
||||||
|
|
||||||
This document describes the behavioral boundary between reusable data-plane actors
|
|
||||||
and MVP-specific orchestration/runtime code. It is intentionally architectural: it
|
|
||||||
names responsibilities, actor roles, message families, and ownership boundaries
|
|
||||||
without prescribing file layout or migration steps.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Purpose
|
|
||||||
|
|
||||||
`data-plane` owns the behavior required to move model objects between stages and
|
|
||||||
between a node process and its local GPU worker process.
|
|
||||||
|
|
||||||
The crate defines the actor protocol for:
|
|
||||||
|
|
||||||
- provisioning logical data edges;
|
|
||||||
- distinguishing network/wire edges from node-local IPC rings;
|
|
||||||
- leasing, installing, readying, faulting, stopping, quiescing, and releasing
|
|
||||||
local rings;
|
|
||||||
- binding logical edges to transport endpoints and local worker rings;
|
|
||||||
- parsing, validating, sequencing, loading, producing, and reporting objects;
|
|
||||||
- gating readiness and object visibility on the correct lifecycle transitions;
|
|
||||||
- translating low-level arena, transport, and worker observations into coarse
|
|
||||||
data-plane outcomes.
|
|
||||||
|
|
||||||
`mvp-system` uses `data-plane` as a reusable actor subsystem. It provides run
|
|
||||||
intent, concrete runtime actor addresses, and MVP-specific report sinks. It does
|
|
||||||
not own the fine-grained data-plane state machine.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Core boundary
|
|
||||||
|
|
||||||
Swactor actors carry control, lifecycle, and identity messages. Payload bytes do
|
|
||||||
not move through actor mailboxes.
|
|
||||||
|
|
||||||
Payload bytes move through:
|
|
||||||
|
|
||||||
- arena-backed shared-memory rings for node-local IPC;
|
|
||||||
- transport byte streams for node-to-node data edges;
|
|
||||||
- GPU-worker-owned device allocations for compute-ready objects.
|
|
||||||
|
|
||||||
The data-plane actors decide when these byte paths are established, readable,
|
|
||||||
writable, faulted, stopped, and safe to release. Runtime adapter actors execute
|
|
||||||
concrete effects and report observations back.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Edge and ring model
|
|
||||||
|
|
||||||
### 3.1 Wire edge
|
|
||||||
|
|
||||||
A wire edge is a logical run-plan connection between a producer endpoint and a
|
|
||||||
consumer endpoint.
|
|
||||||
|
|
||||||
It carries:
|
|
||||||
|
|
||||||
- run identity;
|
|
||||||
- edge identity;
|
|
||||||
- producer and consumer node identity;
|
|
||||||
- edge kind, such as token input, activation, or token output;
|
|
||||||
- object contract;
|
|
||||||
- transport contract;
|
|
||||||
- optional remote endpoint and remote actor identity.
|
|
||||||
|
|
||||||
A wire edge answers: "which logical data stream connects these stage endpoints?"
|
|
||||||
|
|
||||||
It does not answer: "which local arena offset or worker-process ring is being
|
|
||||||
used on this node?"
|
|
||||||
|
|
||||||
### 3.2 Local IPC ring
|
|
||||||
|
|
||||||
A local IPC ring is a node-local buffer used by a Rust node process and its owned
|
|
||||||
GPU worker process.
|
|
||||||
|
|
||||||
It carries:
|
|
||||||
|
|
||||||
- ring identity allocated by the local arena manager;
|
|
||||||
- arena layout and capacity;
|
|
||||||
- worker-process generation;
|
|
||||||
- local role port, such as input or output;
|
|
||||||
- direction relative to the worker process;
|
|
||||||
- object contract installed into the worker;
|
|
||||||
- quiescence and release state.
|
|
||||||
|
|
||||||
A local IPC ring answers: "how does this node exchange bytes with its local
|
|
||||||
worker process for a specific edge endpoint?"
|
|
||||||
|
|
||||||
It does not answer: "which remote node or distributed route owns the other side
|
|
||||||
of the logical edge?"
|
|
||||||
|
|
||||||
### 3.3 Binding
|
|
||||||
|
|
||||||
A wire edge endpoint may bind to zero or more local resources depending on its
|
|
||||||
role:
|
|
||||||
|
|
||||||
```text
|
|
||||||
inbound wire edge endpoint
|
|
||||||
-> recv transport endpoint
|
|
||||||
-> local worker ingress ring
|
|
||||||
-> device object handle
|
|
||||||
|
|
||||||
outbound wire edge endpoint
|
|
||||||
-> local worker egress ring
|
|
||||||
-> send transport endpoint
|
|
||||||
|
|
||||||
local-only edge endpoint
|
|
||||||
-> local producer/consumer binding
|
|
||||||
-> optional worker ring
|
|
||||||
```
|
|
||||||
|
|
||||||
The binding is owned by data-plane state. MVP code may observe the binding only
|
|
||||||
through coarse reports such as edge ready, object loaded, object produced, edge
|
|
||||||
faulted, and edge stopped.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Actor topology
|
|
||||||
|
|
||||||
The target topology is actor-oriented.
|
|
||||||
|
|
||||||
### 4.1 Data-plane node actor
|
|
||||||
|
|
||||||
One data-plane node actor owns the data-plane state for one local node within one
|
|
||||||
active run.
|
|
||||||
|
|
||||||
It owns:
|
|
||||||
|
|
||||||
- local node identity;
|
|
||||||
- run-scoped edge table;
|
|
||||||
- mapping from wire edge endpoints to local rings;
|
|
||||||
- mapping from ring ids to edge endpoints;
|
|
||||||
- object sequence state;
|
|
||||||
- device-handle visibility state;
|
|
||||||
- data-plane child actor addresses;
|
|
||||||
- MVP report sink addresses.
|
|
||||||
|
|
||||||
It receives provisioning intent from MVP code and observations from runtime
|
|
||||||
adapters. It emits actor messages to arena, worker, transport, and MVP report
|
|
||||||
sinks.
|
|
||||||
|
|
||||||
### 4.2 Wire edge actor
|
|
||||||
|
|
||||||
A wire edge actor owns the lifecycle of one logical edge endpoint on the local
|
|
||||||
node.
|
|
||||||
|
|
||||||
It owns:
|
|
||||||
|
|
||||||
- provisioning state;
|
|
||||||
- transport establishment state;
|
|
||||||
- send/receive pump readiness;
|
|
||||||
- stream faults;
|
|
||||||
- logical edge readiness;
|
|
||||||
- stop and fault propagation for that edge endpoint.
|
|
||||||
|
|
||||||
It does not own worker-process state or arena layout details except through a
|
|
||||||
binding supplied by the data-plane node actor or ring actor.
|
|
||||||
|
|
||||||
### 4.3 Local worker ring actor
|
|
||||||
|
|
||||||
A local worker ring actor owns one local IPC ring lifecycle.
|
|
||||||
|
|
||||||
It owns:
|
|
||||||
|
|
||||||
- arena lease request and result;
|
|
||||||
- worker ring installation;
|
|
||||||
- worker ring readable/writable notifications;
|
|
||||||
- ring fault and quiescence observations;
|
|
||||||
- release proof collection;
|
|
||||||
- arena lease release.
|
|
||||||
|
|
||||||
It does not own remote endpoint routing. It can be bound to a wire edge endpoint
|
|
||||||
by edge id, but the ring lifecycle remains local.
|
|
||||||
|
|
||||||
### 4.4 GPU worker control adapter
|
|
||||||
|
|
||||||
The GPU worker control adapter is the actor-facing boundary to the owned Python
|
|
||||||
worker process.
|
|
||||||
|
|
||||||
It owns or fronts:
|
|
||||||
|
|
||||||
- worker process generation;
|
|
||||||
- command serialization to the worker;
|
|
||||||
- stdout/stderr event parsing;
|
|
||||||
- device handle generation checks;
|
|
||||||
- worker stop/crash/restart observations.
|
|
||||||
|
|
||||||
The data plane treats this as an actor endpoint. Worker-process JSON and Python
|
|
||||||
helper details are not exposed to MVP stage logic.
|
|
||||||
|
|
||||||
### 4.5 Transport adapter actors
|
|
||||||
|
|
||||||
Transport adapter actors own concrete wire byte movement.
|
|
||||||
|
|
||||||
They own or front:
|
|
||||||
|
|
||||||
- accepted edge streams;
|
|
||||||
- outbound edge streams;
|
|
||||||
- edge preamble validation;
|
|
||||||
- byte read/write readiness;
|
|
||||||
- transport-specific stream faults;
|
|
||||||
- pump stop observations.
|
|
||||||
|
|
||||||
The data plane treats transport events as observations on a wire edge. Transport
|
|
||||||
actors do not decide stage readiness or object admission.
|
|
||||||
|
|
||||||
### 4.6 MVP report sink
|
|
||||||
|
|
||||||
The MVP report sink receives coarse data-plane outcomes and maps them to
|
|
||||||
MVP-specific control messages.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- inbound edge ready;
|
|
||||||
- outbound edge ready;
|
|
||||||
- object loaded for stage execution;
|
|
||||||
- object produced for downstream transport;
|
|
||||||
- edge faulted;
|
|
||||||
- local edges stopped.
|
|
||||||
|
|
||||||
The sink does not inspect ring cursors, arena leases, worker generations, or
|
|
||||||
transport pump internals.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Actor API surface
|
|
||||||
|
|
||||||
Concrete Rust names are schematic. The contract is the message shape and
|
|
||||||
ownership boundary.
|
|
||||||
|
|
||||||
### 5.1 Provisioning input
|
|
||||||
|
|
||||||
MVP sends provisioning intent to the data-plane node actor:
|
|
||||||
|
|
||||||
```text
|
|
||||||
ProvisionDataPlaneRun {
|
|
||||||
run_id,
|
|
||||||
local_node_id,
|
|
||||||
arena_actor,
|
|
||||||
worker_actor,
|
|
||||||
transport_actor,
|
|
||||||
report_sink,
|
|
||||||
}
|
|
||||||
|
|
||||||
ProvisionWireEdgeEndpoint {
|
|
||||||
run_id,
|
|
||||||
edge_id,
|
|
||||||
direction,
|
|
||||||
edge_kind,
|
|
||||||
local_role_port,
|
|
||||||
local_node_id,
|
|
||||||
peer_node_id,
|
|
||||||
peer_endpoint,
|
|
||||||
object_spec,
|
|
||||||
transport_spec,
|
|
||||||
local_ring_spec,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`direction` is relative to the local node's stage role: inbound means the local
|
|
||||||
stage consumes objects from the edge; outbound means the local stage produces
|
|
||||||
objects to the edge.
|
|
||||||
|
|
||||||
Provisioning is declarative. MVP describes the intended edge endpoint and the
|
|
||||||
actors available to execute effects. It does not prescribe lease/install/driver
|
|
||||||
ordering.
|
|
||||||
|
|
||||||
### 5.2 Runtime observations
|
|
||||||
|
|
||||||
Runtime adapters report observations back to data-plane actors:
|
|
||||||
|
|
||||||
```text
|
|
||||||
ArenaRingLeased
|
|
||||||
ArenaRingLeaseRejected
|
|
||||||
ArenaRingReleased
|
|
||||||
ArenaRingReleaseRejected
|
|
||||||
|
|
||||||
WorkerReady
|
|
||||||
WorkerRingInstalled
|
|
||||||
WorkerRingFaulted
|
|
||||||
WorkerRingQuiesced
|
|
||||||
WorkerRingReadable
|
|
||||||
WorkerRingWritable
|
|
||||||
WorkerObjectLoaded
|
|
||||||
WorkerObjectProduced
|
|
||||||
WorkerObjectFailed
|
|
||||||
WorkerStopped
|
|
||||||
WorkerFaulted
|
|
||||||
|
|
||||||
TransportEdgeReady
|
|
||||||
TransportBytesReceived
|
|
||||||
TransportBytesSent
|
|
||||||
TransportStreamClosed
|
|
||||||
TransportStreamFaulted
|
|
||||||
TransportPumpStopped
|
|
||||||
```
|
|
||||||
|
|
||||||
Observations are facts, not commands. The data plane decides the next state and
|
|
||||||
any follow-up messages.
|
|
||||||
|
|
||||||
### 5.3 Data-plane effects
|
|
||||||
|
|
||||||
The data plane sends effect requests to runtime adapter actors:
|
|
||||||
|
|
||||||
```text
|
|
||||||
LeaseArenaRing
|
|
||||||
CancelArenaRingLease
|
|
||||||
ReleaseArenaRingLease
|
|
||||||
|
|
||||||
InstallWorkerRing
|
|
||||||
UninstallWorkerRing
|
|
||||||
NotifyWorkerRingReadable
|
|
||||||
NotifyWorkerRingWritable
|
|
||||||
LoadObjectFromWorkerRing
|
|
||||||
ExecuteWorkerStep
|
|
||||||
ReleaseWorkerDeviceObject
|
|
||||||
|
|
||||||
EstablishWireSend
|
|
||||||
EstablishWireRecv
|
|
||||||
WriteWireObject
|
|
||||||
StopWirePump
|
|
||||||
```
|
|
||||||
|
|
||||||
Effects are actor messages. The receiving adapter owns the concrete mechanism:
|
|
||||||
memfd/mmap, JSON stdin/stdout, process supervision, iroh streams, or test doubles.
|
|
||||||
|
|
||||||
### 5.4 Data-plane reports
|
|
||||||
|
|
||||||
The data plane reports only stable semantic outcomes to MVP:
|
|
||||||
|
|
||||||
```text
|
|
||||||
InboundEdgeReady { edge_id }
|
|
||||||
OutboundEdgeReady { edge_id }
|
|
||||||
ObjectLoaded { edge_id, object_id, sequence, device_handle }
|
|
||||||
ObjectProduced { edge_id, object_id, sequence, extent }
|
|
||||||
EdgeFaulted { edge_id, reason }
|
|
||||||
EdgeStopped { edge_id }
|
|
||||||
LocalEdgesStopped { run_id }
|
|
||||||
WorkerDataPlaneFaulted { reason }
|
|
||||||
```
|
|
||||||
|
|
||||||
Reports are the only data-plane messages MVP stage/orchestrator actors should
|
|
||||||
need for normal stage progression.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Behavior owned by data-plane
|
|
||||||
|
|
||||||
### 6.1 Edge establishment
|
|
||||||
|
|
||||||
For each provisioned edge endpoint, data-plane actors own the establishment
|
|
||||||
sequence.
|
|
||||||
|
|
||||||
Inbound endpoint:
|
|
||||||
|
|
||||||
```text
|
|
||||||
provision endpoint
|
|
||||||
-> lease local ingress ring
|
|
||||||
-> install ring into worker input port
|
|
||||||
-> establish receive transport if the edge is remote
|
|
||||||
-> report inbound edge ready
|
|
||||||
```
|
|
||||||
|
|
||||||
Outbound endpoint:
|
|
||||||
|
|
||||||
```text
|
|
||||||
provision endpoint
|
|
||||||
-> lease local egress ring when worker output is required
|
|
||||||
-> install ring into worker output port
|
|
||||||
-> establish send transport if the edge is remote
|
|
||||||
-> report outbound edge ready
|
|
||||||
```
|
|
||||||
|
|
||||||
Readiness is reported only after every required local and wire resource for that
|
|
||||||
endpoint is ready. A local-only endpoint may omit transport establishment. A
|
|
||||||
wire-only endpoint may omit worker-ring establishment when it terminates outside
|
|
||||||
the local GPU worker.
|
|
||||||
|
|
||||||
### 6.2 Object ingress
|
|
||||||
|
|
||||||
For inbound data, data-plane actors own object admission.
|
|
||||||
|
|
||||||
The data plane:
|
|
||||||
|
|
||||||
- associates incoming bytes with the correct wire edge and stream;
|
|
||||||
- validates object framing and object spec constraints;
|
|
||||||
- preserves sequence ordering required by the edge contract;
|
|
||||||
- writes or exposes the object through the local ingress ring;
|
|
||||||
- asks the worker to load the object to device;
|
|
||||||
- waits for a valid worker object-loaded observation;
|
|
||||||
- reports object loaded to MVP only after the device handle is current and the
|
|
||||||
logical object is complete.
|
|
||||||
|
|
||||||
MVP does not parse object headers, track ingress buffers, reload cursors, or gate
|
|
||||||
object-loaded visibility.
|
|
||||||
|
|
||||||
### 6.3 Object egress
|
|
||||||
|
|
||||||
For outbound data, data-plane actors own object production and forwarding.
|
|
||||||
|
|
||||||
The data plane:
|
|
||||||
|
|
||||||
- receives compute/output observations from the worker;
|
|
||||||
- binds produced objects to the correct outbound edge and sequence;
|
|
||||||
- validates object extent and object contract;
|
|
||||||
- publishes readable/writable state to the worker and transport actors;
|
|
||||||
- forwards complete object records on the wire when the edge is remote;
|
|
||||||
- reports object produced or step-visible outcomes to MVP at semantic
|
|
||||||
boundaries, not cursor boundaries.
|
|
||||||
|
|
||||||
MVP does not decide when a local output ring is readable, when a transport stream
|
|
||||||
should consume it, or when a produced object is safe to expose downstream.
|
|
||||||
|
|
||||||
### 6.4 Faults
|
|
||||||
|
|
||||||
The data plane owns data movement fault classification and propagation.
|
|
||||||
|
|
||||||
Fault sources include:
|
|
||||||
|
|
||||||
- arena lease rejection or release rejection;
|
|
||||||
- worker ring installation failure;
|
|
||||||
- worker ring fault;
|
|
||||||
- malformed object framing;
|
|
||||||
- sequence violation;
|
|
||||||
- worker object load/produce failure;
|
|
||||||
- transport stream read/write/protocol failure;
|
|
||||||
- pump stop before quiescence;
|
|
||||||
- stale worker generation or device handle.
|
|
||||||
|
|
||||||
A fault on one edge endpoint must not silently corrupt another endpoint. The data
|
|
||||||
plane maps local faults to edge-scoped or worker-scoped reports, starts the
|
|
||||||
required stop/quiescence path, and emits the appropriate MVP report.
|
|
||||||
|
|
||||||
### 6.5 Stop, quiescence, and release
|
|
||||||
|
|
||||||
The data plane owns teardown ordering for local resources.
|
|
||||||
|
|
||||||
For a bound edge/ring pair, stop requires:
|
|
||||||
|
|
||||||
```text
|
|
||||||
stop transport pump if present
|
|
||||||
-> uninstall or quiesce worker ring if installed
|
|
||||||
-> prove no local reader/writer still uses the ring
|
|
||||||
-> release arena lease
|
|
||||||
-> report edge stopped
|
|
||||||
```
|
|
||||||
|
|
||||||
Arena release must be gated by quiescence proof. MVP may request run or edge
|
|
||||||
stop, but it does not supply low-level release proof or decide when a ring is
|
|
||||||
safe to release.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. MVP-system utilization
|
|
||||||
|
|
||||||
`mvp-system` remains responsible for MVP orchestration and stage semantics.
|
|
||||||
|
|
||||||
It owns:
|
|
||||||
|
|
||||||
- run planning and edge assignment;
|
|
||||||
- stage provisioning authority;
|
|
||||||
- membership/readiness gates outside the data plane;
|
|
||||||
- weight loading and role configuration intent;
|
|
||||||
- stage controller behavior;
|
|
||||||
- prompt injection and token consumption;
|
|
||||||
- mapping data-plane reports to MVP lifecycle messages;
|
|
||||||
- selecting concrete runtime adapters for arena, worker process, and transport.
|
|
||||||
|
|
||||||
For data movement, MVP code acts as a client:
|
|
||||||
|
|
||||||
1. Spawn or obtain actor addresses for the data-plane node actor and required
|
|
||||||
runtime adapters.
|
|
||||||
2. Send run and edge provisioning intent to the data-plane node actor.
|
|
||||||
3. Forward runtime observations from concrete adapter actors when those adapters
|
|
||||||
are MVP-owned.
|
|
||||||
4. Receive coarse data-plane reports.
|
|
||||||
5. Translate those reports into stage-controller or orchestrator messages.
|
|
||||||
|
|
||||||
MVP must not rely on private edge states such as waiting-for-lease,
|
|
||||||
waiting-for-worker-ring, waiting-for-driver, pump-stopped, ring-quiesced, or
|
|
||||||
release-ready. Those are data-plane implementation states.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Required invariants
|
|
||||||
|
|
||||||
- `EdgeId` names a logical run-plan edge, not a local ring allocation.
|
|
||||||
- `RingId` names a local arena-backed IPC ring, not a distributed edge.
|
|
||||||
- A ring may be bound to an edge endpoint, but the identifiers are not
|
|
||||||
interchangeable.
|
|
||||||
- Actor messages carry control and identities, not tensor payload bytes.
|
|
||||||
- Object-loaded reports are emitted only for complete, validated objects with
|
|
||||||
current-generation device handles.
|
|
||||||
- Edge-ready reports are emitted only after required wire and local IPC resources
|
|
||||||
are ready.
|
|
||||||
- Arena lease release is gated by local quiescence proof.
|
|
||||||
- Worker process generation is part of every device-handle validity decision.
|
|
||||||
- Transport faults and worker faults are classified by data-plane before they
|
|
||||||
become MVP reports.
|
|
||||||
- MVP stage logic observes semantic outcomes, never ring cursor mechanics.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Non-goals
|
|
||||||
|
|
||||||
`data-plane` does not own:
|
|
||||||
|
|
||||||
- global run planning;
|
|
||||||
- placement optimization;
|
|
||||||
- model layer assignment;
|
|
||||||
- weight download or weight loading semantics;
|
|
||||||
- prompt tokenization or output token policy;
|
|
||||||
- membership convergence;
|
|
||||||
- provider provisioning;
|
|
||||||
- concrete iroh endpoint construction;
|
|
||||||
- concrete Python helper implementation.
|
|
||||||
|
|
||||||
The crate defines reusable actor protocols and data-movement behavior. Concrete
|
|
||||||
runtime adapters may live beside MVP code, inside reusable support crates, or in
|
|
||||||
tests, as long as they satisfy the actor contracts above.
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
name = "datastream"
|
name = "datastream"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
swactor = { path = "../..", features = ["serde", "transport"] }
|
swactor = { path = "../..", features = ["serde", "transport"] }
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
# The Datastream — Specification
|
# The Datastream — Specification
|
||||||
|
|
||||||
> **Status.** Current-state specification for the `datastream` crate. This keeps
|
Id: 7
|
||||||
> the original outline, but updates the model to the implementation that now
|
Last modified:
|
||||||
> exists: stream-local numeric channel ids, a channel catalog, broadcast endpoint
|
Last reviewed:
|
||||||
> subscriptions, and catalog-aware transport events.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -758,9 +758,11 @@ impl<'a> RuntimeActorStatsRecord<'a> {
|
||||||
actors: snapshots
|
actors: snapshots
|
||||||
.iter()
|
.iter()
|
||||||
.map(|snapshot| RuntimeActorSnapshotRecord {
|
.map(|snapshot| RuntimeActorSnapshotRecord {
|
||||||
address: snapshot.address.to_string(),
|
address: snapshot.address.to_full_hex(),
|
||||||
mailbox_depth: snapshot.mailbox_depth,
|
mailbox_depth: snapshot.mailbox_depth,
|
||||||
last_msg_type: snapshot.last_msg_type,
|
last_msg_type: snapshot.last_msg_type,
|
||||||
|
actor_type: snapshot.actor_type,
|
||||||
|
message_type: snapshot.message_type,
|
||||||
messages_processed: snapshot.messages_processed,
|
messages_processed: snapshot.messages_processed,
|
||||||
poisoned: snapshot.poisoned,
|
poisoned: snapshot.poisoned,
|
||||||
message_type_counts: snapshot
|
message_type_counts: snapshot
|
||||||
|
|
@ -779,6 +781,8 @@ struct RuntimeActorSnapshotRecord<'a> {
|
||||||
address: String,
|
address: String,
|
||||||
mailbox_depth: usize,
|
mailbox_depth: usize,
|
||||||
last_msg_type: Option<&'static str>,
|
last_msg_type: Option<&'static str>,
|
||||||
|
actor_type: Option<&'static str>,
|
||||||
|
message_type: Option<&'static str>,
|
||||||
messages_processed: u64,
|
messages_processed: u64,
|
||||||
poisoned: bool,
|
poisoned: bool,
|
||||||
message_type_counts: Vec<RuntimeMessageTypeCount<'a>>,
|
message_type_counts: Vec<RuntimeMessageTypeCount<'a>>,
|
||||||
|
|
|
||||||
|
|
@ -300,6 +300,8 @@ fn stats_hook_adapter_submits_worker_snapshot_json() {
|
||||||
address: actor,
|
address: actor,
|
||||||
mailbox_depth: 3,
|
mailbox_depth: 3,
|
||||||
last_msg_type: Some("Ping"),
|
last_msg_type: Some("Ping"),
|
||||||
|
actor_type: Some("TestActor"),
|
||||||
|
message_type: Some("Ping"),
|
||||||
messages_processed: 5,
|
messages_processed: 5,
|
||||||
poisoned: false,
|
poisoned: false,
|
||||||
message_type_counts: vec![("Ping", 5)],
|
message_type_counts: vec![("Ping", 5)],
|
||||||
|
|
@ -313,11 +315,13 @@ fn stats_hook_adapter_submits_worker_snapshot_json() {
|
||||||
assert_eq!(delivery.channel.channel, runtime);
|
assert_eq!(delivery.channel.channel, runtime);
|
||||||
let json: Value = serde_json::from_slice(&delivery.payload).unwrap();
|
let json: Value = serde_json::from_slice(&delivery.payload).unwrap();
|
||||||
assert_eq!(json["worker_id"], 2);
|
assert_eq!(json["worker_id"], 2);
|
||||||
assert_eq!(json["actors"][0]["address"], actor.to_string());
|
assert_eq!(json["actors"][0]["address"], actor.to_full_hex());
|
||||||
assert_eq!(json["actors"][0]["mailbox_depth"], 3);
|
assert_eq!(json["actors"][0]["mailbox_depth"], 3);
|
||||||
assert_eq!(json["actors"][0]["last_msg_type"], "Ping");
|
assert_eq!(json["actors"][0]["last_msg_type"], "Ping");
|
||||||
assert_eq!(json["actors"][0]["messages_processed"], 5);
|
assert_eq!(json["actors"][0]["messages_processed"], 5);
|
||||||
assert_eq!(json["actors"][0]["message_type_counts"][0]["ty"], "Ping");
|
assert_eq!(json["actors"][0]["message_type_counts"][0]["ty"], "Ping");
|
||||||
|
assert_eq!(json["actors"][0]["actor_type"], "TestActor");
|
||||||
|
assert_eq!(json["actors"][0]["message_type"], "Ping");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn positions(events: &[DatastreamEvent]) -> Vec<u64> {
|
fn positions(events: &[DatastreamEvent]) -> Vec<u64> {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
name = "distribution"
|
name = "distribution"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
name = "iroh-driver"
|
name = "iroh-driver"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "iroh_driver"
|
name = "iroh_driver"
|
||||||
|
|
@ -19,9 +20,8 @@ iroh = "0.98"
|
||||||
# `test-utils` exposes `CaRootsConfig::insecure_skip_verify()` so clients can
|
# `test-utils` exposes `CaRootsConfig::insecure_skip_verify()` so clients can
|
||||||
# trust operator-controlled custom relays with self-signed QAD certs.
|
# trust operator-controlled custom relays with self-signed QAD certs.
|
||||||
iroh-relay = { version = "0.98", features = ["test-utils"] }
|
iroh-relay = { version = "0.98", features = ["test-utils"] }
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "time"] }
|
tokio.workspace = true
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
iroh-relay = { version = "0.98", features = ["server", "test-utils"] }
|
iroh-relay = { version = "0.98", features = ["server", "test-utils"] }
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "time"] }
|
|
||||||
|
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "mvp-system"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
publish = false
|
|
||||||
autobins = false
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
dashboard = []
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
datastream = { path = "../datastream" }
|
|
||||||
data-plane = { path = "../data-plane" }
|
|
||||||
provisioning = { path = "../provisioning" }
|
|
||||||
dashboard = { path = "../dashboard" }
|
|
||||||
serde_json = "1"
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
swactor = { path = "../..", features = ["serde", "transport"] }
|
|
||||||
swactor-transport = { path = "../transport" }
|
|
||||||
swactor-process = { path = "../process" }
|
|
||||||
distribution = { path = "../distribution" }
|
|
||||||
iroh-driver = { path = "../iroh-driver" }
|
|
||||||
iroh = "0.98"
|
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "io-util", "sync", "time", "net", "signal"] }
|
|
||||||
swactor-vastai = { path = "../../tools/vastai" }
|
|
||||||
parking_lot = "0.12"
|
|
||||||
blake3 = "1"
|
|
||||||
toml = "0.8"
|
|
||||||
ureq = "2"
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
|
||||||
libc = "0.2"
|
|
||||||
signal-hook = "0.3"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "mvp-worker-node"
|
|
||||||
path = "src/bin/worker_node.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "mvp-orchestrator"
|
|
||||||
path = "src/bin/orchestrator.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "mvp-chat"
|
|
||||||
path = "src/bin/mvp_chat.rs"
|
|
||||||
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
fn main() -> std::process::ExitCode {
|
|
||||||
mvp_system::run_chat_from_args(std::env::args().skip(1))
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
//! MVP operator chat wrapper public surface.
|
|
||||||
|
|
||||||
mod node_image;
|
|
||||||
pub(super) mod runtime;
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
//! MVP prompt protocol public surface.
|
|
||||||
|
|
||||||
pub(crate) mod rpc;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue