feat: better benchmarking

Turn mvp-chat-check into a benchmarking harness with per-stage latency capture, a run envelope, summary artifacts, and a mvp-chat-compare command for delta analysis.

- xtask: add mvp-chat-compare <baseline> <candidate> that checks comparability (schema/scenario/workload/model/provider/pipeline_stages) and prints deltas for total, prepare, standup-to-RPC, and per-prompt roundtrip/first-token/decode/text-decode ms.
- xtask: build_benchmark_summary now writes stdout/stderr/prompts/redacted-config/summary artifacts with per-artifact byte counts and a vastai summary section; rename the dump log to datastream.ndjson.
- xtask: add write_failure_artifacts so failed checks still emit a failure summary with the prompt-corpus blake3 and artifact sizes.
- mvp_chat: add the mvp.chat.benchmark channel and emit_benchmark_envelope (BenchmarkRunEnvelope with model/runtime/provider/workload detail), and tag prompt events with prompt_index and a blake3 prompt_hash.
- tinygrad_worker: add per-phase latency metrics (encode/decode/text-decode/first-token elapsed_ms; stage_execution_ms/record_write_ms on execute_step; ring_readable/encode_prompt/decode_tokens elapsed_ms plus payload sizes).
- vastai_provisioning: emit VastAiLeaseReady and VastAiSshEndpointReady provider lines (contract/offer/host/gpu/dph, ssh host/port/user) for observability.

Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
This commit is contained in:
Zachery Aaron Shores-Chmielewski 2026-07-24 23:05:11 +04:00
parent 5c9b2045cf
commit a5c1c1e68b
6 changed files with 937 additions and 18 deletions

1
Cargo.lock generated
View file

@ -5736,6 +5736,7 @@ dependencies = [
name = "xtask" name = "xtask"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"blake3",
"libc", "libc",
"serde_json", "serde_json",
] ]

View file

@ -751,6 +751,7 @@ def decode_greedy_device_resident(
request_id: int | None, request_id: int | None,
model_id: str | None, model_id: str | None,
progress_every: int, progress_every: int,
decode_started_at: float,
) -> list[int]: ) -> list[int]:
if max_tokens <= 0: if max_tokens <= 0:
return [] return []
@ -797,6 +798,7 @@ def decode_greedy_device_resident(
model_id=model_id, model_id=model_id,
token_index=1, token_index=1,
prompt_tokens=len(prompt_tokens), prompt_tokens=len(prompt_tokens),
first_token_elapsed_ms=int((time.monotonic() - decode_started_at) * 1000),
) )
elif progress_every > 0 and tokens_generated % progress_every == 0: elif progress_every > 0 and tokens_generated % progress_every == 0:
control( control(
@ -836,6 +838,7 @@ def infer_prompt(cmd: dict[str, Any]) -> None:
prompt_chars=len(prompt), prompt_chars=len(prompt),
max_tokens=max_tokens, max_tokens=max_tokens,
) )
encode_started = time.monotonic()
model_prompt, prompt_template = model_prompt_text(prompt) model_prompt, prompt_template = model_prompt_text(prompt)
control( control(
type="PromptEncodeStarted", type="PromptEncodeStarted",
@ -852,8 +855,10 @@ def infer_prompt(cmd: dict[str, Any]) -> None:
model_prompt_bytes=len(model_prompt.encode("utf-8")), model_prompt_bytes=len(model_prompt.encode("utf-8")),
prompt_template=prompt_template, prompt_template=prompt_template,
prompt_tokens=len(prompt_tokens), prompt_tokens=len(prompt_tokens),
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("MVP_TOKEN_PROGRESS_EVERY", "16") or "16")
decode_started = time.monotonic()
control( control(
type="DecodeStarted", type="DecodeStarted",
request_id=request_id, request_id=request_id,
@ -874,6 +879,7 @@ def infer_prompt(cmd: dict[str, Any]) -> None:
request_id=request_id, request_id=request_id,
model_id=loaded.get("model_id"), model_id=loaded.get("model_id"),
progress_every=progress_every, progress_every=progress_every,
decode_started_at=decode_started,
) )
finally: finally:
stop_cpu_line_sampler(cpu_sampler) stop_cpu_line_sampler(cpu_sampler)
@ -883,7 +889,9 @@ def infer_prompt(cmd: dict[str, Any]) -> None:
model_id=loaded.get("model_id"), model_id=loaded.get("model_id"),
prompt_tokens=len(prompt_tokens), prompt_tokens=len(prompt_tokens),
tokens_generated=len(generated), tokens_generated=len(generated),
elapsed_ms=int((time.monotonic() - decode_started) * 1000),
) )
text_decode_started = time.monotonic()
control(type="TextDecodeStarted", request_id=request_id, model_id=loaded.get("model_id"), tokens_generated=len(generated)) control(type="TextDecodeStarted", request_id=request_id, model_id=loaded.get("model_id"), tokens_generated=len(generated))
raw_text = tokenizer.decode(generated) if generated else "" raw_text = tokenizer.decode(generated) if generated else ""
text = strip_chat_stop_markers(raw_text) text = strip_chat_stop_markers(raw_text)
@ -893,6 +901,7 @@ def infer_prompt(cmd: dict[str, Any]) -> None:
model_id=loaded.get("model_id"), model_id=loaded.get("model_id"),
tokens_generated=len(generated), tokens_generated=len(generated),
text_bytes=len(text.encode("utf-8")), text_bytes=len(text.encode("utf-8")),
elapsed_ms=int((time.monotonic() - text_decode_started) * 1000),
) )
control( control(
type="PromptCompleted", type="PromptCompleted",
@ -1039,13 +1048,13 @@ def materialize_object(payload: bytes, sequence: int, flags: int) -> dict[str, A
} }
def ring_readable(cmd: dict[str, Any]) -> None: def ring_readable(cmd: dict[str, Any]) -> None:
global next_handle global next_handle
ring_id = int(cmd["ring_id"]) ring_id = int(cmd["ring_id"])
ring = rings[ring_id] ring = rings[ring_id]
if ring["direction"] != "ingress": if ring["direction"] != "ingress":
fatal("WrongRingDirection", ring_id=ring_id, direction=ring["direction"]) fatal("WrongRingDirection", ring_id=ring_id, direction=ring["direction"])
started = time.monotonic()
object_id, sequence, extent, flags, payload = parse_record(ring) object_id, sequence, extent, flags, payload = parse_record(ring)
handle = next_handle handle = next_handle
next_handle += 1 next_handle += 1
@ -1070,6 +1079,7 @@ def ring_readable(cmd: dict[str, Any]) -> None:
token_count=materialized.get("token_count"), token_count=materialized.get("token_count"),
hidden_dim=materialized.get("hidden_dim"), hidden_dim=materialized.get("hidden_dim"),
start_pos=materialized.get("start_pos"), start_pos=materialized.get("start_pos"),
elapsed_ms=int((time.monotonic() - started) * 1000),
) )
@ -1103,6 +1113,7 @@ def write_record(ring: dict[str, Any], object_id: int, sequence: int, payload: b
def execute_step(cmd: dict[str, Any]) -> None: def execute_step(cmd: dict[str, Any]) -> None:
if not role: if not role:
fatal("RoleNotConfigured") fatal("RoleNotConfigured")
step_started = time.monotonic()
handle = int(cmd["input_handle_id"]) handle = int(cmd["input_handle_id"])
obj = device_objects.get(handle) obj = device_objects.get(handle)
if obj is None: if obj is None:
@ -1148,6 +1159,7 @@ def execute_step(cmd: dict[str, Any]) -> None:
payload = activation.tobytes() payload = activation.tobytes()
output_kind = "activation" output_kind = "activation"
flags = 0 flags = 0
compute_ready = time.monotonic()
committed = write_record( committed = write_record(
ring, ring,
int(cmd["output_object_id"]), int(cmd["output_object_id"]),
@ -1155,6 +1167,7 @@ def execute_step(cmd: dict[str, Any]) -> None:
payload, payload,
flags, flags,
) )
write_ready = time.monotonic()
control( control(
type="StepExecuted", type="StepExecuted",
step_id=int(cmd["step_id"]), step_id=int(cmd["step_id"]),
@ -1169,6 +1182,9 @@ def execute_step(cmd: dict[str, Any]) -> None:
output_kind=output_kind, output_kind=output_kind,
payload_bytes=len(payload), payload_bytes=len(payload),
record_bytes=committed, record_bytes=committed,
stage_execution_ms=int((compute_ready - step_started) * 1000),
record_write_ms=int((write_ready - compute_ready) * 1000),
elapsed_ms=int((write_ready - step_started) * 1000),
) )
@ -1179,22 +1195,39 @@ 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:
started = time.monotonic()
prompt = str(cmd.get("prompt", "")) prompt = str(cmd.get("prompt", ""))
if tokenizer is not None: 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:
model_prompt = prompt
tokens = [int(byte) for byte in prompt.encode("utf-8")] or [0] tokens = [int(byte) for byte in prompt.encode("utf-8")] or [0]
control(type="PromptEncoded", request_id=cmd.get("request_id"), tokens=tokens) control(
type="PromptEncoded",
request_id=cmd.get("request_id"),
tokens=tokens,
prompt_bytes=len(prompt.encode("utf-8")),
model_prompt_bytes=len(model_prompt.encode("utf-8")),
elapsed_ms=int((time.monotonic() - started) * 1000),
)
def decode_tokens(cmd: dict[str, Any]) -> None: def decode_tokens(cmd: dict[str, Any]) -> None:
started = time.monotonic()
tokens = [int(token) for token in cmd.get("tokens", [])] tokens = [int(token) for token in cmd.get("tokens", [])]
if tokenizer is not None: 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)
control(type="TokensDecoded", request_id=cmd.get("request_id"), text=text) control(
type="TokensDecoded",
request_id=cmd.get("request_id"),
text=text,
tokens=len(tokens),
text_bytes=len(text.encode("utf-8")),
elapsed_ms=int((time.monotonic() - started) * 1000),
)
def shutdown_worker(_: dict[str, Any]) -> None: def shutdown_worker(_: dict[str, Any]) -> None:

