Add window-scoped session closure fallback
This commit is contained in:
@@ -200,3 +200,16 @@ diverge,因此做 ragged verifier”这一表述太宽,不能作为实现前
|
||||
queue policy。若全部通过,最小原型的边界也只应是 verifier-side compaction:当前
|
||||
EAGLE 仍按 Kmax 产生 candidate,短 `k_i` 不会自动消除 draft-side work,不能把 verifier
|
||||
节省误报为完整 draft+verify speedup。
|
||||
|
||||
### Trace 数据可用性与 window-closure fallback
|
||||
|
||||
本轮发现 `prepare_trace_windows.py` 依赖的 2026-03-27 原始格式化 trace span 在 dash0
|
||||
已不存在,不能把它的 streaming full-session root 当作已经复验。现有的完整 600 秒
|
||||
materialized window 仍含 prompt、`chat_id` 与 `parent_chat_id`,因此新增的 fallback 仅将
|
||||
窗口内图的 connected component 作为选择原子:保留 384/384 条窗口内 parent edge,且把
|
||||
同一个窗口外 parent 的 sibling 归为同一 component。该窗口有 15,479 requests、15,095
|
||||
components;414 条 non-root edge 中 30 条(7.25%)父节点落在窗口外。
|
||||
|
||||
这只能称为 **window-session-closed**,不等于 full-session coherent:任何结果都必须报告
|
||||
这 30 条 boundary-parent residual,且不能据此声称跨窗口 KV reuse。若原始 span 恢复,必须
|
||||
重新从完整 source resolve root 与重新采样,不能沿用 fallback 的 score/threshold。
|
||||
|
||||
@@ -86,7 +86,9 @@ def main() -> int:
|
||||
if not source_trace.is_file():
|
||||
raise SystemExit(f"source trace does not exist: {source_trace}")
|
||||
source_sampling_strategy = source_window.get("sampling_strategy")
|
||||
session_coherent_source = source_sampling_strategy == "session_coherent_uniform_score"
|
||||
full_session_source = source_sampling_strategy == "session_coherent_uniform_score"
|
||||
window_session_source = source_sampling_strategy == "window_session_closed_uniform_score"
|
||||
session_closed_source = full_session_source or window_session_source
|
||||
|
||||
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_trace = args.output_dir / f"{args.output_window_id}.jsonl"
|
||||
@@ -105,10 +107,10 @@ def main() -> int:
|
||||
raise SystemExit(f"row {row_index}: expected an object")
|
||||
sampling_u = numeric(row.get("sampling_u"), field="sampling_u", row_index=row_index)
|
||||
session_entry: dict[str, int] | None = None
|
||||
if session_coherent_source:
|
||||
if session_closed_source:
|
||||
if "session_root" not in row:
|
||||
raise SystemExit(
|
||||
f"row {row_index}: session-coherent source has no session_root"
|
||||
f"row {row_index}: session-closed source has no session_root"
|
||||
)
|
||||
session_key = stable_session_key(row["session_root"])
|
||||
session_entry = session_counts.setdefault(
|
||||
@@ -153,8 +155,12 @@ def main() -> int:
|
||||
"num_requests": selected,
|
||||
"sampling_strategy": (
|
||||
"session_coherent_uniform_score_then_greedy_temperature"
|
||||
if session_coherent_source
|
||||
else "fixed_uniform_score_then_greedy_temperature"
|
||||
if full_session_source
|
||||
else (
|
||||
"window_session_closed_uniform_score_then_greedy_temperature"
|
||||
if window_session_source
|
||||
else "fixed_uniform_score_then_greedy_temperature"
|
||||
)
|
||||
),
|
||||
"collectivespec_source_window_id": args.window_id,
|
||||
"collectivespec_sampling_u_threshold": threshold,
|
||||
@@ -162,7 +168,10 @@ def main() -> int:
|
||||
"collectivespec_source_trace_path": str(source_trace),
|
||||
"collectivespec_source_trace_sha256": sha256(source_trace),
|
||||
"collectivespec_source_sampling_strategy": source_sampling_strategy,
|
||||
"collectivespec_session_closed_within_window": session_coherent_source,
|
||||
"collectivespec_session_closed_within_window": session_closed_source,
|
||||
"collectivespec_session_closure_scope": (
|
||||
"full_session" if full_session_source else "window_only" if window_session_source else None
|
||||
),
|
||||
}
|
||||
)
|
||||
output_windows = {
|
||||
@@ -197,13 +206,14 @@ def main() -> int:
|
||||
},
|
||||
"temperature": {"distinct_value_count": 1, "all_zero": True},
|
||||
"session": {
|
||||
"source_is_session_coherent": session_coherent_source,
|
||||
"session_root_count": len(session_counts) if session_coherent_source else None,
|
||||
"source_is_session_coherent": full_session_source,
|
||||
"source_is_window_session_closed": window_session_source,
|
||||
"session_root_count": len(session_counts) if session_closed_source else None,
|
||||
"selected_session_count": (
|
||||
len(selected_session_turn_counts) if session_coherent_source else None
|
||||
len(selected_session_turn_counts) if session_closed_source else None
|
||||
),
|
||||
"split_session_count": len(split_sessions) if session_coherent_source else None,
|
||||
"all_selected_or_dropped": not split_sessions if session_coherent_source else None,
|
||||
"split_session_count": len(split_sessions) if session_closed_source else None,
|
||||
"all_selected_or_dropped": not split_sessions if session_closed_source else None,
|
||||
"selected_turns": {
|
||||
"min": min(selected_session_turn_counts)
|
||||
if selected_session_turn_counts
|
||||
|
||||
290
scripts/collectivespec/create_window_session_closed_windows.py
Normal file
290
scripts/collectivespec/create_window_session_closed_windows.py
Normal file
@@ -0,0 +1,290 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a session-closed replay window from an already materialized trace.
|
||||
|
||||
This is deliberately a *window-scoped fallback* for when the raw trace span
|
||||
needed by ``prepare_trace_windows.py`` is unavailable. It closes every known
|
||||
parent/child edge within the supplied window and groups siblings whose common
|
||||
parent lies outside the window. It cannot recover turns outside the window;
|
||||
the manifest records that residual rather than calling the result fully
|
||||
session-coherent.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
SCORE_ALGORITHM = "collectivespec-window-cc-v1"
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def typed_id(value: Any) -> str:
|
||||
"""Encode IDs without conflating values such as ``1`` and ``"1"``."""
|
||||
return json.dumps(
|
||||
{"type": type(value).__name__, "value": value},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
|
||||
|
||||
def session_uniform(*, seed: int, window_id: str, component_key: str) -> float:
|
||||
payload = json.dumps(
|
||||
{
|
||||
"algorithm": SCORE_ALGORITHM,
|
||||
"seed": seed,
|
||||
"window_id": window_id,
|
||||
"component_key": component_key,
|
||||
},
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
digest = hashlib.blake2b(payload, digest_size=8).digest()
|
||||
return int.from_bytes(digest, byteorder="big", signed=False) / float(1 << 64)
|
||||
|
||||
|
||||
def resolve_trace_path(windows_path: Path, trace_file: str) -> Path:
|
||||
candidate = Path(trace_file)
|
||||
if candidate.is_absolute():
|
||||
return candidate
|
||||
return (windows_path.parent / candidate).resolve()
|
||||
|
||||
|
||||
def is_root_parent(value: Any) -> bool:
|
||||
return value is None or (
|
||||
isinstance(value, (int, float)) and not isinstance(value, bool) and int(value) < 0
|
||||
)
|
||||
|
||||
|
||||
class UnionFind:
|
||||
def __init__(self) -> None:
|
||||
self.parent: dict[str, str] = {}
|
||||
|
||||
def add(self, item: str) -> None:
|
||||
self.parent.setdefault(item, item)
|
||||
|
||||
def find(self, item: str) -> str:
|
||||
parent = self.parent[item]
|
||||
if parent != item:
|
||||
self.parent[item] = self.find(parent)
|
||||
return self.parent[item]
|
||||
|
||||
def union(self, left: str, right: str) -> None:
|
||||
left_root, right_root = self.find(left), self.find(right)
|
||||
if left_root != right_root:
|
||||
# A canonical representative makes the component key independent
|
||||
# of trace order and union order.
|
||||
self.parent[max(left_root, right_root)] = min(left_root, right_root)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--windows-path", type=Path, required=True)
|
||||
parser.add_argument("--window-id", required=True)
|
||||
parser.add_argument("--sample-seed", type=int, default=20260325)
|
||||
parser.add_argument("--output-dir", type=Path, required=True)
|
||||
parser.add_argument("--output-window-id", required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
source_payload = json.loads(args.windows_path.read_text(encoding="utf-8"))
|
||||
source_windows = source_payload.get("windows") if isinstance(source_payload, dict) else source_payload
|
||||
if not isinstance(source_windows, list):
|
||||
raise SystemExit("windows payload must contain a list")
|
||||
source_window = next(
|
||||
(item for item in source_windows if item.get("window_id") == args.window_id),
|
||||
None,
|
||||
)
|
||||
if not isinstance(source_window, dict):
|
||||
raise SystemExit(f"window id not found: {args.window_id}")
|
||||
trace_file = source_window.get("trace_file")
|
||||
if not isinstance(trace_file, str) or not trace_file:
|
||||
raise SystemExit(f"window {args.window_id} has no trace_file")
|
||||
source_trace = resolve_trace_path(args.windows_path, trace_file)
|
||||
if not source_trace.is_file():
|
||||
raise SystemExit(f"source trace does not exist: {source_trace}")
|
||||
|
||||
rows: list[dict[str, Any]] = []
|
||||
id_to_index: dict[str, int] = {}
|
||||
with source_trace.open("r", encoding="utf-8") as source_handle:
|
||||
for row_index, raw in enumerate(source_handle):
|
||||
if not raw.strip():
|
||||
continue
|
||||
row = json.loads(raw)
|
||||
if not isinstance(row, dict):
|
||||
raise SystemExit(f"row {row_index}: expected an object")
|
||||
if "chat_id" not in row or "parent_chat_id" not in row:
|
||||
raise SystemExit(f"row {row_index}: chat_id and parent_chat_id are required")
|
||||
if "timestamp" not in row:
|
||||
raise SystemExit(f"row {row_index}: timestamp is required")
|
||||
try:
|
||||
timestamp = float(row["timestamp"])
|
||||
except (TypeError, ValueError) as error:
|
||||
raise SystemExit(f"row {row_index}: timestamp must be numeric") from error
|
||||
if not math.isfinite(timestamp):
|
||||
raise SystemExit(f"row {row_index}: timestamp must be finite")
|
||||
key = typed_id(row["chat_id"])
|
||||
if key in id_to_index:
|
||||
raise SystemExit(f"row {row_index}: duplicate chat_id {row['chat_id']!r}")
|
||||
id_to_index[key] = len(rows)
|
||||
rows.append(row)
|
||||
if not rows:
|
||||
raise SystemExit("source trace has no rows")
|
||||
|
||||
union_find = UnionFind()
|
||||
for chat_key in id_to_index:
|
||||
union_find.add(chat_key)
|
||||
known_parent_edges = 0
|
||||
absent_parent_edges = 0
|
||||
root_request_count = 0
|
||||
for row in rows:
|
||||
chat_key = typed_id(row["chat_id"])
|
||||
parent = row["parent_chat_id"]
|
||||
if is_root_parent(parent):
|
||||
root_request_count += 1
|
||||
continue
|
||||
parent_key = typed_id(parent)
|
||||
union_find.add(parent_key)
|
||||
if parent_key in id_to_index:
|
||||
union_find.union(chat_key, parent_key)
|
||||
known_parent_edges += 1
|
||||
else:
|
||||
# Keep the absent parent as a virtual node. This groups siblings
|
||||
# with a common boundary parent without pretending that the parent
|
||||
# turn itself is in this window.
|
||||
union_find.union(chat_key, parent_key)
|
||||
absent_parent_edges += 1
|
||||
|
||||
component_members: dict[str, list[str]] = {}
|
||||
for node in union_find.parent:
|
||||
component_members.setdefault(union_find.find(node), []).append(node)
|
||||
component_key_by_chat: dict[str, str] = {}
|
||||
for members in component_members.values():
|
||||
# Virtual boundary parents intentionally participate: they ensure all
|
||||
# children of an unavailable parent share a deterministic component.
|
||||
component_key = min(members)
|
||||
for chat_key in (member for member in members if member in id_to_index):
|
||||
component_key_by_chat[chat_key] = component_key
|
||||
for row in rows:
|
||||
parent = row["parent_chat_id"]
|
||||
if is_root_parent(parent):
|
||||
continue
|
||||
parent_key = typed_id(parent)
|
||||
if parent_key in id_to_index and union_find.find(typed_id(row["chat_id"])) != union_find.find(parent_key):
|
||||
raise SystemExit("same-window parent edge did not close into one component")
|
||||
|
||||
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
trace_dir = args.output_dir / "traces"
|
||||
trace_dir.mkdir(exist_ok=True)
|
||||
output_trace = trace_dir / f"{args.output_window_id}.jsonl"
|
||||
score_values: list[float] = []
|
||||
timestamps: list[float] = []
|
||||
component_counts: Counter[str] = Counter()
|
||||
with output_trace.open("w", encoding="utf-8") as handle:
|
||||
for row in rows:
|
||||
chat_key = typed_id(row["chat_id"])
|
||||
component_key = component_key_by_chat[chat_key]
|
||||
component_id = f"window-component:{component_key}"
|
||||
out = dict(row)
|
||||
out["collectivespec_original_sampling_u"] = out.get("sampling_u")
|
||||
out["session_root"] = component_id
|
||||
out["sampling_u"] = session_uniform(
|
||||
seed=args.sample_seed,
|
||||
window_id=args.window_id,
|
||||
component_key=component_key,
|
||||
)
|
||||
handle.write(json.dumps(out, ensure_ascii=False, separators=(",", ":")) + "\n")
|
||||
score_values.append(float(out["sampling_u"]))
|
||||
timestamps.append(float(out["timestamp"]))
|
||||
component_counts[component_id] += 1
|
||||
|
||||
component_sizes = list(component_counts.values())
|
||||
output_window = dict(source_window)
|
||||
output_window.update(
|
||||
{
|
||||
"window_id": args.output_window_id,
|
||||
"trace_file": str(output_trace.resolve()),
|
||||
"num_requests": len(rows),
|
||||
"sampling_strategy": "window_session_closed_uniform_score",
|
||||
"sampling_seed": args.sample_seed,
|
||||
"collectivespec_sampling_algorithm": SCORE_ALGORITHM,
|
||||
"collectivespec_source_window_id": args.window_id,
|
||||
"collectivespec_source_trace_path": str(source_trace),
|
||||
"collectivespec_source_trace_sha256": sha256(source_trace),
|
||||
"collectivespec_session_closure_scope": "window_only",
|
||||
"collectivespec_known_parent_edges": known_parent_edges,
|
||||
"collectivespec_absent_parent_edges": absent_parent_edges,
|
||||
}
|
||||
)
|
||||
output_windows = {
|
||||
"kind": "collectivespec_window_session_closed_fallback",
|
||||
"source_windows_path": str(args.windows_path.resolve()),
|
||||
"source_window_id": args.window_id,
|
||||
"sample_seed": args.sample_seed,
|
||||
"windows": [output_window],
|
||||
}
|
||||
output_manifest = args.output_dir / "windows.json"
|
||||
output_manifest.write_text(
|
||||
json.dumps(output_windows, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
sanity = {
|
||||
"n": len(rows),
|
||||
"timestamp": {
|
||||
"min": min(timestamps),
|
||||
"max": max(timestamps),
|
||||
"non_negative": all(value >= 0 for value in timestamps),
|
||||
"nondecreasing": all(
|
||||
timestamps[index] >= timestamps[index - 1]
|
||||
for index in range(1, len(timestamps))
|
||||
),
|
||||
},
|
||||
"sampling_u": {
|
||||
"min": min(score_values),
|
||||
"max": max(score_values),
|
||||
"distinct_value_count": len(set(score_values)),
|
||||
"within_0_1": all(0 <= value < 1 for value in score_values),
|
||||
},
|
||||
"window_session_closure": {
|
||||
"component_count": len(component_sizes),
|
||||
"component_size_min": min(component_sizes),
|
||||
"component_size_max": max(component_sizes),
|
||||
"component_size_distinct_value_count": len(set(component_sizes)),
|
||||
"known_parent_edges": known_parent_edges,
|
||||
"absent_parent_edges": absent_parent_edges,
|
||||
"root_request_count": root_request_count,
|
||||
"all_known_parent_edges_closed": True,
|
||||
"scope": "window_only",
|
||||
},
|
||||
"source_trace_sha256": sha256(source_trace),
|
||||
"output_trace_sha256": sha256(output_trace),
|
||||
}
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"windows_path": str(output_manifest),
|
||||
"trace_path": str(output_trace),
|
||||
"data_sanity": sanity,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user