feat: mvp-chat benchmarking

This commit is contained in:
Zachery Aaron Shores-Chmielewski 2026-07-23 09:35:04 +04:00
parent 9e0a26d99d
commit 92643bf399
8 changed files with 2095 additions and 506 deletions

View file

@ -29,6 +29,9 @@ device_objects: dict[int, dict[str, Any]] = {}
next_handle = 42 next_handle = 42
HEADER_LEN = 40 HEADER_LEN = 40
WORKER_GENERATION = 1 WORKER_GENERATION = 1
BENCHMARK_SCHEMA = 1
_benchmark_start = time.monotonic()
_benchmark_seq = 0
class CpuLineSampler: class CpuLineSampler:
@ -142,9 +145,37 @@ def env_flag(name: str, default: bool = True) -> bool:
return raw.strip().lower() not in {"0", "false", "no", "off"} return raw.strip().lower() not in {"0", "false", "no", "off"}
def benchmark_stamp() -> dict[str, Any]:
global _benchmark_seq
_benchmark_seq += 1
return {
"schema": BENCHMARK_SCHEMA,
"component": "tinygrad-worker",
"pid": os.getpid(),
"seq": _benchmark_seq,
"wall_unix_ms": time.time_ns() // 1_000_000,
"mono_ms": int((time.monotonic() - _benchmark_start) * 1000),
}
def env_int(name: str) -> int | None:
raw = os.environ.get(name)
if raw is None:
return None
try:
return int(raw)
except ValueError:
return None
def control(**event: Any) -> None: def control(**event: Any) -> None:
event.setdefault("benchmark", benchmark_stamp())
if (run_id := env_int("MVP_RUN_ID")) is not None:
event.setdefault("run_id", run_id)
if (node_id := env_int("MVP_LOGICAL_NODE_ID")) is not None:
event.setdefault("node_id", node_id)
if (stage_index := env_int("MVP_STAGE_INDEX")) is not None:
event.setdefault("stage_index", stage_index)
print(json.dumps(event, separators=(",", ":")), flush=True) print(json.dumps(event, separators=(",", ":")), flush=True)
@ -157,8 +188,6 @@ def fatal(reason: str, **fields: Any) -> None:
raise SystemExit(1) raise SystemExit(1)
def test_mode() -> bool:
return os.environ.get("MVP_TINYGRAD_TEST_MODE", "").strip().lower() in {"1", "true", "yes", "on"}
def configure_tinygrad_cuda_compiler(device: str) -> None: def configure_tinygrad_cuda_compiler(device: str) -> None:
if device.split(":", 1)[0].upper() != "CUDA": if device.split(":", 1)[0].upper() != "CUDA":
@ -170,31 +199,31 @@ def configure_tinygrad_cuda_compiler(device: str) -> None:
os.environ["CUDA_PTX"] = "1" os.environ["CUDA_PTX"] = "1"
control(type="TinygradCudaCompilerSelected", requested_device=device, compiler="PTX", reason="nvcc_not_found") control(type="TinygradCudaCompilerSelected", requested_device=device, compiler="PTX", reason="nvcc_not_found")
def select_tinygrad_device(device: str) -> str:
device_kind = device.split(":", 1)[0].upper()
if device_kind == "CPU" and ":" not in device and shutil.which("clang") is None:
selected = "CPU:X86"
os.environ["DEV"] = selected
control(type="TinygradCpuCompilerSelected", requested_device=device, selected_device=selected, compiler="X86", reason="clang_not_found")
return selected
os.environ["DEV"] = device
configure_tinygrad_cuda_compiler(device)
return device
def initialize(cmd: dict[str, Any]) -> None: def initialize(cmd: dict[str, Any]) -> None:
global Tensor, dtypes, arena global Tensor, dtypes, arena
if int(cmd.get("helper_abi_version", 1)) != 1: if int(cmd.get("helper_abi_version", 1)) != 1:
fatal("UnsupportedHelperAbi", helper_abi_version=cmd.get("helper_abi_version")) fatal("UnsupportedHelperAbi", helper_abi_version=cmd.get("helper_abi_version"))
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")
os.environ["DEV"] = device device = select_tinygrad_device(requested_device)
arena_fd = os.environ.get("MVP_ARENA_FD") arena_fd = os.environ.get("MVP_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("MVP_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()
if test_mode():
control(
type="WorkerReady",
pid=os.getpid(),
backend={"requested_device": device, "env_DEV": os.environ.get("DEV"), "test_mode": True},
cuda_probe=[],
test_mode=True,
elapsed_ms=int((time.monotonic() - started) * 1000),
)
return
configure_tinygrad_cuda_compiler(device)
control(type="TinygradImportStarted", requested_device=device, env_DEV=os.environ.get("DEV")) control(type="TinygradImportStarted", requested_device=device, env_DEV=os.environ.get("DEV"))
from tinygrad import Tensor as TinyTensor, dtypes as tiny_dtypes from tinygrad import Tensor as TinyTensor, dtypes as tiny_dtypes
@ -353,31 +382,42 @@ class PipelineStageTinygradModel:
vocab_size: int, vocab_size: int,
head_dim: int, head_dim: int,
rope_theta: float, rope_theta: float,
rope_dim: int,
v_head_dim: int,
max_context: int, max_context: int,
qk_norm: int, qk_norm: int,
num_experts: int, num_experts: int,
num_experts_per_tok: int, num_experts_per_tok: int,
norm_topk_prob: bool,
qkv_bias: bool,
expert_bias: bool,
first_stage: bool, first_stage: bool,
final_stage: bool, final_stage: bool,
nn_mod: Any, nn_mod: Any,
config_cls: Any,
block_cls: Any, block_cls: Any,
) -> None: ) -> None:
self.blk = [ block_config = config_cls(
block_cls( num_blocks=block_count,
dim, dim=dim,
hidden_dim, hidden_dim=hidden_dim,
n_heads, n_heads=n_heads,
n_kv_heads, n_kv_heads=n_kv_heads,
norm_eps, norm_eps=norm_eps,
head_dim, vocab_size=vocab_size,
rope_theta, head_dim=head_dim,
max_context, rope_theta=rope_theta,
qk_norm, rope_dim=rope_dim,
num_experts, v_head_dim=v_head_dim,
num_experts_per_tok, max_context=max_context,
qk_norm=qk_norm,
num_experts=num_experts,
num_experts_per_tok=num_experts_per_tok,
norm_topk_prob=norm_topk_prob,
qkv_bias=qkv_bias,
expert_bias=expert_bias,
) )
for _ in range(block_count) self.blk = [block_cls(block_config) for _ in range(block_count)]
]
self.max_context = max_context self.max_context = max_context
self.hidden_dim = dim self.hidden_dim = dim
self.first_stage = first_stage self.first_stage = first_stage
@ -389,16 +429,18 @@ class PipelineStageTinygradModel:
self.output = nn_mod.Linear(dim, vocab_size, bias=False) self.output = nn_mod.Linear(dim, vocab_size, bias=False)
def token_hidden(self, tokens_tensor: Any) -> Any: def token_hidden(self, tokens_tensor: Any) -> Any:
return self.token_embd(tokens_tensor) return self.token_embd(tokens_tensor).float()
def forward_hidden(self, hidden: Any, start_pos: int) -> Any: def forward_hidden(self, hidden: Any, start_pos: Any) -> Any:
for block in self.blk: for block in self.blk:
hidden = block(hidden, start_pos) hidden = block(hidden, start_pos)
return hidden.contiguous() return hidden.contiguous()
def next_token(self, hidden: Any) -> Any: def next_token(self, hidden: Any) -> Any:
return self.output(self.output_norm(hidden))[:, -1, :].softmax(-1, dtype="float").argmax(-1, keepdim=True) return self.output(self.output_norm(hidden))[:, -1, :].argmax(-1, keepdim=True)
def __call__(self, tokens_tensor: Any, start_pos: Any) -> Any:
return self.next_token(self.forward_hidden(self.token_hidden(tokens_tensor), start_pos))
def remap_stage_state_dict( def remap_stage_state_dict(
state_dict: dict[str, Any], state_dict: dict[str, Any],
@ -436,42 +478,66 @@ def load_pipeline_stage_model(
) -> tuple[PipelineStageTinygradModel, dict[str, Any]]: ) -> tuple[PipelineStageTinygradModel, dict[str, Any]]:
TensorCls = require_tinygrad() TensorCls = require_tinygrad()
from tinygrad import nn from tinygrad import nn
from tinygrad.apps.llm import TransformerBlock from tinygrad.llm.gguf import gguf_load
from tinygrad.llm.model import TransformerBlock, TransformerConfig
kv, state_dict = nn.state.gguf_load(TensorCls(path).to(None)) kv, state_dict = gguf_load(path)
state_dict = {key: value.cast("float16") if env_flag("HALF", True) else value for key, value in state_dict.items()} state_dict = {key: value.cast("float16") if env_flag("HALF", True) else value for key, value in state_dict.items()}
if "output.weight" not in state_dict and "token_embd.weight" in state_dict:
state_dict["output.weight"] = state_dict["token_embd.weight"]
arch = kv["general.architecture"] arch = kv["general.architecture"]
max_context = min(max_context, int(kv[f"{arch}.context_length"])) max_context = min(max_context, int(kv[f"{arch}.context_length"]))
n_heads = int(kv[f"{arch}.attention.head_count"]) n_heads = int(kv[f"{arch}.attention.head_count"])
n_kv_heads = int(kv[f"{arch}.attention.head_count_kv"]) n_kv_heads = int(kv[f"{arch}.attention.head_count_kv"])
if arch == "llama": dim = int(kv[f"{arch}.embedding_length"])
kv_lora_rank = int(kv.get(f"{arch}.attention.kv_lora_rank", 0))
head_dim = int(kv.get(f"{arch}.attention.key_length_mla", kv.get(f"{arch}.attention.key_length", dim // n_heads)))
rope_dim = int(kv.get(f"{arch}.rope.dimension_count", head_dim))
for name in list(state_dict): for name in list(state_dict):
if "attn_q.weight" in name: if ("attn_q.weight" in name or "attn_q_b.weight" in name) and (arch == "llama" or kv_lora_rank):
state_dict[name] = state_dict[name].rearrange("(n h two) d -> (n two h) d", n=n_heads, two=2) weight = state_dict[name].reshape(n_heads, state_dict[name].shape[0] // n_heads, -1)
if "attn_k.weight" in name: prefix = head_dim - rope_dim
state_dict[name] = state_dict[name].rearrange("(n h two) d -> (n two h) d", n=n_kv_heads, two=2) state_dict[name] = (
total_layers = int(kv[f"{arch}.block_count"]) weight[:, :prefix]
.cat(weight[:, prefix:].rearrange("n (h two) d -> n (two h) d", two=2), dim=1)
.reshape(-1, weight.shape[-1])
)
elif arch == "llama" and "attn_k.weight" in name:
weight = state_dict[name].reshape(n_kv_heads, state_dict[name].shape[0] // n_kv_heads, -1)
state_dict[name] = weight.rearrange("n (h two) d -> n (two h) d", two=2).reshape(-1, weight.shape[-1])
elif kv_lora_rank and "attn_kv_a_mqa.weight" in name:
state_dict[name] = state_dict[name][:kv_lora_rank].cat(
state_dict[name][kv_lora_rank:].rearrange("(h two) d -> (two h) d", two=2),
dim=0,
)
total_layers = int(kv[f"{arch}.block_count"]) - int(kv.get(f"{arch}.nextn_predict_layers", 0))
first_stage = layer_start == 0 first_stage = layer_start == 0
final_stage = layer_end_exclusive >= total_layers final_stage = layer_end_exclusive >= total_layers
qk_key = f"blk.{layer_start}.attn_q_norm.weight" qk_key = f"blk.{layer_start}.attn_q_norm.weight"
qk_norm = int(state_dict[qk_key].shape[0]) if qk_key in state_dict else 0 qk_norm = int(state_dict[qk_key].shape[0]) if qk_key in state_dict else 0
stage_model = PipelineStageTinygradModel( stage_model = PipelineStageTinygradModel(
block_count=layer_end_exclusive - layer_start, block_count=layer_end_exclusive - layer_start,
dim=int(kv[f"{arch}.embedding_length"]), dim=dim,
hidden_dim=int(kv.get(f"{arch}.expert_feed_forward_length", kv[f"{arch}.feed_forward_length"])), hidden_dim=int(kv.get(f"{arch}.expert_feed_forward_length", kv.get(f"{arch}.feed_forward_length", 0))),
n_heads=n_heads, n_heads=n_heads,
n_kv_heads=n_kv_heads, n_kv_heads=n_kv_heads,
norm_eps=float(kv[f"{arch}.attention.layer_norm_rms_epsilon"]), norm_eps=float(kv[f"{arch}.attention.layer_norm_rms_epsilon"]),
vocab_size=len(kv["tokenizer.ggml.tokens"]), vocab_size=len(kv["tokenizer.ggml.tokens"]),
head_dim=int(kv.get(f"{arch}.attention.key_length", int(kv[f"{arch}.embedding_length"]) // n_heads)), head_dim=head_dim,
rope_theta=float(kv[f"{arch}.rope.freq_base"]), rope_theta=float(kv[f"{arch}.rope.freq_base"]),
rope_dim=rope_dim,
v_head_dim=int(kv.get(f"{arch}.attention.value_length_mla", kv.get(f"{arch}.attention.value_length", head_dim))),
max_context=max_context, max_context=max_context,
qk_norm=qk_norm, qk_norm=qk_norm,
num_experts=int(kv.get(f"{arch}.expert_count", 0)), num_experts=int(kv.get(f"{arch}.expert_count", 0)),
num_experts_per_tok=int(kv.get(f"{arch}.expert_used_count", 0)), num_experts_per_tok=int(kv.get(f"{arch}.expert_used_count", 0)),
norm_topk_prob=bool(kv.get(f"{arch}.expert_weights_norm", arch in ("qwen3moe", "qwen35moe"))),
qkv_bias="blk.0.attn_q.bias" in state_dict,
expert_bias=f"blk.{int(kv.get(f'{arch}.leading_dense_block_count', 0))}.exp_probs_b.bias" in state_dict,
first_stage=first_stage, first_stage=first_stage,
final_stage=final_stage, final_stage=final_stage,
nn_mod=nn, nn_mod=nn,
config_cls=TransformerConfig,
block_cls=TransformerBlock, block_cls=TransformerBlock,
) )
stage_state = remap_stage_state_dict( stage_state = remap_stage_state_dict(
@ -501,24 +567,6 @@ def load_weights(cmd: dict[str, Any]) -> None:
layer_start=int(cmd.get("layer_start", 0)), layer_start=int(cmd.get("layer_start", 0)),
layer_end_exclusive=int(cmd.get("layer_end_exclusive", 0)), layer_end_exclusive=int(cmd.get("layer_end_exclusive", 0)),
) )
if test_mode():
model = {"test_mode": True}
tokenizer = {"test_mode": True}
loaded.clear()
loaded.update(
model_id=model_id,
path="mvp-tinygrad-test-mode",
layer_start=int(cmd.get("layer_start", 0)),
layer_end_exclusive=int(cmd.get("layer_end_exclusive", 0)),
)
control(
type="WeightsLoaded",
model_id=model_id,
path=loaded["path"],
test_mode=True,
elapsed_ms=int((time.monotonic() - started) * 1000),
)
return
control(type="GgufResolveStarted", model_id=model_id, source_kind=source_kind(source)) control(type="GgufResolveStarted", model_id=model_id, source_kind=source_kind(source))
path = fetch_whole(source) path = fetch_whole(source)
model_bytes = path.stat().st_size model_bytes = path.stat().st_size
@ -527,7 +575,7 @@ def load_weights(cmd: dict[str, Any]) -> None:
layer_end_exclusive = int(cmd.get("layer_end_exclusive", 0)) layer_end_exclusive = int(cmd.get("layer_end_exclusive", 0))
try: try:
control(type="TinygradLlmImportStarted", model_id=model_id) control(type="TinygradLlmImportStarted", model_id=model_id)
from tinygrad.apps.llm import SimpleTokenizer from tinygrad.llm.cli import SimpleTokenizer
control(type="TinygradLlmImportReady", model_id=model_id) control(type="TinygradLlmImportReady", model_id=model_id)
max_context_raw = os.environ.get("MVP_MAX_CONTEXT", "512") max_context_raw = os.environ.get("MVP_MAX_CONTEXT", "512")
@ -739,19 +787,6 @@ def infer_prompt(cmd: dict[str, Any]) -> None:
prompt_chars=len(prompt), prompt_chars=len(prompt),
max_tokens=max_tokens, max_tokens=max_tokens,
) )
if test_mode():
text = f"mvp-test response: {prompt}"
control(
type="PromptCompleted",
request_id=request_id,
model_id=loaded.get("model_id"),
prompt_tokens=[],
generated_tokens=list(range(min(max_tokens, 3))),
text=text,
test_mode=True,
elapsed_ms=int((time.monotonic() - started) * 1000),
)
return
model_prompt, prompt_template = model_prompt_text(prompt) model_prompt, prompt_template = model_prompt_text(prompt)
control( control(
type="PromptEncodeStarted", type="PromptEncodeStarted",
@ -907,7 +942,7 @@ def object_start_pos(sequence: int, token_count: int) -> int:
def materialize_object(payload: bytes, sequence: int) -> dict[str, Any]: def materialize_object(payload: bytes, sequence: int) -> dict[str, Any]:
if test_mode() or not isinstance(model, PipelineStageTinygradModel): if not isinstance(model, PipelineStageTinygradModel):
return { return {
"kind": "words", "kind": "words",
"words": payload_words(payload), "words": payload_words(payload),
@ -1013,7 +1048,7 @@ def execute_step(cmd: dict[str, Any]) -> None:
if ring["direction"] != "egress": if ring["direction"] != "egress":
fatal("WrongRingDirection", ring_id=output_ring_id, direction=ring["direction"]) fatal("WrongRingDirection", ring_id=output_ring_id, direction=ring["direction"])
final_stage = bool(cmd.get("final_stage")) final_stage = bool(cmd.get("final_stage"))
if test_mode() or not isinstance(model, PipelineStageTinygradModel): if not isinstance(model, PipelineStageTinygradModel):
if final_stage: if final_stage:
base = sum(int(word) for word in obj["words"]) + int(role.get("stage_index", 0)) base = sum(int(word) for word in obj["words"]) + int(role.get("stage_index", 0))
token = 6 if base % 2 else 8 token = 6 if base % 2 else 8
@ -1067,7 +1102,7 @@ def release_device_object(cmd: dict[str, Any]) -> None:
def encode_prompt(cmd: dict[str, Any]) -> None: def encode_prompt(cmd: dict[str, Any]) -> None:
prompt = str(cmd.get("prompt", "")) prompt = str(cmd.get("prompt", ""))
if tokenizer is not None and not test_mode(): if tokenizer is not None:
model_prompt, _ = model_prompt_text(prompt) model_prompt, _ = model_prompt_text(prompt)
tokens = [int(token) for token in tokenizer.encode(model_prompt)] tokens = [int(token) for token in tokenizer.encode(model_prompt)]
else: else:
@ -1077,7 +1112,7 @@ def encode_prompt(cmd: dict[str, Any]) -> None:
def decode_tokens(cmd: dict[str, Any]) -> None: def decode_tokens(cmd: dict[str, Any]) -> None:
tokens = [int(token) for token in cmd.get("tokens", [])] tokens = [int(token) for token in cmd.get("tokens", [])]
if tokenizer is not None and not test_mode(): if tokenizer is not None:
text = strip_chat_stop_markers(tokenizer.decode(tokens)) text = strip_chat_stop_markers(tokenizer.decode(tokens))
else: else:
text = "".join(chr(token) if 32 <= token <= 126 else f"<tok:{token}>" for token in tokens) text = "".join(chr(token) if 32 <= token <= 126 else f"<tok:{token}>" for token in tokens)

View file

@ -0,0 +1,32 @@
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use serde_json::{Value, json};
pub const BENCHMARK_SCHEMA: u64 = 1;
static BENCHMARK_START: OnceLock<Instant> = OnceLock::new();
static BENCHMARK_SEQ: AtomicU64 = AtomicU64::new(1);
pub fn unix_ms_now() -> u64 {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
u64::try_from(millis).unwrap_or(u64::MAX)
}
pub fn stamp(component: &'static str) -> Value {
let start = BENCHMARK_START.get_or_init(Instant::now);
let mono_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
let seq = BENCHMARK_SEQ.fetch_add(1, Ordering::Relaxed);
json!({
"schema": BENCHMARK_SCHEMA,
"component": component,
"pid": std::process::id(),
"seq": seq,
"wall_unix_ms": unix_ms_now(),
"mono_ms": mono_ms,
})
}

View file

@ -22,6 +22,7 @@ use signal_hook::consts::signal::{SIGINT, SIGTERM};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use signal_hook::iterator::Signals; use signal_hook::iterator::Signals;
use mvp_system::benchmark_observability;
use mvp_system::config as chat_config; use mvp_system::config as chat_config;
use mvp_system::config::ResolvedVastAiConfig; use mvp_system::config::ResolvedVastAiConfig;
use mvp_system::node_image::{ use mvp_system::node_image::{
@ -73,7 +74,7 @@ where
I: IntoIterator<Item = String>, I: IntoIterator<Item = String>,
{ {
let config = Config::from_args(args)?; let config = Config::from_args(args)?;
let mut progress = ChatDatastream::new(1, config.datastream_frame_log.clone())?; let mut progress = ChatDatastream::new(config.run_id, config.datastream_frame_log.clone())?;
progress.emit( progress.emit(
CHAT_LIFECYCLE_CHANNEL, CHAT_LIFECYCLE_CHANNEL,
"config", "config",
@ -87,7 +88,14 @@ where
}), }),
); );
confirm_vastai_if_needed(&config)?; confirm_vastai_if_needed(&config)?;
let image_ref = match prepare_runtime(&config) { progress.emit(
CHAT_RUNTIME_CHANNEL,
"prepare_runtime",
"started",
json!({"provider": config.provider.as_str()}),
);
let image_ref =
match prepare_runtime_with_progress(&config, prepare_node_image, Some(&mut progress)) {
Ok(image_ref) => { Ok(image_ref) => {
progress.emit( progress.emit(
CHAT_RUNTIME_CHANNEL, CHAT_RUNTIME_CHANNEL,
@ -108,8 +116,20 @@ where
return Err(error); return Err(error);
} }
}; };
progress.emit(
CHAT_COMPONENT_CHANNEL,
"orchestrator_process_spawn",
"started",
json!({"binary": config.orch_bin.to_string_lossy()}),
);
let mut orch = match OrchChild::spawn(&config, &image_ref) { let mut orch = match OrchChild::spawn(&config, &image_ref) {
Ok(orch) => { Ok(orch) => {
progress.emit(
CHAT_COMPONENT_CHANNEL,
"orchestrator_process_spawn",
"ready",
json!({"binary": config.orch_bin.to_string_lossy(), "pid": orch.child.id()}),
);
progress.emit( progress.emit(
CHAT_COMPONENT_CHANNEL, CHAT_COMPONENT_CHANNEL,
"orchestrator_process", "orchestrator_process",
@ -119,6 +139,12 @@ where
orch orch
} }
Err(error) => { Err(error) => {
progress.emit(
CHAT_COMPONENT_CHANNEL,
"orchestrator_process_spawn",
"failed",
json!({"binary": config.orch_bin.to_string_lossy(), "error": error}),
);
progress.emit( progress.emit(
CHAT_COMPONENT_CHANNEL, CHAT_COMPONENT_CHANNEL,
"orchestrator_process", "orchestrator_process",
@ -129,8 +155,20 @@ where
return Err(error); return Err(error);
} }
}; };
progress.emit(
CHAT_RUNTIME_CHANNEL,
"prompt_rpc_wait",
"started",
json!({"addr": config.rpc_addr}),
);
let rpc_addr = match orch.wait_ready(config.rpc_addr.clone()) { let rpc_addr = match orch.wait_ready(config.rpc_addr.clone()) {
Ok(addr) => { Ok(addr) => {
progress.emit(
CHAT_RUNTIME_CHANNEL,
"prompt_rpc_wait",
"ready",
json!({"addr": addr}),
);
progress.emit( progress.emit(
CHAT_RUNTIME_CHANNEL, CHAT_RUNTIME_CHANNEL,
"prompt_rpc", "prompt_rpc",
@ -139,7 +177,13 @@ where
); );
addr addr
} }
Err(_) if STOP_REQUESTED.load(Ordering::SeqCst) => { Err(error) if STOP_REQUESTED.load(Ordering::SeqCst) => {
progress.emit(
CHAT_RUNTIME_CHANNEL,
"prompt_rpc_wait",
"failed",
json!({"addr": config.rpc_addr, "error": error}),
);
progress.emit( progress.emit(
CHAT_LIFECYCLE_CHANNEL, CHAT_LIFECYCLE_CHANNEL,
"shutdown", "shutdown",
@ -157,6 +201,12 @@ where
return Ok(()); return Ok(());
} }
Err(error) => { Err(error) => {
progress.emit(
CHAT_RUNTIME_CHANNEL,
"prompt_rpc_wait",
"failed",
json!({"addr": config.rpc_addr, "error": error}),
);
progress.emit( progress.emit(
CHAT_RUNTIME_CHANNEL, CHAT_RUNTIME_CHANNEL,
"prompt_rpc", "prompt_rpc",
@ -201,6 +251,7 @@ struct Config {
image_tag: Option<String>, image_tag: Option<String>,
cached_model: Option<CachedModelConfig>, cached_model: Option<CachedModelConfig>,
datastream_frame_log: Option<PathBuf>, datastream_frame_log: Option<PathBuf>,
run_id: u64,
vastai_yes: bool, vastai_yes: bool,
vastai: Option<ResolvedVastAiConfig>, vastai: Option<ResolvedVastAiConfig>,
pipeline_stages: u32, pipeline_stages: u32,
@ -210,6 +261,7 @@ struct Config {
struct ChatDatastream { struct ChatDatastream {
stream: StreamId, stream: StreamId,
run_id: u64,
endpoint: DatastreamEndpoint, endpoint: DatastreamEndpoint,
producer: DatastreamProducer, producer: DatastreamProducer,
channels: BTreeMap<String, ChannelId>, channels: BTreeMap<String, ChannelId>,
@ -233,6 +285,7 @@ impl ChatDatastream {
let producer = endpoint.producer(); let producer = endpoint.producer();
let mut out = Self { let mut out = Self {
stream, stream,
run_id,
endpoint, endpoint,
producer, producer,
channels: BTreeMap::new(), channels: BTreeMap::new(),
@ -272,6 +325,8 @@ impl ChatDatastream {
"type": "ChatProgress", "type": "ChatProgress",
"phase": phase, "phase": phase,
"status": status, "status": status,
"run_id": self.run_id,
"benchmark": benchmark_observability::stamp("mvp-chat"),
"detail": detail, "detail": detail,
})) }))
.expect("serialize mvp-chat progress event"); .expect("serialize mvp-chat progress event");
@ -356,6 +411,7 @@ impl ChatFrameArchive {
}; };
let record = json!({ let record = json!({
"arrival_seq": self.next_seq, "arrival_seq": self.next_seq,
"arrival_unix_ms": benchmark_observability::unix_ms_now(),
"source": source, "source": source,
"stream": stream.to_string(), "stream": stream.to_string(),
"channel": channel, "channel": channel,
@ -514,6 +570,7 @@ impl Config {
image_tag: first_non_empty([toml.image.tag.clone()]), image_tag: first_non_empty([toml.image.tag.clone()]),
cached_model, cached_model,
datastream_frame_log, datastream_frame_log,
run_id: args.run_id.unwrap_or(1),
vastai_yes: args.vastai_yes, vastai_yes: args.vastai_yes,
pipeline_stages, pipeline_stages,
max_tokens, max_tokens,
@ -534,6 +591,8 @@ impl Config {
self.rpc_addr.clone(), self.rpc_addr.clone(),
"--max-tokens".to_owned(), "--max-tokens".to_owned(),
self.max_tokens.to_string(), self.max_tokens.to_string(),
"--run-id".to_owned(),
self.run_id.to_string(),
"--pipeline-stages".to_owned(), "--pipeline-stages".to_owned(),
self.pipeline_stages.to_string(), self.pipeline_stages.to_string(),
"--no-dashboard".to_owned(), "--no-dashboard".to_owned(),
@ -617,6 +676,7 @@ struct ParsedArgs {
pipeline_stages: Option<u32>, pipeline_stages: Option<u32>,
dump_logs: bool, dump_logs: bool,
dump_log_path: Option<PathBuf>, dump_log_path: Option<PathBuf>,
run_id: Option<u64>,
skip_rebuild: bool, skip_rebuild: bool,
cached_model: Option<CachedModelSource>, cached_model: Option<CachedModelSource>,
} }
@ -658,6 +718,13 @@ impl ParsedArgs {
parsed.pipeline_stages = parsed.pipeline_stages =
Some(parse_pipeline_stages_value(&mut args, arg.as_str())?) Some(parse_pipeline_stages_value(&mut args, arg.as_str())?)
} }
"--run-id" => {
let run_id: u64 = parse_next(&mut args, "--run-id")?;
if run_id == 0 {
return Err("--run-id must be greater than 0".to_owned());
}
parsed.run_id = Some(run_id);
}
"--dump-logs" => { "--dump-logs" => {
parsed.dump_logs = true; parsed.dump_logs = true;
} }
@ -916,33 +983,193 @@ fn signal_orch_process_group(child: &Child, signal: libc::c_int) -> io::Result<(
type PrepareNodeImageFn = fn(NodeImageRequest) -> Result<PreparedNodeImage, String>; type PrepareNodeImageFn = fn(NodeImageRequest) -> Result<PreparedNodeImage, String>;
#[allow(dead_code)]
fn prepare_runtime(config: &Config) -> Result<String, String> { fn prepare_runtime(config: &Config) -> Result<String, String> {
prepare_runtime_with(config, prepare_node_image) prepare_runtime_with(config, prepare_node_image)
} }
#[allow(dead_code)]
fn prepare_runtime_with( fn prepare_runtime_with(
config: &Config, config: &Config,
prepare_node_image_fn: PrepareNodeImageFn, prepare_node_image_fn: PrepareNodeImageFn,
) -> Result<String, String> { ) -> Result<String, String> {
ensure_orch_binary(config)?; prepare_runtime_with_progress(config, prepare_node_image_fn, None)
}
fn prepare_runtime_with_progress(
config: &Config,
prepare_node_image_fn: PrepareNodeImageFn,
progress: Option<&mut ChatDatastream>,
) -> Result<String, String> {
let mut progress = progress;
let binary_mode = if config.skip_rebuild {
"existing_artifact"
} else {
"cargo_build"
};
emit_chat_progress(
&mut progress,
CHAT_RUNTIME_CHANNEL,
"ensure_orch_binary",
"started",
json!({"mode": binary_mode}),
);
match ensure_orch_binary(config) {
Ok(()) => emit_chat_progress(
&mut progress,
CHAT_RUNTIME_CHANNEL,
"ensure_orch_binary",
"ready",
json!({"mode": binary_mode}),
),
Err(error) => {
emit_chat_progress(
&mut progress,
CHAT_RUNTIME_CHANNEL,
"ensure_orch_binary",
"failed",
json!({"mode": binary_mode, "error": error.as_str()}),
);
return Err(error);
}
}
if config.provider == ProviderKind::Process { if config.provider == ProviderKind::Process {
ensure_worker_binary(config)?; emit_chat_progress(
&mut progress,
CHAT_RUNTIME_CHANNEL,
"ensure_worker_binary",
"started",
json!({"mode": binary_mode}),
);
match ensure_worker_binary(config) {
Ok(()) => emit_chat_progress(
&mut progress,
CHAT_RUNTIME_CHANNEL,
"ensure_worker_binary",
"ready",
json!({"mode": binary_mode}),
),
Err(error) => {
emit_chat_progress(
&mut progress,
CHAT_RUNTIME_CHANNEL,
"ensure_worker_binary",
"failed",
json!({"mode": binary_mode, "error": error.as_str()}),
);
return Err(error);
}
}
emit_chat_progress(
&mut progress,
CHAT_RUNTIME_CHANNEL,
"prepare_node_image",
"skipped",
json!({"provider": config.provider.as_str(), "reason": "process_provider"}),
);
return Ok(config.node_image.clone()); return Ok(config.node_image.clone());
} }
if config.skip_rebuild { if config.skip_rebuild {
ensure_worker_binary(config)?; emit_chat_progress(
&mut progress,
CHAT_RUNTIME_CHANNEL,
"ensure_worker_binary",
"started",
json!({"mode": binary_mode}),
);
match ensure_worker_binary(config) {
Ok(()) => emit_chat_progress(
&mut progress,
CHAT_RUNTIME_CHANNEL,
"ensure_worker_binary",
"ready",
json!({"mode": binary_mode}),
),
Err(error) => {
emit_chat_progress(
&mut progress,
CHAT_RUNTIME_CHANNEL,
"ensure_worker_binary",
"failed",
json!({"mode": binary_mode, "error": error.as_str()}),
);
return Err(error);
}
}
emit_chat_progress(
&mut progress,
CHAT_RUNTIME_CHANNEL,
"prepare_node_image",
"skipped",
json!({"provider": config.provider.as_str(), "reason": "skip_rebuild"}),
);
return Ok(config.node_image.clone()); return Ok(config.node_image.clone());
} }
let prepared = prepare_node_image_fn(NodeImageRequest {
emit_chat_progress(
&mut progress,
CHAT_RUNTIME_CHANNEL,
"prepare_node_image",
"started",
json!({"provider": config.provider.as_str()}),
);
let node_bin = match node_bin_for_current_profile() {
Ok(path) => path,
Err(error) => {
emit_chat_progress(
&mut progress,
CHAT_RUNTIME_CHANNEL,
"prepare_node_image",
"failed",
json!({"provider": config.provider.as_str(), "error": error.as_str()}),
);
return Err(error);
}
};
let provider = match node_image_provider(config.provider) {
Ok(provider) => provider,
Err(error) => {
emit_chat_progress(
&mut progress,
CHAT_RUNTIME_CHANNEL,
"prepare_node_image",
"failed",
json!({"provider": config.provider.as_str(), "error": error.as_str()}),
);
return Err(error);
}
};
let prepared = match prepare_node_image_fn(NodeImageRequest {
requested_image: config.node_image.clone(), requested_image: config.node_image.clone(),
base_image: BASE_NODE_IMAGE.to_owned(), base_image: BASE_NODE_IMAGE.to_owned(),
node_bin: node_bin_for_current_profile()?, node_bin,
provider: node_image_provider(config.provider)?, provider,
extra_tag: config.image_tag.clone(), extra_tag: config.image_tag.clone(),
push: false, push: false,
force_refresh: false, force_refresh: false,
enabled: true, enabled: true,
})?; }) {
Ok(prepared) => prepared,
Err(error) => {
emit_chat_progress(
&mut progress,
CHAT_RUNTIME_CHANNEL,
"prepare_node_image",
"failed",
json!({"provider": config.provider.as_str(), "error": error.as_str()}),
);
return Err(error);
}
};
emit_chat_progress(
&mut progress,
CHAT_RUNTIME_CHANNEL,
"prepare_node_image",
"ready",
json!({"provider": config.provider.as_str(), "image_ref": prepared.image_ref}),
);
Ok(prepared.image_ref) Ok(prepared.image_ref)
} }
@ -1254,7 +1481,12 @@ where
json!({"request_id": request_id, "text_bytes": text.len()}), json!({"request_id": request_id, "text_bytes": text.len()}),
); );
} }
PromptEvent::Done { .. } => { PromptEvent::Done {
final_text,
tokens_generated,
elapsed_ms,
..
} => {
if response_started { if response_started {
writeln!(output).map_err(|e| format!("write response terminator: {e}"))?; writeln!(output).map_err(|e| format!("write response terminator: {e}"))?;
} else { } else {
@ -1266,7 +1498,13 @@ where
CHAT_PROMPT_CHANNEL, CHAT_PROMPT_CHANNEL,
"request_completed", "request_completed",
"ready", "ready",
json!({"request_id": request_id, "response_started": response_started}), json!({
"request_id": request_id,
"response_started": response_started,
"tokens_generated": tokens_generated,
"elapsed_ms": elapsed_ms,
"final_text_bytes": final_text.len(),
}),
); );
break; break;
} }
@ -1642,6 +1880,7 @@ mod tests {
image_tag: None, image_tag: None,
cached_model: None, cached_model: None,
datastream_frame_log: None, datastream_frame_log: None,
run_id: 1,
vastai_yes: false, vastai_yes: false,
vastai: None, vastai: None,
pipeline_stages: 1, pipeline_stages: 1,
@ -1704,6 +1943,48 @@ mod tests {
.collect() .collect()
} }
#[test]
fn benchmark_observability_chat_progress_records_include_run_id_and_stamp() {
let temp = TempDir::new("chat-progress-archive");
let archive_path = temp.path().join("frames.ndjson");
let mut progress = ChatDatastream::new(77, Some(archive_path.clone()))
.expect("chat datastream constructs");
progress.emit(
CHAT_RUNTIME_CHANNEL,
"unit_phase",
"ready",
serde_json::json!({"ok": true}),
);
progress.archive_pending().expect("archive pending frames");
let archive = fs::read_to_string(&archive_path).expect("read archive");
let line = archive.lines().next().expect("archive line");
let outer: serde_json::Value = serde_json::from_str(line).expect("outer archive JSON");
let inner_text = outer
.get("payload")
.and_then(|payload| payload.get("value"))
.and_then(serde_json::Value::as_str)
.expect("inner event text");
let inner: serde_json::Value = serde_json::from_str(inner_text).expect("inner event JSON");
assert_eq!(
inner.get("type").and_then(serde_json::Value::as_str),
Some("ChatProgress")
);
assert_eq!(
inner.get("run_id").and_then(serde_json::Value::as_u64),
Some(77)
);
assert_eq!(
inner
.get("benchmark")
.and_then(|benchmark| benchmark.get("schema"))
.and_then(serde_json::Value::as_u64),
Some(1)
);
}
#[test] #[test]
fn parsed_args_accepts_public_flags() { fn parsed_args_accepts_public_flags() {
let parsed = ParsedArgs::parse(strings(&[ let parsed = ParsedArgs::parse(strings(&[
@ -1729,6 +2010,32 @@ mod tests {
assert!(parsed.skip_rebuild); assert!(parsed.skip_rebuild);
} }
#[test]
fn benchmark_observability_parsed_args_accepts_run_id_and_forwards_to_orchestrator() {
let parsed = ParsedArgs::parse(strings(&["--run-id", "123"])).expect("run id parses");
assert_eq!(parsed.run_id, Some(123));
let temp = TempDir::new("run-id-config");
with_process_state(&[], Some(temp.path()), || {
let config =
Config::from_args(strings(&["--run-id", "123"])).expect("config resolves run id");
assert_eq!(config.run_id, 123);
let args = config.orchestrator_cli_args("resolved-image");
let run_id_arg = args
.windows(2)
.find(|pair| pair[0] == "--run-id")
.map(|pair| pair[1].as_str());
assert_eq!(run_id_arg, Some("123"), "{args:?}");
});
}
#[test]
fn benchmark_observability_parsed_args_rejects_zero_run_id() {
let error =
ParsedArgs::parse(strings(&["--run-id", "0"])).expect_err("zero run id should fail");
assert_eq!(error, "--run-id must be greater than 0");
}
#[test] #[test]
fn parsed_args_accepts_cached_model_path() { fn parsed_args_accepts_cached_model_path() {
let parsed = ParsedArgs::parse(strings(&["--cached-model=/tmp/model.gguf"])) let parsed = ParsedArgs::parse(strings(&["--cached-model=/tmp/model.gguf"]))
@ -1795,6 +2102,7 @@ mod tests {
let defaults = Config::from_args(Vec::<String>::new()).expect("defaults resolve"); let defaults = Config::from_args(Vec::<String>::new()).expect("defaults resolve");
assert_eq!(defaults.provider, ProviderKind::Process); assert_eq!(defaults.provider, ProviderKind::Process);
assert_eq!(defaults.pipeline_stages, 1); assert_eq!(defaults.pipeline_stages, 1);
assert_eq!(defaults.run_id, 1);
assert!(defaults.datastream_frame_log.is_none()); assert!(defaults.datastream_frame_log.is_none());
assert!(defaults.cached_model.is_none()); assert!(defaults.cached_model.is_none());
assert!(defaults.vastai.is_none()); assert!(defaults.vastai.is_none());

View file

@ -27,6 +27,7 @@ use mvp_system::actors::node_agent::{
}; };
use mvp_system::actors::orchestrator::{OrchestratorActor, OrchestratorReport}; use mvp_system::actors::orchestrator::{OrchestratorActor, OrchestratorReport};
use mvp_system::actors::register_mvp_actor_codecs; use mvp_system::actors::register_mvp_actor_codecs;
use mvp_system::benchmark_observability;
use mvp_system::config::{DEFAULT_CONFIG_PATH, TomlConfigOverlay}; use mvp_system::config::{DEFAULT_CONFIG_PATH, TomlConfigOverlay};
#[cfg(feature = "dashboard")] #[cfg(feature = "dashboard")]
use mvp_system::dashboard_view::MvpClusterDashboardView; use mvp_system::dashboard_view::MvpClusterDashboardView;
@ -415,6 +416,18 @@ fn run() -> Result<(), String> {
orchestrator_actor, orchestrator_actor,
)?; )?;
orch_datastream.emit_bootstrap(
dashboard.as_ref(),
config.run_id,
config.node_id,
"prompt_rpc",
"started",
json!({
"bind":config.rpc_bind.to_string(),
"default_max_tokens":config.default_max_tokens,
}),
);
let rpc_addr = match spawn_prompt_rpc(config.rpc_bind, work_tx, config.default_max_tokens) { let rpc_addr = match spawn_prompt_rpc(config.rpc_bind, work_tx, config.default_max_tokens) {
Ok(addr) => { Ok(addr) => {
orch_datastream.emit_bootstrap( orch_datastream.emit_bootstrap(
@ -1700,9 +1713,6 @@ impl Config {
if self.provider == ProviderKind::Docker { if self.provider == ProviderKind::Docker {
keys.push("MVP_DOCKER_GPUS"); keys.push("MVP_DOCKER_GPUS");
} }
if std::env::var_os("MVP_TINYGRAD_TEST_MODE").is_some() {
keys.push("MVP_TINYGRAD_TEST_MODE");
}
if local_tinygrad_worker_env(self.provider).is_some() { if local_tinygrad_worker_env(self.provider).is_some() {
keys.push("MVP_TINYGRAD_WORKER"); keys.push("MVP_TINYGRAD_WORKER");
} }
@ -1800,7 +1810,6 @@ impl Config {
if self.provider == ProviderKind::Docker { if self.provider == ProviderKind::Docker {
env.push(("MVP_DOCKER_GPUS".to_owned(), self.docker_gpus.clone())); env.push(("MVP_DOCKER_GPUS".to_owned(), self.docker_gpus.clone()));
} }
env.extend(optional_env("MVP_TINYGRAD_TEST_MODE"));
env.extend(local_tinygrad_worker_env(self.provider)); env.extend(local_tinygrad_worker_env(self.provider));
env.extend(optional_env("MVP_CPU_LINE_PROFILE")); env.extend(optional_env("MVP_CPU_LINE_PROFILE"));
env.extend(optional_env("MVP_CPU_LINE_PROFILE_INTERVAL_MS")); env.extend(optional_env("MVP_CPU_LINE_PROFILE_INTERVAL_MS"));
@ -3132,6 +3141,7 @@ impl FrameArchive {
}; };
let record = json!({ let record = json!({
"arrival_seq":self.next_seq, "arrival_seq":self.next_seq,
"arrival_unix_ms":benchmark_observability::unix_ms_now(),
"source":source, "source":source,
"stream":stream.to_string(), "stream":stream.to_string(),
"channel":channel, "channel":channel,
@ -3252,6 +3262,7 @@ impl OrchDatastream {
"status":status, "status":status,
"run_id":run_id, "run_id":run_id,
"node_id":node_id, "node_id":node_id,
"benchmark":benchmark_observability::stamp("mvp-orchestrator"),
"detail":detail, "detail":detail,
})) }))
.expect("serialize orch bootstrap event"); .expect("serialize orch bootstrap event");
@ -3275,6 +3286,7 @@ impl OrchDatastream {
"run_id":run_id, "run_id":run_id,
"node_id":node_id, "node_id":node_id,
"request_id":request_id, "request_id":request_id,
"benchmark":benchmark_observability::stamp("mvp-orchestrator"),
"detail":detail, "detail":detail,
})) }))
.expect("serialize orch prompt event"); .expect("serialize orch prompt event");
@ -4042,6 +4054,21 @@ impl PipelinePromptRuntime {
.unwrap_or(0); .unwrap_or(0);
let final_text = self.final_text.clone(); let final_text = self.final_text.clone();
let tokens_generated = self.generated_tokens.len() as u32; let tokens_generated = self.generated_tokens.len() as u32;
orch_datastream.emit_prompt(
dashboard,
run_id,
node_id,
request_id,
"prompt_complete",
"ready",
json!({
"event":"Done",
"terminal":true,
"tokens_generated":tokens_generated,
"elapsed_ms":elapsed_ms,
"final_text_bytes":final_text.len(),
}),
);
let _ = active.events.send(PromptEvent::Done { let _ = active.events.send(PromptEvent::Done {
request_id, request_id,
final_text, final_text,
@ -4985,7 +5012,6 @@ mod tests {
"MVP_PIPELINE_STAGES", "MVP_PIPELINE_STAGES",
"MVP_STAGE_INDEX", "MVP_STAGE_INDEX",
"MVP_TOKEN_PROGRESS_EVERY", "MVP_TOKEN_PROGRESS_EVERY",
"MVP_TINYGRAD_TEST_MODE",
"MVP_TINYGRAD_WORKER", "MVP_TINYGRAD_WORKER",
"MVP_TOKENIZER_LOCAL_PATH", "MVP_TOKENIZER_LOCAL_PATH",
"MVP_VASTAI_API_KEY", "MVP_VASTAI_API_KEY",
@ -6070,28 +6096,37 @@ mod tests {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&path);
assert_eq!(records.len(), 2);
assert_eq!(records[0]["arrival_seq"], json!(0));
assert!(
records[0]["arrival_unix_ms"]
.as_u64()
.is_some_and(|value| value > 0)
);
assert_eq!(records[0]["source"], json!("orchestrator"));
assert_eq!(records[0]["stream"], json!("test-node#42"));
assert_eq!(records[0]["channel"], json!("stdout"));
assert_eq!(records[0]["channel_id"], json!(1));
assert_eq!(records[0]["position"], json!(7));
assert_eq!( assert_eq!(
records, records[0]["payload"],
vec![ json!({"encoding":"utf8","value":"hello λ"})
json!({ );
"arrival_seq":0,
"source":"orchestrator", assert_eq!(records[1]["arrival_seq"], json!(1));
"stream":"test-node#42", assert!(
"channel":"stdout", records[1]["arrival_unix_ms"]
"channel_id":1, .as_u64()
"position":7, .is_some_and(|value| value > 0)
"payload":{"encoding":"utf8","value":"hello λ"}, );
}), assert_eq!(records[1]["source"], json!("orchestrator"));
json!({ assert_eq!(records[1]["stream"], json!("test-node#42"));
"arrival_seq":1, assert_eq!(records[1]["channel"], json!("stderr"));
"source":"orchestrator", assert_eq!(records[1]["channel_id"], json!(2));
"stream":"test-node#42", assert_eq!(records[1]["position"], json!(8));
"channel":"stderr", assert_eq!(
"channel_id":2, records[1]["payload"],
"position":8, json!({"encoding":"bytes","value":[255,0,65]})
"payload":{"encoding":"bytes","value":[255,0,65]},
}),
]
); );
} }

View file

@ -29,6 +29,7 @@ use mvp_system::actors::node_agent::{
}; };
use mvp_system::actors::register_mvp_actor_codecs; use mvp_system::actors::register_mvp_actor_codecs;
use mvp_system::arena_manager as arena; use mvp_system::arena_manager as arena;
use mvp_system::benchmark_observability;
use mvp_system::distribution_stack::DistributionRuntimeStack; use mvp_system::distribution_stack::DistributionRuntimeStack;
use mvp_system::driver_pumps as driver_model; use mvp_system::driver_pumps as driver_model;
use mvp_system::edge_establisher as edge; use mvp_system::edge_establisher as edge;
@ -72,10 +73,26 @@ fn node_event_payload(
"run_id":config.run_id, "run_id":config.run_id,
"node_id":config.logical_node_id, "node_id":config.logical_node_id,
"stage_index":config.stage_index, "stage_index":config.stage_index,
"benchmark":benchmark_observability::stamp("mvp-worker-node"),
"detail":detail, "detail":detail,
}) })
} }
fn emit_stdio_datastream_frame(channel: &str, payload: &Value) -> Result<(), String> {
println!(
"{}",
json!({
"mvp_stdio_event":1,
"kind":"datastream_frame",
"channel":channel,
"payload":payload,
})
);
std::io::stdout()
.flush()
.map_err(|e| format!("flush stdio datastream frame: {e}"))
}
fn emit_stdio_node_event( fn emit_stdio_node_event(
config: &DeploymentConfig, config: &DeploymentConfig,
channel: &str, channel: &str,
@ -83,18 +100,8 @@ fn emit_stdio_node_event(
status: &str, status: &str,
detail: Value, detail: Value,
) -> Result<(), String> { ) -> Result<(), String> {
println!( let payload = node_event_payload(config, phase, status, detail);
"{}", emit_stdio_datastream_frame(channel, &payload)
json!({
"mvp_stdio_event":1,
"kind":"datastream_frame",
"channel":channel,
"payload":node_event_payload(config, phase, status, detail),
})
);
std::io::stdout()
.flush()
.map_err(|e| format!("flush stdio node event: {e}"))
} }
fn emit_node_event( fn emit_node_event(
@ -2956,6 +2963,12 @@ impl DeploymentConfig {
.into_owned(), .into_owned(),
), ),
}; };
let provider = env_string("MVP_NODE_PROVIDER", "process");
let default_device = if provider == "process" {
"CPU"
} else {
DEFAULT_DEVICE
};
Ok(Self { Ok(Self {
run_id, run_id,
logical_node_id, logical_node_id,
@ -2966,7 +2979,7 @@ impl DeploymentConfig {
debug_join_socket, debug_join_socket,
relay_mode: relay.mode, relay_mode: relay.mode,
worker_script: env_string("MVP_TINYGRAD_WORKER", DEFAULT_WORKER_SCRIPT), worker_script: env_string("MVP_TINYGRAD_WORKER", DEFAULT_WORKER_SCRIPT),
device: env_string("DEV", DEFAULT_DEVICE), device: env_string("DEV", default_device),
model_id: env_string("MVP_MODEL_ID", DEFAULT_MODEL_ID), model_id: env_string("MVP_MODEL_ID", DEFAULT_MODEL_ID),
gguf_source: gguf_source_from_env(), gguf_source: gguf_source_from_env(),
tokenizer: tokenizer_from_env(), tokenizer: tokenizer_from_env(),
@ -2991,6 +3004,9 @@ 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("MVP_LOGICAL_NODE_ID", config.logical_node_id.to_string())
.env("MVP_STAGE_INDEX", config.stage_index.to_string())
.env("MVP_ARENA_FD", arena_fd.to_string()) .env("MVP_ARENA_FD", arena_fd.to_string())
.env("MVP_ARENA_BYTES", config.arena_bytes.to_string()) .env("MVP_ARENA_BYTES", config.arena_bytes.to_string())
.stdin(Stdio::piped()) .stdin(Stdio::piped())
@ -3518,6 +3534,8 @@ impl TinygradWorker {
json!({"expected_event_type":expected,"channel":channel_name,"line_bytes":n,"worker_event_type":worker_event_type}), json!({"expected_event_type":expected,"channel":channel_name,"line_bytes":n,"worker_event_type":worker_event_type}),
); );
datastream.submit_text(channel, value.to_string()); datastream.submit_text(channel, value.to_string());
emit_stdio_datastream_frame(channel_name, &value)
.map_err(|e| format!("emit worker stdio datastream frame: {e}"))?;
datastream.tick(); datastream.tick();
pump(); pump();
if worker_event_type == "WorkerFatal" { if worker_event_type == "WorkerFatal" {
@ -3651,6 +3669,23 @@ mod tests {
DistributionRuntimeStack::new(DistNodeId([1; 32]), DistributedNodeConfig::default()) DistributionRuntimeStack::new(DistNodeId([1; 32]), DistributedNodeConfig::default())
} }
#[test]
fn benchmark_observability_node_event_payload_includes_stamp() {
let config = test_config(None);
let payload = node_event_payload(&config, "phase", "ready", json!({"ok": true}));
assert_eq!(payload.get("run_id").and_then(Value::as_u64), Some(7));
assert_eq!(payload.get("node_id").and_then(Value::as_u64), Some(11));
assert_eq!(payload.get("stage_index").and_then(Value::as_u64), Some(3));
assert_eq!(
payload
.get("benchmark")
.and_then(|benchmark| benchmark.get("schema"))
.and_then(Value::as_u64),
Some(1)
);
}
#[test] #[test]
fn debug_join_client_serializes_endpoint_from_stdin() { fn debug_join_client_serializes_endpoint_from_stdin() {
let secret = iroh::SecretKey::from_bytes(&[7; 32]); let secret = iroh::SecretKey::from_bytes(&[7; 32]);

View file

@ -3,6 +3,7 @@ extern crate self as mvp_system;
pub mod actors; pub mod actors;
pub mod arena_manager; pub mod arena_manager;
pub mod benchmark_observability;
pub mod bootstrap_datastream; pub mod bootstrap_datastream;
pub mod config; pub mod config;
pub mod dashboard_view; pub mod dashboard_view;

View file

@ -3,11 +3,8 @@
use std::io::{BufRead, BufReader, Write}; use std::io::{BufRead, BufReader, Write};
use std::process::{Child, ChildStdin, Command, Stdio}; use std::process::{Child, ChildStdin, Command, Stdio};
use mvp_system::arena_manager as arena;
use serde_json::{Value, json}; use serde_json::{Value, json};
const HEADER_LEN: usize = 40;
struct WorkerProcess { struct WorkerProcess {
child: Child, child: Child,
stdin: ChildStdin, stdin: ChildStdin,
@ -15,22 +12,17 @@ struct WorkerProcess {
} }
impl WorkerProcess { impl WorkerProcess {
fn spawn(arena: Option<(&arena::ArenaManager, u64)>) -> Self { fn spawn() -> Self {
let script = worker_script(); let script = worker_script();
let mut command = Command::new("python3"); let mut child = Command::new("python3")
command
.arg(&script) .arg(&script)
.env("MVP_TINYGRAD_TEST_MODE", "1")
.env("DEV", "CPU") .env("DEV", "CPU")
.env("MVP_RUN_ID", "9")
.env("MVP_LOGICAL_NODE_ID", "3")
.env("MVP_STAGE_INDEX", "2")
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()); .stderr(Stdio::piped())
if let Some((arena, arena_bytes)) = arena {
command
.env("MVP_ARENA_FD", arena.arena_fd().to_string())
.env("MVP_ARENA_BYTES", arena_bytes.to_string());
}
let mut child = command
.spawn() .spawn()
.unwrap_or_else(|e| panic!("spawn {}: {e}", script.display())); .unwrap_or_else(|e| panic!("spawn {}: {e}", script.display()));
let stdin = child.stdin.take().expect("worker stdin"); let stdin = child.stdin.take().expect("worker stdin");
@ -42,9 +34,10 @@ impl WorkerProcess {
} }
} }
fn send_expect(&mut self, command: Value, expected_type: &str) -> Value { fn send_collect_until(&mut self, command: Value, expected_type: &str) -> Vec<Value> {
writeln!(self.stdin, "{command}").expect("write worker command"); writeln!(self.stdin, "{command}").expect("write worker command");
self.stdin.flush().expect("flush worker command"); self.stdin.flush().expect("flush worker command");
let mut events = Vec::new();
loop { loop {
let mut line = String::new(); let mut line = String::new();
let read = self.stdout.read_line(&mut line).expect("read worker event"); let read = self.stdout.read_line(&mut line).expect("read worker event");
@ -55,8 +48,10 @@ impl WorkerProcess {
actual_type, "WorkerFatal", actual_type, "WorkerFatal",
"worker fatal while waiting for {expected_type}: {event}" "worker fatal while waiting for {expected_type}: {event}"
); );
if actual_type == expected_type { let done = actual_type == expected_type;
return event; events.push(event);
if done {
return events;
} }
} }
} }
@ -70,301 +65,62 @@ impl Drop for WorkerProcess {
} }
#[test] #[test]
fn test_mode_tokenizer_commands_round_trip_prompt_bytes_and_visible_tokens() { fn benchmark_observability_worker_ready_includes_stamps_and_identity() {
let mut worker = WorkerProcess::spawn(None); if !tinygrad_available() {
worker.send_expect( eprintln!("skipping Python worker protocol check: tinygrad is unavailable");
json!({"type":"InitializeWorker","helper_abi_version":1,"backend":{"device":"CPU"}}), return;
"WorkerReady", }
);
worker.send_expect(
json!({
"type":"LoadWeights",
"model_id":"test-model",
"gguf_source":{"LocalPath":"/tmp/not-used-in-test-mode.gguf"},
"tokenizer":{"EmbeddedGguf":{}},
"layer_start":0,
"layer_end_exclusive":1
}),
"WeightsLoaded",
);
let encoded = worker.send_expect( let mut worker = WorkerProcess::spawn();
json!({"type":"EncodePrompt","request_id":7,"prompt":"Hi!"}), let events = worker.send_collect_until(
"PromptEncoded",
);
assert_eq!(encoded.get("request_id").and_then(Value::as_u64), Some(7));
assert_eq!(
encoded
.get("tokens")
.and_then(Value::as_array)
.expect("encoded tokens")
.iter()
.map(|value| value.as_u64().expect("token is u64"))
.collect::<Vec<_>>(),
vec![72, 105, 33]
);
let decoded = worker.send_expect(
json!({"type":"DecodeTokens","request_id":8,"tokens":[72,105,33,6]}),
"TokensDecoded",
);
assert_eq!(decoded.get("request_id").and_then(Value::as_u64), Some(8));
assert_eq!(
decoded.get("text").and_then(Value::as_str),
Some("Hi!<tok:6>")
);
}
#[test]
fn test_mode_worker_executes_single_and_three_stage_mo01_flow_through_real_arena() {
let arena_bytes = 16 * 1024;
let mut arena = arena::ArenaManager::boot(arena::ArenaConfig {
node_id: arena::NodeId(1),
reservation_ceiling: arena_bytes,
base_alignment: 64,
})
.expect("arena boots");
let ingress = lease_ring(&mut arena, 1, 1024, 64);
let egress = lease_ring(&mut arena, 2, 1024, 64);
let mut worker = WorkerProcess::spawn(Some((&arena, arena_bytes)));
worker.send_expect(
json!({"type":"InitializeWorker","helper_abi_version":1,"backend":{"device":"CPU"}}), json!({"type":"InitializeWorker","helper_abi_version":1,"backend":{"device":"CPU"}}),
"WorkerReady", "WorkerReady",
); );
let single_token = run_stage( assert!(
&mut worker, events
&arena,
&ingress,
&egress,
StageFixture {
stage_index: 0,
layer_start: 0,
layer_end_exclusive: 7,
final_stage: true,
},
900,
&[2, 3],
);
assert_eq!(
single_token,
vec![6],
"N=1 stage should emit a token record"
);
let stage0 = run_stage(
&mut worker,
&arena,
&ingress,
&egress,
StageFixture {
stage_index: 0,
layer_start: 0,
layer_end_exclusive: 3,
final_stage: false,
},
901,
&[2, 3],
);
assert_eq!(stage0, vec![8], "stage 0 activation fixture word");
let stage1 = run_stage(
&mut worker,
&arena,
&ingress,
&egress,
StageFixture {
stage_index: 1,
layer_start: 3,
layer_end_exclusive: 5,
final_stage: false,
},
902,
&stage0,
);
assert_eq!(stage1, vec![17], "stage 1 activation fixture word");
let stage2 = run_stage(
&mut worker,
&arena,
&ingress,
&egress,
StageFixture {
stage_index: 2,
layer_start: 5,
layer_end_exclusive: 7,
final_stage: true,
},
903,
&stage1,
);
assert_eq!(stage2, vec![6], "final stage should emit a token record");
}
#[derive(Clone, Copy)]
struct StageFixture {
stage_index: u32,
layer_start: u32,
layer_end_exclusive: u32,
final_stage: bool,
}
fn run_stage(
worker: &mut WorkerProcess,
arena: &arena::ArenaManager,
ingress: &arena::RingLease,
egress: &arena::RingLease,
stage: StageFixture,
output_object_id: u64,
input_words: &[u32],
) -> Vec<u32> {
worker.send_expect(
json!({
"type":"ConfigureRole",
"role_id":1,
"config":{
"run_id":1,
"stage_index":stage.stage_index,
"layer_start":stage.layer_start,
"layer_end_exclusive":stage.layer_end_exclusive
}
}),
"RoleConfigured",
);
worker.send_expect(
json!({
"type":"LoadWeights",
"model_id":"test-model",
"gguf_source":{"LocalPath":"/tmp/not-used-in-test-mode.gguf"},
"tokenizer":{"EmbeddedGguf":{}},
"layer_start":stage.layer_start,
"layer_end_exclusive":stage.layer_end_exclusive
}),
"WeightsLoaded",
);
install_ring(worker, ingress, 1, 10, "ingress", 4096, 4);
install_ring(worker, egress, 2, 11, "egress", 4096, 4);
let input_payload = words_payload(input_words);
arena
.write_arena(
ingress.layout.data_offset,
&object_record(100 + u64::from(stage.stage_index), 0, 0, &input_payload),
)
.expect("write ingress record");
let loaded = worker.send_expect(json!({"type":"RingReadable","ring_id":1}), "ObjectLoaded");
let handle_id = loaded
.get("handle_id")
.and_then(Value::as_u64)
.expect("handle id");
worker.send_expect(
json!({
"type":"ExecuteStep",
"role_id":1,
"step_id":u64::from(stage.stage_index) + 1,
"input_handle_id":handle_id,
"input_object_id":100 + u64::from(stage.stage_index),
"input_sequence":0,
"output_ring_id":2,
"output_object_id":output_object_id,
"output_sequence":0,
"final_stage":stage.final_stage
}),
"StepExecuted",
);
let output = arena
.read_arena(egress.layout.data_offset, HEADER_LEN + 4)
.expect("read egress record");
decode_record_words(&output, output_object_id)
}
fn install_ring(
worker: &mut WorkerProcess,
lease: &arena::RingLease,
ring_id: u64,
edge_id: u64,
direction: &str,
max_extent: u64,
alignment: u32,
) {
worker.send_expect(
json!({
"type":"InstallRing",
"ring_id":ring_id,
"edge_id":edge_id,
"port":direction,
"direction":direction,
"layout":{
"data_offset":lease.layout.data_offset,
"data_bytes":lease.layout.data_bytes
},
"object_spec":{
"max_extent":max_extent,
"alignment":alignment
}
}),
"RingInstalled",
);
}
fn lease_ring(
arena: &mut arena::ArenaManager,
request_id: u64,
data_bytes: u64,
alignment: u64,
) -> arena::RingLease {
let events = arena.request(arena::ArenaRequest::LeaseRing(arena::LeaseRing {
request_id: arena::LeaseRequestId(request_id),
ring_spec: arena::RingSpec {
header_bytes: 64,
data_bytes,
alignment,
},
}));
match events.into_iter().next().expect("arena event") {
arena::ArenaEvent::RingLeased { lease } => lease,
event => panic!("expected ring lease, got {event:?}"),
}
}
fn object_record(object_id: u64, sequence: u64, flags: u32, payload: &[u8]) -> Vec<u8> {
let mut bytes = vec![0_u8; HEADER_LEN];
bytes[0..4].copy_from_slice(b"MO01");
bytes[4..6].copy_from_slice(&1_u16.to_le_bytes());
bytes[6..8].copy_from_slice(&(HEADER_LEN as u16).to_le_bytes());
bytes[8..16].copy_from_slice(&object_id.to_le_bytes());
bytes[16..24].copy_from_slice(&sequence.to_le_bytes());
bytes[24..32].copy_from_slice(&(payload.len() as u64).to_le_bytes());
bytes[32..36].copy_from_slice(&flags.to_le_bytes());
bytes.extend_from_slice(payload);
bytes
}
fn words_payload(words: &[u32]) -> Vec<u8> {
words
.iter() .iter()
.flat_map(|word| word.to_le_bytes()) .any(|event| event.get("type").and_then(Value::as_str) == Some("TinygradImportStarted")),
.collect::<Vec<_>>() "expected tinygrad import milestone in {events:?}"
);
assert!(
events
.iter()
.any(|event| event.get("type").and_then(Value::as_str) == Some("WorkerReady")),
"expected WorkerReady in {events:?}"
);
for event in &events {
assert_eq!(
event
.get("benchmark")
.and_then(|benchmark| benchmark.get("schema"))
.and_then(Value::as_u64),
Some(1),
"{event}"
);
assert_eq!(
event.get("run_id").and_then(Value::as_u64),
Some(9),
"{event}"
);
assert_eq!(
event.get("node_id").and_then(Value::as_u64),
Some(3),
"{event}"
);
assert_eq!(
event.get("stage_index").and_then(Value::as_u64),
Some(2),
"{event}"
);
}
} }
fn decode_record_words(record: &[u8], expected_object_id: u64) -> Vec<u32> { fn tinygrad_available() -> bool {
assert!(record.len() >= HEADER_LEN); Command::new("python3")
assert_eq!(&record[0..4], b"MO01"); .args(["-c", "import tinygrad"])
assert_eq!(u16::from_le_bytes(record[4..6].try_into().unwrap()), 1); .status()
assert_eq!( .is_ok_and(|status| status.success())
u16::from_le_bytes(record[6..8].try_into().unwrap()),
HEADER_LEN as u16
);
assert_eq!(
u64::from_le_bytes(record[8..16].try_into().unwrap()),
expected_object_id
);
let extent = u64::from_le_bytes(record[24..32].try_into().unwrap()) as usize;
assert_eq!(extent % 4, 0);
record[HEADER_LEN..HEADER_LEN + extent]
.chunks_exact(4)
.map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap()))
.collect()
} }
fn worker_script() -> std::path::PathBuf { fn worker_script() -> std::path::PathBuf {

File diff suppressed because it is too large Load diff