View file

@ -61,6 +61,7 @@ const CHAT_LIFECYCLE_CHANNEL: &str = "mvp.chat.lifecycle";
const CHAT_RUNTIME_CHANNEL: &str = "mvp.chat.runtime"; const CHAT_RUNTIME_CHANNEL: &str = "mvp.chat.runtime";
const CHAT_PROMPT_CHANNEL: &str = "mvp.chat.prompt"; const CHAT_PROMPT_CHANNEL: &str = "mvp.chat.prompt";
const CHAT_COMPONENT_CHANNEL: &str = "mvp.chat.component"; const CHAT_COMPONENT_CHANNEL: &str = "mvp.chat.component";
const CHAT_BENCHMARK_CHANNEL: &str = "mvp.chat.benchmark";
#[derive(Debug)] #[derive(Debug)]
enum PromptInput { enum PromptInput {
@ -151,6 +152,7 @@ where
"gpu_run": config.gpu_run, "gpu_run": config.gpu_run,
}), }),
); );
progress.emit_benchmark_envelope(&config);
confirm_vastai_if_needed(&config)?; confirm_vastai_if_needed(&config)?;
progress.emit( progress.emit(
CHAT_RUNTIME_CHANNEL, CHAT_RUNTIME_CHANNEL,
@ -380,6 +382,7 @@ impl ChatDatastream {
CHAT_RUNTIME_CHANNEL, CHAT_RUNTIME_CHANNEL,
CHAT_PROMPT_CHANNEL, CHAT_PROMPT_CHANNEL,
CHAT_COMPONENT_CHANNEL, CHAT_COMPONENT_CHANNEL,
CHAT_BENCHMARK_CHANNEL,
] { ] {
out.channel_by_name(name); out.channel_by_name(name);
} }
@ -416,6 +419,68 @@ impl ChatDatastream {
self.flush(); self.flush();
} }
fn emit_benchmark_envelope(&mut self, config: &Config) {
let id = self.channel_by_name(CHAT_BENCHMARK_CHANNEL);
let payload = serde_json::to_vec(&json!({
"type": "BenchmarkRunEnvelope",
"phase": "run_envelope",
"status": "ready",
"run_id": self.run_id,
"benchmark": benchmark_observability::stamp("mvp-chat"),
"detail": {
"scenario": "mvp-chat",
"detail_level": "benchmark_observability_v1",
"workload": {
"mode": "stdin_prompt_corpus",
"max_tokens": config.max_tokens,
"prompt_corpus": "external_or_stdin",
},
"model": {
"id": config.model.id.as_deref(),
"gguf_local_path": config.model.gguf_local_path.as_deref(),
"gguf_repo": config.model.gguf_repo.as_deref(),
"gguf_file": config.model.gguf_file.as_deref(),
"gguf_revision": config.model.gguf_revision.as_deref(),
"tokenizer_local_path": config.model.tokenizer_local_path.as_deref(),
"max_context": config.model.max_context,
},
"runtime": {
"provider": config.provider.as_str(),
"pipeline_stages": config.pipeline_stages,
"orchestrator_launch_mode": config.orchestrator_launch_mode(),
"gpu_run": config.gpu_run,
"relay_mode": config.relay_mode.as_deref(),
"relay_configured": config.relay_url.is_some(),
"endpoint_addr_mask": config.endpoint_addr_mask.as_str(),
},
"provider": {
"kind": config.provider.as_str(),
"node_image": &config.node_image,
"image_tag": config.image_tag.as_deref(),
"cached_model": config.cached_model.as_ref().map(|model| model.host_path.to_string_lossy().to_string()),
"vastai": config.vastai.as_ref().map(|vastai| json!({
"image": &vastai.image,
"relay_configured": !vastai.relay_url.is_empty(),
"bootstrap_command_configured": !vastai.bootstrap_command.is_empty(),
"gpu_name": vastai.gpu_name.as_deref(),
"min_gpu_ram_mb": vastai.min_gpu_ram_mb,
"min_down_mbps": vastai.min_down_mbps,
"min_up_mbps": vastai.min_up_mbps,
"max_dph_total": vastai.max_dph_total,
"min_reliability": vastai.min_reliability,
"require_verified": vastai.require_verified,
"disk_gb": vastai.disk_gb,
"has_onstart": vastai.onstart.is_some(),
"has_ssh_identity": vastai.ssh_identity.is_some(),
})),
},
},
}))
.expect("serialize mvp-chat benchmark envelope");
self.producer.submit_bytes(id, payload);
self.flush();
}
fn flush(&mut self) { fn flush(&mut self) {
let stream = self.stream.clone(); let stream = self.stream.clone();
for frame in self.endpoint.mux().drain() { for frame in self.endpoint.mux().drain() {
@ -1662,6 +1727,10 @@ fn emit_chat_progress(
} }
} }
fn prompt_hash_hex(prompt: &str) -> String {
blake3::hash(prompt.as_bytes()).to_hex().to_string()
}
fn run_chat_session_with_output_and_progress<R, W, O>( fn run_chat_session_with_output_and_progress<R, W, O>(
writer: &mut W, writer: &mut W,
mut reader: R, mut reader: R,
@ -1677,6 +1746,7 @@ where
{ {
let mut progress = progress; let mut progress = progress;
let mut next_request_id = 1_u64; let mut next_request_id = 1_u64;
let mut next_prompt_index = 1_u64;
loop { loop {
if STOP_REQUESTED.load(Ordering::SeqCst) { if STOP_REQUESTED.load(Ordering::SeqCst) {
@ -1694,7 +1764,7 @@ where
CHAT_PROMPT_CHANNEL, CHAT_PROMPT_CHANNEL,
"waiting_for_prompt", "waiting_for_prompt",
"started", "started",
json!({"next_request_id": next_request_id}), json!({"next_request_id": next_request_id, "next_prompt_index": next_prompt_index}),
); );
write!(output, "prompt:> ").map_err(|e| format!("write prompt: {e}"))?; write!(output, "prompt:> ").map_err(|e| format!("write prompt: {e}"))?;
output.flush().map_err(|e| format!("flush prompt: {e}"))?; output.flush().map_err(|e| format!("flush prompt: {e}"))?;
@ -1727,12 +1797,15 @@ where
let request_id = next_request_id; let request_id = next_request_id;
next_request_id = next_request_id.wrapping_add(1).max(1); next_request_id = next_request_id.wrapping_add(1).max(1);
let prompt_index = next_prompt_index;
next_prompt_index = next_prompt_index.wrapping_add(1).max(1);
let prompt_hash = prompt_hash_hex(&prompt);
emit_chat_progress( emit_chat_progress(
&mut progress, &mut progress,
CHAT_PROMPT_CHANNEL, CHAT_PROMPT_CHANNEL,
"prompt_submitted", "prompt_submitted",
"ready", "ready",
json!({"request_id": request_id, "prompt_bytes": prompt.len(), "max_tokens": max_tokens}), json!({"request_id": request_id, "prompt_index": prompt_index, "prompt_hash": &prompt_hash, "prompt_bytes": prompt.len(), "max_tokens": max_tokens}),
); );
write_json_line( write_json_line(
writer, writer,
@ -1748,7 +1821,7 @@ where
CHAT_PROMPT_CHANNEL, CHAT_PROMPT_CHANNEL,
"decoding", "decoding",
"started", "started",
json!({"request_id": request_id}), json!({"request_id": request_id, "prompt_index": prompt_index, "prompt_hash": &prompt_hash}),
); );
let mut response_started = false; let mut response_started = false;
@ -1829,7 +1902,7 @@ where
CHAT_PROMPT_CHANNEL, CHAT_PROMPT_CHANNEL,
"response_text", "response_text",
"observed", "observed",
json!({"request_id": request_id, "text_bytes": text.len()}), json!({"request_id": request_id, "prompt_index": prompt_index, "prompt_hash": &prompt_hash, "text_bytes": text.len()}),
); );
} }
PromptEvent::Done { PromptEvent::Done {
@ -1851,6 +1924,8 @@ where
"ready", "ready",
json!({ json!({
"request_id": request_id, "request_id": request_id,
"prompt_index": prompt_index,
"prompt_hash": &prompt_hash,
"response_started": response_started, "response_started": response_started,
"tokens_generated": tokens_generated, "tokens_generated": tokens_generated,
"elapsed_ms": elapsed_ms, "elapsed_ms": elapsed_ms,
@ -1867,7 +1942,7 @@ where
CHAT_PROMPT_CHANNEL, CHAT_PROMPT_CHANNEL,
"request_faulted", "request_faulted",
"ready", "ready",
json!({"request_id": request_id, "error": error}), json!({"request_id": request_id, "prompt_index": prompt_index, "prompt_hash": &prompt_hash, "error": error}),
); );
break; break;
} }

View file

@ -797,6 +797,24 @@ where
instance.contract_id instance.contract_id
), ),
}); });
sink.observe(PluginObservation::ProviderLine {
run_id: spec.run_id,
node_id: spec.node_id,
line: serde_json::json!({
"type": "VastAiLeaseReady",
"run_id": spec.run_id,
"node_id": spec.node_id,
"label": &label,
"image": &spec.image,
"contract_id": instance.contract_id,
"offer_id": instance.offer_id,
"host_id": instance.host_id,
"gpu_name": &instance.gpu_name,
"gpu_ram": instance.gpu_ram,
"dph_total": instance.dph_total,
})
.to_string(),
});
let endpoint = match self.client.ssh_endpoint( let endpoint = match self.client.ssh_endpoint(
instance.contract_id, instance.contract_id,
@ -812,6 +830,20 @@ where
)); ));
} }
}; };
sink.observe(PluginObservation::ProviderLine {
run_id: spec.run_id,
node_id: spec.node_id,
line: serde_json::json!({
"type": "VastAiSshEndpointReady",
"run_id": spec.run_id,
"node_id": spec.node_id,
"contract_id": instance.contract_id,
"host": &endpoint.host,
"port": endpoint.port,
"user": &endpoint.user,
})
.to_string(),
});
let bootstrap = match self.bootstrap.start_bootstrap( let bootstrap = match self.bootstrap.start_bootstrap(
spec.clone(), spec.clone(),

View file

@ -5,6 +5,7 @@ edition = "2024"
[dependencies] [dependencies]
serde_json = "1" serde_json = "1"
blake3 = "1"
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
libc = "0.2" libc = "0.2"

View file

@ -27,6 +27,11 @@ const DATA_PATH_MIN_PAYLOAD_BYTES: u64 = 512;
struct MvpChatCheckPaths { struct MvpChatCheckPaths {
root: PathBuf, root: PathBuf,
dump_log: PathBuf, dump_log: PathBuf,
stdout: PathBuf,
stderr: PathBuf,
prompts: PathBuf,
redacted_config: PathBuf,
summary: PathBuf,
} }
struct MvpChatCheckOutput { struct MvpChatCheckOutput {
@ -185,7 +190,9 @@ USAGE: cargo xtask <command>
COMMANDS: COMMANDS:
mvp-chat [--gpu] [--process|--docker|--vastai] [--pipeline-stages n] [--cached-model] [-- args...] Run the human chat wrapper against the real orchestrator/worker bins. mvp-chat [--gpu] [--process|--docker|--vastai] [--pipeline-stages n] [--cached-model] [-- args...] Run the human chat wrapper against the real orchestrator/worker bins.
mvp-chat-check [--gpu|--multinode|--multinode-docker|--vastai] mvp-chat-check [--gpu|--multinode|--multinode-docker|--vastai]
Run real cargo mvp-chat acceptance check for one explicit scenario. Run real cargo mvp-chat acceptance check and write benchmark artifacts.
mvp-chat-compare <baseline-summary.json> <candidate-summary.json>
Compare two benchmark summaries and report comparable deltas.
test Run the basic non-binding test barrier: root crate plus each test Run the basic non-binding test barrier: root crate plus each
non-binding repository package with `cargo test -p`." non-binding repository package with `cargo test -p`."
); );
@ -319,6 +326,155 @@ fn run_mvp_chat(args: Vec<String>) -> ExitCode {
} }
} }
fn run_mvp_chat_compare(args: Vec<String>) -> ExitCode {
if args.len() != 2 {
eprintln!(
"USAGE: cargo xtask mvp-chat-compare <baseline-summary.json> <candidate-summary.json>"
);
return ExitCode::from(1);
}
let baseline = match read_summary_json(Path::new(&args[0])) {
Ok(value) => value,
Err(error) => {
eprintln!("{error}");
return ExitCode::from(1);
}
};
let candidate = match read_summary_json(Path::new(&args[1])) {
Ok(value) => value,
Err(error) => {
eprintln!("{error}");
return ExitCode::from(1);
}
};
let comparable = summaries_comparable(&baseline, &candidate);
println!("mvp-chat-compare: comparable={comparable}");
for reason in summary_incomparability_reasons(&baseline, &candidate) {
println!("mvp-chat-compare: incomparable {reason}");
}
print_summary_metric_delta(
"total_child_ms",
summary_pointer_u64(&baseline, "/timings/total_child_ms"),
summary_pointer_u64(&candidate, "/timings/total_child_ms"),
);
print_summary_metric_delta(
"prepare_runtime_ms",
summary_pointer_u64(&baseline, "/timings/prepare_runtime_ms/value_ms"),
summary_pointer_u64(&candidate, "/timings/prepare_runtime_ms/value_ms"),
);
print_summary_metric_delta(
"standup_to_prompt_rpc_ms",
summary_pointer_u64(&baseline, "/timings/standup_to_prompt_rpc_ms/value_ms"),
summary_pointer_u64(&candidate, "/timings/standup_to_prompt_rpc_ms/value_ms"),
);
for request_id in 1..=2 {
for metric in [
"roundtrip_ms",
"first_token_ms",
"decode_ms",
"text_decode_ms",
] {
print_summary_metric_delta(
&format!("prompt_{request_id}_{metric}"),
summary_prompt_metric(&baseline, request_id, metric),
summary_prompt_metric(&candidate, request_id, metric),
);
}
}
if comparable {
ExitCode::SUCCESS
} else {
ExitCode::from(2)
}
}
fn read_summary_json(path: &Path) -> Result<Value, String> {
let content = fs::read_to_string(path)
.map_err(|e| format!("mvp-chat-compare: read summary {}: {e}", path.display()))?;
serde_json::from_str(&content)
.map_err(|e| format!("mvp-chat-compare: parse summary {}: {e}", path.display()))
}
fn summaries_comparable(baseline: &Value, candidate: &Value) -> bool {
summary_incomparability_reasons(baseline, candidate).is_empty()
}
fn summary_incomparability_reasons(baseline: &Value, candidate: &Value) -> Vec<String> {
let mut reasons = Vec::new();
for (label, pointer) in [
("schema", "/schema"),
("scenario", "/scenario"),
("workload", "/workload/prompt_corpus_blake3"),
("model", "/run_envelope/detail/model/id"),
("provider", "/run_envelope/detail/provider/kind"),
(
"pipeline_stages",
"/run_envelope/detail/runtime/pipeline_stages",
),
("gpu_run", "/run_envelope/detail/runtime/gpu_run"),
("node_image", "/run_envelope/detail/provider/node_image"),
] {
let left = baseline.pointer(pointer);
let right = candidate.pointer(pointer);
if left != right {
reasons.push(format!(
"{label} baseline={} candidate={}",
render_summary_value(left),
render_summary_value(right)
));
}
}
reasons
}
fn print_summary_metric_delta(name: &str, baseline: Option<u64>, candidate: Option<u64>) {
match (baseline, candidate) {
(Some(left), Some(right)) => {
let delta = right as i128 - left as i128;
let pct = if left == 0 {
"unavailable".to_owned()
} else {
format!("{:.2}", (delta as f64 / left as f64) * 100.0)
};
println!(
"mvp-chat-compare: {name} baseline={left} candidate={right} delta_ms={delta} delta_pct={pct}"
);
}
_ => println!(
"mvp-chat-compare: {name} baseline={} candidate={} delta_ms=unavailable",
baseline
.map(|value| value.to_string())
.unwrap_or_else(|| "unavailable".to_owned()),
candidate
.map(|value| value.to_string())
.unwrap_or_else(|| "unavailable".to_owned())
),
}
}
fn summary_prompt_metric(summary: &Value, request_id: u64, metric: &str) -> Option<u64> {
summary
.pointer("/timings/prompts")
.and_then(Value::as_array)?
.iter()
.find(|prompt| prompt.get("request_id").and_then(Value::as_u64) == Some(request_id))?
.get(metric)?
.get("value_ms")
.and_then(Value::as_u64)
}
fn summary_pointer_u64(summary: &Value, pointer: &str) -> Option<u64> {
summary.pointer(pointer).and_then(Value::as_u64)
}
fn render_summary_value(value: Option<&Value>) -> String {
match value {
Some(Value::String(value)) => value.clone(),
Some(Value::Null) | None => "unavailable".to_owned(),
Some(value) => value.to_string(),
}
}
fn explicit_dump_log_path_from_mvp_chat_args(args: &[String]) -> Option<PathBuf> { fn explicit_dump_log_path_from_mvp_chat_args(args: &[String]) -> Option<PathBuf> {
let args = strip_leading_double_dash(args); let args = strip_leading_double_dash(args);
let mut index = 0; let mut index = 0;
@ -482,7 +638,7 @@ fn write_mvp_chat_check_paths(root: &Path) -> Result<MvpChatCheckPaths, String>
root.display() root.display()
)); ));
} }
let dump_log = root.join("mvp-chat.ndjson"); let dump_log = root.join("datastream.ndjson");
if dump_log.exists() { if dump_log.exists() {
return Err(format!( return Err(format!(
"mvp-chat-check: dump log path already exists: {}", "mvp-chat-check: dump log path already exists: {}",
@ -492,6 +648,11 @@ fn write_mvp_chat_check_paths(root: &Path) -> Result<MvpChatCheckPaths, String>
Ok(MvpChatCheckPaths { Ok(MvpChatCheckPaths {
root: root.to_path_buf(), root: root.to_path_buf(),
dump_log, dump_log,
stdout: root.join("stdout.txt"),
stderr: root.join("stderr.txt"),
prompts: root.join("prompts.txt"),
redacted_config: root.join("redacted-config.json"),
summary: root.join("summary.json"),
}) })
} }
@ -590,17 +751,33 @@ fn run_mvp_chat_check(args: Vec<String>) -> ExitCode {
); );
} }
}; };
let summary =
match build_benchmark_summary(&events, output.child_elapsed_ms, run_id, scenario, &paths) {
Ok(summary) => summary,
Err(error) => {
return fail_mvp_chat_check(
&error,
&paths,
&output.stdout,
&output.stderr,
Some(&output.status),
);
}
};
if let Err(error) = write_benchmark_artifacts(&paths, run_id, scenario, &output, &summary) {
return fail_mvp_chat_check(
&error,
&paths,
&output.stdout,
&output.stderr,
Some(&output.status),
);
}
for line in &report.lines { for line in &report.lines {
println!("{line}"); println!("{line}");
} }
println!("mvp-chat-check: artifacts {}", paths.root.display());
if let Err(error) = fs::remove_dir_all(&paths.root) { println!("mvp-chat-check: summary {}", paths.summary.display());
eprintln!(
"mvp-chat-check: remove temp directory {}: {error}",
paths.root.display()
);
return ExitCode::from(1);
}
println!("mvp-chat-check: ok"); println!("mvp-chat-check: ok");
for (index, response) in responses.iter().enumerate() { for (index, response) in responses.iter().enumerate() {
@ -1569,6 +1746,527 @@ fn duration_ms_u64(duration: Duration) -> u64 {
u64::try_from(duration.as_millis()).unwrap_or(u64::MAX) u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
} }
fn build_benchmark_summary(
events: &[DumpLogEvent],
child_elapsed_ms: u64,
run_id: u64,
scenario: MvpChatCheckScenario,
paths: &MvpChatCheckPaths,
) -> Result<Value, String> {
let facts = BenchmarkFacts::from_events(events, run_id);
let mut dump_facts = DumpLogFacts::default();
for record in events {
record_dump_log_event(&record.channel, &record.event, &mut dump_facts)?;
}
let run_envelope = benchmark_run_envelope(events, run_id);
let channel_counts = benchmark_channel_counts(events);
let event_counts = benchmark_event_counts(events);
let prompt_summaries = facts
.prompts
.values()
.map(prompt_summary_json)
.collect::<Vec<_>>();
let summary = json!({
"schema": "swactor.mvp_chat.benchmark_summary.v1",
"source": "datastream",
"run_id": run_id,
"scenario": scenario.name(),
"created_unix_ms": unix_ms_now(),
"workload": {
"name": "mvp-chat-check",
"input_format": "stdin_prompt_corpus",
"prompt_count": 2,
"prompt_bytes": MVP_CHAT_CHECK_PROMPTS.len(),
"prompt_corpus_blake3": bytes_blake3_hex(MVP_CHAT_CHECK_PROMPTS),
"prompts": prompt_workload_summary(events, run_id),
},
"artifacts": {
"root": paths.root.display().to_string(),
"datastream": {
"path": paths.dump_log.display().to_string(),
"blake3": file_blake3_hex(&paths.dump_log)?,
},
"stdout": {
"path": paths.stdout.display().to_string(),
},
"stderr": {
"path": paths.stderr.display().to_string(),
},
"prompts": {
"path": paths.prompts.display().to_string(),
"blake3": bytes_blake3_hex(MVP_CHAT_CHECK_PROMPTS),
},
"redacted_config": {
"path": paths.redacted_config.display().to_string(),
},
"summary": {
"path": paths.summary.display().to_string(),
},
},
"run_envelope": run_envelope,
"event_counts": {
"total": events.len(),
"channels": channel_counts,
"types": event_counts,
},
"timings": {
"total_child_ms": child_elapsed_ms,
"cargo_run_mvp_chat_ms": benchmark_span_json(&facts, "mvp.xtask.benchmark", "XtaskBenchmark", "cargo_run_mvp_chat", "started", "ready"),
"prepare_runtime_ms": benchmark_span_json(&facts, "mvp.chat.runtime", "ChatProgress", "prepare_runtime", "started", "ready"),
"standup_to_weights_loaded_ms": duration_summary_json(duration_between(
facts.span_point("mvp.chat.runtime", "ChatProgress", "prepare_runtime", "ready"),
facts.span_point("mvp.orch.bootstrap", "OrchBootstrap", "weights_loaded", "ready"),
)),
"standup_to_prompt_rpc_ms": duration_summary_json(duration_between(
facts.span_point("mvp.chat.runtime", "ChatProgress", "prepare_runtime", "ready"),
facts.span_point("mvp.chat.runtime", "ChatProgress", "prompt_rpc", "ready"),
)),
"prompts": prompt_summaries,
},
"pipeline": pipeline_summary_json(events, &dump_facts),
"gpu": gpu_summary_json(events, &dump_facts),
"invariants": benchmark_invariants_json(&dump_facts, scenario),
"legacy_tolerance": legacy_tolerance_summary(events),
});
Ok(summary)
}
fn write_benchmark_artifacts(
paths: &MvpChatCheckPaths,
run_id: u64,
scenario: MvpChatCheckScenario,
output: &MvpChatCheckOutput,
summary: &Value,
) -> Result<(), String> {
fs::write(&paths.stdout, &output.stdout).map_err(|e| {
format!(
"mvp-chat-check: write stdout artifact {}: {e}",
paths.stdout.display()
)
})?;
fs::write(&paths.stderr, &output.stderr).map_err(|e| {
format!(
"mvp-chat-check: write stderr artifact {}: {e}",
paths.stderr.display()
)
})?;
fs::write(&paths.prompts, MVP_CHAT_CHECK_PROMPTS).map_err(|e| {
format!(
"mvp-chat-check: write prompt corpus {}: {e}",
paths.prompts.display()
)
})?;
let redacted_config = benchmark_redacted_config(summary, run_id, scenario);
write_json_file(&paths.redacted_config, &redacted_config)?;
write_json_file(&paths.summary, summary)?;
Ok(())
}
fn write_json_file(path: &Path, value: &Value) -> Result<(), String> {
let mut bytes = serde_json::to_vec_pretty(value)
.map_err(|e| format!("serialize benchmark artifact {}: {e}", path.display()))?;
bytes.push(b'\n');
fs::write(path, bytes).map_err(|e| format!("write benchmark artifact {}: {e}", path.display()))
}
fn benchmark_redacted_config(
summary: &Value,
run_id: u64,
scenario: MvpChatCheckScenario,
) -> Value {
let mut config = summary
.get("run_envelope")
.and_then(|value| value.get("detail"))
.cloned()
.unwrap_or_else(|| json!({}));
redact_sensitive_values(&mut config);
json!({
"schema": "swactor.mvp_chat.redacted_config.v1",
"run_id": run_id,
"scenario": scenario.name(),
"detail": config,
})
}
fn redact_sensitive_values(value: &mut Value) {
match value {
Value::Object(object) => {
for (key, child) in object.iter_mut() {
let key_lower = key.to_ascii_lowercase();
if child.is_string()
&& (key_lower.contains("api_key")
|| key_lower.contains("token")
|| key_lower.contains("secret")
|| key_lower.contains("password")
|| key_lower.contains("ssh_identity")
|| key_lower.contains("private_key")
|| key_lower.contains("bootstrap_command"))
{
*child = Value::String("<redacted>".to_owned());
} else {
redact_sensitive_values(child);
}
}
}
Value::Array(items) => {
for child in items {
redact_sensitive_values(child);
}
}
_ => {}
}
}
fn benchmark_run_envelope(events: &[DumpLogEvent], run_id: u64) -> Value {
events
.iter()
.find(|record| {
event_matches_run_id(&record.event, run_id)
&& record.event.get("type").and_then(Value::as_str) == Some("BenchmarkRunEnvelope")
})
.map(|record| record.event.clone())
.unwrap_or_else(|| {
json!({
"type": "BenchmarkRunEnvelope",
"status": "unavailable",
"detail": {
"reason": "event not present in datastream",
},
})
})
}
fn prompt_workload_summary(events: &[DumpLogEvent], run_id: u64) -> Vec<Value> {
let mut prompts = BTreeMap::new();
for record in events {
if !event_matches_run_id(&record.event, run_id)
|| record.channel != "mvp.chat.prompt"
|| record.event.get("type").and_then(Value::as_str) != Some("ChatProgress")
|| record.event.get("phase").and_then(Value::as_str) != Some("prompt_submitted")
|| record.event.get("status").and_then(Value::as_str) != Some("ready")
{
continue;
}
let Some(request_id) = dump_log_request_id(&record.event) else {
continue;
};
let detail = record.event.get("detail").unwrap_or(&Value::Null);
prompts.insert(
request_id,
json!({
"request_id": request_id,
"prompt_index": detail.get("prompt_index").and_then(Value::as_u64),
"prompt_hash": detail.get("prompt_hash").and_then(Value::as_str),
"prompt_bytes": detail.get("prompt_bytes").and_then(Value::as_u64),
"max_tokens": detail.get("max_tokens").and_then(Value::as_u64),
}),
);
}
prompts.into_values().collect()
}
fn prompt_summary_json(prompt: &PromptBenchmarkFacts) -> Value {
let roundtrip = duration_between(
prompt.chat_submitted.as_ref(),
prompt.chat_completed.as_ref(),
);
let worker_start = prompt
.worker_started
.as_ref()
.or(prompt.encode_started.as_ref());
let worker_end = prompt
.worker_completed
.as_ref()
.or(prompt.chat_completed.as_ref());
let worker_total = duration_between(worker_start, worker_end);
let encode = duration_between(prompt.encode_started.as_ref(), prompt.encode_ready.as_ref());
let first_token = duration_between(
prompt.decode_started.as_ref(),
prompt.first_token_ready.as_ref(),
);
let decode = duration_between(prompt.decode_started.as_ref(), prompt.decode_ready.as_ref());
let text_decode = duration_between(
prompt.text_decode_started.as_ref(),
prompt.text_decode_ready.as_ref(),
);
json!({
"request_id": prompt.request_id,
"roundtrip_ms": duration_summary_json(roundtrip),
"worker_total_ms": duration_summary_json(worker_total),
"tokenization_ms": duration_summary_json(encode),
"first_token_ms": duration_summary_json(first_token),
"decode_ms": duration_summary_json(decode),
"text_decode_ms": duration_summary_json(text_decode),
"tokens_generated": prompt.tokens_generated,
"tokens_per_sec": tokens_per_sec(prompt.tokens_generated, decode.value_ms),
})
}
fn benchmark_span_json(
facts: &BenchmarkFacts,
channel: &str,
event_type: &str,
phase: &str,
start_status: &str,
end_status: &str,
) -> Value {
duration_summary_json(duration_between(
facts.span_point(channel, event_type, phase, start_status),
facts.span_point(channel, event_type, phase, end_status),
))
}
fn duration_summary_json(duration: DurationRender) -> Value {
json!({
"value_ms": duration.value_ms,
"clock_skew": duration.clock_skew,
})
}
fn pipeline_summary_json(events: &[DumpLogEvent], facts: &DumpLogFacts) -> Value {
let mut worker_steps = BenchmarkAggregate::default();
let mut object_loads = BenchmarkAggregate::default();
let mut ring_installs = BTreeMap::<String, u64>::new();
for record in events {
match (
record.channel.as_str(),
record.event.get("type").and_then(Value::as_str),
) {
("mvp.worker.step", Some("StepExecuted")) => {
worker_steps.count += 1;
worker_steps.elapsed_ms += event_u64(&record.event, "elapsed_ms").unwrap_or(0);
worker_steps.stage_execution_ms +=
event_u64(&record.event, "stage_execution_ms").unwrap_or(0);
worker_steps.record_write_ms +=
event_u64(&record.event, "record_write_ms").unwrap_or(0);
worker_steps.payload_bytes +=
event_u64(&record.event, "payload_bytes").unwrap_or(0);
worker_steps.record_bytes += event_u64(&record.event, "record_bytes").unwrap_or(0);
}
("mvp.worker.ingress", Some("ObjectLoaded")) => {
object_loads.count += 1;
object_loads.elapsed_ms += event_u64(&record.event, "elapsed_ms").unwrap_or(0);
object_loads.record_bytes += event_u64(&record.event, "extent").unwrap_or(0);
}
("mvp.worker.ring", Some("RingInstalled")) => {
let direction = record
.event
.get("direction")
.and_then(Value::as_str)
.unwrap_or("unavailable")
.to_owned();
*ring_installs.entry(direction).or_default() += 1;
}
_ => {}
}
}
json!({
"worker_steps": worker_steps.to_json(),
"object_loads": object_loads.to_json(),
"ring_installs": ring_installs,
"data_path": {
"activation_object_loaded": facts.activation_object_loaded,
"activation_step_executed": facts.activation_step_executed,
"activation_egress_ring_read": facts.activation_egress_ring_read,
"activation_ingress_ring_write": facts.activation_ingress_ring_write,
"activation_iroh_edge_sent": facts.activation_iroh_edge_sent,
"activation_iroh_edge_read": facts.activation_iroh_edge_read,
"activation_interstage_handoff": facts.activation_interstage_handoff,
"activation_edge_ids": facts.activation_edge_ids,
"max_activation_record_bytes": facts.max_activation_record_bytes,
"max_worker_command_bytes": facts.max_worker_command_bytes,
"control_json_large_object_violation": facts.max_activation_record_bytes >= DATA_PATH_MIN_PAYLOAD_BYTES
&& facts.max_worker_command_bytes >= facts.max_activation_record_bytes,
},
})
}
fn gpu_summary_json(events: &[DumpLogEvent], facts: &DumpLogFacts) -> Value {
let mut cpu_profile_summaries = Vec::new();
for record in events {
if record.event.get("type").and_then(Value::as_str) == Some("CpuLineProfileSummary") {
cpu_profile_summaries.push(record.event.clone());
}
}
json!({
"worker_device_requested": facts.gpu_worker_device_requested,
"import_ready": facts.gpu_import_ready,
"probe_ready": facts.gpu_probe_ready,
"worker_ready": facts.gpu_worker_ready,
"cpu_fallback_seen": facts.gpu_cpu_fallback_seen,
"decode_started_request_ids": facts.gpu_decode_started,
"first_token_request_ids": facts.gpu_first_token_ready,
"decode_ready_request_ids": facts.gpu_decode_ready,
"prompt_completed_request_ids": facts.gpu_prompt_completed,
"pipeline_real_worker_step_seen": facts.gpu_pipeline_real_worker_step_seen,
"cpu_profile_summaries": cpu_profile_summaries,
})
}
#[derive(Default)]
struct BenchmarkAggregate {
count: u64,
elapsed_ms: u64,
stage_execution_ms: u64,
record_write_ms: u64,
payload_bytes: u64,
record_bytes: u64,
}
impl BenchmarkAggregate {
fn to_json(&self) -> Value {
json!({
"count": self.count,
"elapsed_ms_sum": self.elapsed_ms,
"stage_execution_ms_sum": self.stage_execution_ms,
"record_write_ms_sum": self.record_write_ms,
"payload_bytes_sum": self.payload_bytes,
"record_bytes_sum": self.record_bytes,
})
}
}
fn benchmark_invariants_json(facts: &DumpLogFacts, scenario: MvpChatCheckScenario) -> Vec<Value> {
let mut invariants = vec![
invariant_json("chat_config_ready", facts.chat_config_ready),
invariant_json("prepare_runtime_ready", facts.prepare_runtime_ready),
invariant_json("prompt_rpc_ready", facts.prompt_rpc_ready),
invariant_json(
"orchestrator_weights_loaded",
facts.orch_weights_loaded_ready,
),
invariant_json(
"two_prompt_responses",
facts.response_text_1 && facts.response_text_2,
),
invariant_json(
"two_prompt_completions",
facts.request_completed_1 && facts.request_completed_2,
),
invariant_json("shutdown_requested", facts.shutdown_requested),
invariant_json("orchestrator_stopped", facts.orchestrator_stopped),
];
if matches!(
scenario,
MvpChatCheckScenario::Gpu | MvpChatCheckScenario::VastAi
) {
invariants.extend([
invariant_json("gpu_no_cpu_fallback", !facts.gpu_cpu_fallback_seen),
invariant_json("gpu_worker_ready", facts.gpu_worker_ready),
]);
}
if matches!(
scenario,
MvpChatCheckScenario::MultinodeDocker | MvpChatCheckScenario::VastAi
) {
invariants.extend([
invariant_json(
"activation_large_object_loaded",
facts.activation_object_loaded,
),
invariant_json(
"activation_large_object_step_executed",
facts.activation_step_executed,
),
invariant_json(
"activation_large_object_iroh_sent",
facts.activation_iroh_edge_sent,
),
invariant_json(
"activation_large_object_iroh_read",
facts.activation_iroh_edge_read,
),
]);
}
invariants
}
fn invariant_json(name: &str, passed: bool) -> Value {
json!({
"name": name,
"passed": passed,
})
}
fn legacy_tolerance_summary(events: &[DumpLogEvent]) -> Value {
let missing_benchmark_stamp = events
.iter()
.filter(|record| record.event.get("benchmark").is_none())
.count();
let missing_elapsed_fields = events
.iter()
.filter(|record| {
matches!(
record.event.get("type").and_then(Value::as_str),
Some(
"PromptEncodeReady"
| "DecodeReady"
| "TextDecodeReady"
| "StepExecuted"
| "ObjectLoaded"
| "PromptEncoded"
| "TokensDecoded"
)
) && record.event.get("elapsed_ms").is_none()
})
.count();
json!({
"accepted": true,
"missing_benchmark_stamp_events": missing_benchmark_stamp,
"missing_elapsed_field_events": missing_elapsed_fields,
"unavailable_fields_are_null": true,
})
}
fn benchmark_channel_counts(events: &[DumpLogEvent]) -> BTreeMap<String, u64> {
let mut counts = BTreeMap::new();
for record in events {
*counts.entry(record.channel.clone()).or_default() += 1;
}
counts
}
fn benchmark_event_counts(events: &[DumpLogEvent]) -> BTreeMap<String, u64> {
let mut counts = BTreeMap::new();
for record in events {
let key = format!(
"{}/{}/{}/{}",
record.channel,
record
.event
.get("type")
.and_then(Value::as_str)
.unwrap_or("<missing>"),
record
.event
.get("phase")
.and_then(Value::as_str)
.unwrap_or("<none>"),
record
.event
.get("status")
.and_then(Value::as_str)
.unwrap_or("<none>")
);
*counts.entry(key).or_default() += 1;
}
counts
}
fn event_u64(event: &Value, key: &str) -> Option<u64> {
event.get(key).and_then(Value::as_u64)
}
fn file_blake3_hex(path: &Path) -> Result<String, String> {
let bytes =
fs::read(path).map_err(|e| format!("read artifact for hash {}: {e}", path.display()))?;
Ok(bytes_blake3_hex(&bytes))
}
fn bytes_blake3_hex(bytes: &[u8]) -> String {
blake3::hash(bytes).to_hex().to_string()
}
fn event_matches_run_id(event: &Value, run_id: u64) -> bool { fn event_matches_run_id(event: &Value, run_id: u64) -> bool {
event event
.get("run_id") .get("run_id")
@ -2455,6 +3153,10 @@ mod tests {
"run_id": 9, "run_id": 9,
"detail": { "detail": {
"request_id": request_id, "request_id": request_id,
"prompt_index": request_id,
"prompt_hash": format!("hash-{request_id}"),
"prompt_bytes": 4,
"max_tokens": 8,
"tokens_generated": 3, "tokens_generated": 3,
"elapsed_ms": 50, "elapsed_ms": 50,
"final_text_bytes": 5, "final_text_bytes": 5,
@ -3286,6 +3988,80 @@ mod tests {
&& line.contains("decode_ms=8") && line.contains("decode_ms=8")
})); }));
} }
#[test]
fn benchmark_summary_preserves_workload_artifacts_and_prompt_timings() {
let mut event_pairs = benchmark_report_events(true);
event_pairs.push((
"mvp.chat.benchmark",
stamped(
json!({
"type": "BenchmarkRunEnvelope",
"phase": "run_envelope",
"status": "ready",
"run_id": 9,
"detail": {
"model": {"id": "unit-model"},
"runtime": {"pipeline_stages": 1, "gpu_run": false},
"provider": {"kind": "process", "node_image": "unit-image"},
},
}),
"mvp-chat",
1_001,
1,
),
));
let path = write_synthetic_event_dump("summary-contract", event_pairs);
let events = parse_dump_log_events(&path).expect("parse summary events");
let paths = MvpChatCheckPaths {
root: std::env::temp_dir(),
dump_log: path.clone(),
stdout: temp_path("summary-stdout"),
stderr: temp_path("summary-stderr"),
prompts: temp_path("summary-prompts"),
redacted_config: temp_path("summary-config"),
summary: temp_path("summary-json"),
};
let summary = build_benchmark_summary(
&events,
80,
9,
MvpChatCheckScenario::ProcessBaseline,
&paths,
)
.expect("summary builds");
let _ = fs::remove_file(path);
assert_eq!(
summary.get("source").and_then(Value::as_str),
Some("datastream")
);
assert_eq!(
summary
.pointer("/run_envelope/detail/model/id")
.and_then(Value::as_str),
Some("unit-model")
);
assert_eq!(
summary
.pointer("/workload/prompts/0/prompt_hash")
.and_then(Value::as_str),
Some("hash-1")
);
assert_eq!(
summary
.pointer("/timings/prompts/0/first_token_ms/value_ms")
.and_then(Value::as_u64),
Some(5)
);
assert!(
summary
.pointer("/artifacts/datastream/blake3")
.and_then(Value::as_str)
.is_some()
);
}
} }
fn main() -> ExitCode { fn main() -> ExitCode {
@ -3293,6 +4069,7 @@ fn main() -> ExitCode {
match args.next().as_deref() { match args.next().as_deref() {
Some("test") if args.next().is_none() => run_tests(), Some("test") if args.next().is_none() => run_tests(),
Some("mvp-chat-check") => run_mvp_chat_check(args.collect()), Some("mvp-chat-check") => run_mvp_chat_check(args.collect()),
Some("mvp-chat-compare") => run_mvp_chat_compare(args.collect()),
Some("mvp-chat") => run_mvp_chat(args.collect()), Some("mvp-chat") => run_mvp_chat(args.collect()),
Some("help" | "--help" | "-h") | None => { Some("help" | "--help" | "-h") | None => {
print_usage(); print_usage();