Add window-scoped session closure fallback
This commit is contained in:
@@ -200,3 +200,16 @@ diverge,因此做 ragged verifier”这一表述太宽,不能作为实现前
|
|||||||
queue policy。若全部通过,最小原型的边界也只应是 verifier-side compaction:当前
|
queue policy。若全部通过,最小原型的边界也只应是 verifier-side compaction:当前
|
||||||
EAGLE 仍按 Kmax 产生 candidate,短 `k_i` 不会自动消除 draft-side work,不能把 verifier
|
EAGLE 仍按 Kmax 产生 candidate,短 `k_i` 不会自动消除 draft-side work,不能把 verifier
|
||||||
节省误报为完整 draft+verify speedup。
|
节省误报为完整 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():
|
if not source_trace.is_file():
|
||||||
raise SystemExit(f"source trace does not exist: {source_trace}")
|
raise SystemExit(f"source trace does not exist: {source_trace}")
|
||||||
source_sampling_strategy = source_window.get("sampling_strategy")
|
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)
|
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
output_trace = args.output_dir / f"{args.output_window_id}.jsonl"
|
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")
|
raise SystemExit(f"row {row_index}: expected an object")
|
||||||
sampling_u = numeric(row.get("sampling_u"), field="sampling_u", row_index=row_index)
|
sampling_u = numeric(row.get("sampling_u"), field="sampling_u", row_index=row_index)
|
||||||
session_entry: dict[str, int] | None = None
|
session_entry: dict[str, int] | None = None
|
||||||
if session_coherent_source:
|
if session_closed_source:
|
||||||
if "session_root" not in row:
|
if "session_root" not in row:
|
||||||
raise SystemExit(
|
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_key = stable_session_key(row["session_root"])
|
||||||
session_entry = session_counts.setdefault(
|
session_entry = session_counts.setdefault(
|
||||||
@@ -153,8 +155,12 @@ def main() -> int:
|
|||||||
"num_requests": selected,
|
"num_requests": selected,
|
||||||
"sampling_strategy": (
|
"sampling_strategy": (
|
||||||
"session_coherent_uniform_score_then_greedy_temperature"
|
"session_coherent_uniform_score_then_greedy_temperature"
|
||||||
if session_coherent_source
|
if full_session_source
|
||||||
|
else (
|
||||||
|
"window_session_closed_uniform_score_then_greedy_temperature"
|
||||||
|
if window_session_source
|
||||||
else "fixed_uniform_score_then_greedy_temperature"
|
else "fixed_uniform_score_then_greedy_temperature"
|
||||||
|
)
|
||||||
),
|
),
|
||||||
"collectivespec_source_window_id": args.window_id,
|
"collectivespec_source_window_id": args.window_id,
|
||||||
"collectivespec_sampling_u_threshold": threshold,
|
"collectivespec_sampling_u_threshold": threshold,
|
||||||
@@ -162,7 +168,10 @@ def main() -> int:
|
|||||||
"collectivespec_source_trace_path": str(source_trace),
|
"collectivespec_source_trace_path": str(source_trace),
|
||||||
"collectivespec_source_trace_sha256": sha256(source_trace),
|
"collectivespec_source_trace_sha256": sha256(source_trace),
|
||||||
"collectivespec_source_sampling_strategy": source_sampling_strategy,
|
"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 = {
|
output_windows = {
|
||||||
@@ -197,13 +206,14 @@ def main() -> int:
|
|||||||
},
|
},
|
||||||
"temperature": {"distinct_value_count": 1, "all_zero": True},
|
"temperature": {"distinct_value_count": 1, "all_zero": True},
|
||||||
"session": {
|
"session": {
|
||||||
"source_is_session_coherent": session_coherent_source,
|
"source_is_session_coherent": full_session_source,
|
||||||
"session_root_count": len(session_counts) if session_coherent_source else None,
|
"source_is_window_session_closed": window_session_source,
|
||||||
|
"session_root_count": len(session_counts) if session_closed_source else None,
|
||||||
"selected_session_count": (
|
"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,
|
"split_session_count": len(split_sessions) if session_closed_source else None,
|
||||||
"all_selected_or_dropped": not split_sessions if session_coherent_source else None,
|
"all_selected_or_dropped": not split_sessions if session_closed_source else None,
|
||||||
"selected_turns": {
|
"selected_turns": {
|
||||||
"min": min(selected_session_turn_counts)
|
"min": min(selected_session_turn_counts)
|
||||||
if 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