Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions simulator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,11 @@ python -m simulator sweep \
| `length_mix` | `{tokens,weight}[]` | 持续模式的离散长度分布;`weight` 只需为正数,不要求预归一化。 |
| `csv_path` | `string|null` | CLI 读取的 CSV 路径;与 `csv_text` 互斥。页面上传会使用 `csv_text`。 |
| `csv_text` | `string|null` | CSV 原始内容;与 `csv_path` 互斥。 |
| `csv_sampling` | `cycle|sample` / `"cycle"` | 无时间戳 CSV 在持续模式下的选取方式:按行循环或有放回随机采样。 |
| `csv_sampling` | `cycle|sample` / `"cycle"` | CSV 在合成到达过程下的长度选取方式:按行循环或有放回随机采样。 |

优先级:CSV > `fixed_lengths`/`length_mix`。固定模式下 CSV 每行回放一次;持续模式下无时间戳 CSV 提供经验长度分布。
优先级:CSV > `fixed_lengths`/`length_mix`。固定模式下 CSV 每行回放一次;
持续模式下,`constant`/`poisson` 把 CSV 当作经验长度分布,即使文件带
`arrival_time_ms` 也会忽略该列;只有 `trace`/`scaled_trace` 会消费时间戳。

WebUI 还提供 MoonConv V4 Flash 的 `formal_0/1/2`(各 512 条)和
`screening`(128 条)内置 trace。它们只保留输入长度与相对到达时间;选择后
Expand All @@ -177,6 +179,10 @@ WebUI 还提供 MoonConv V4 Flash 的 `formal_0/1/2`(各 512 条)和
配置值。QPS sweep 可以使用 `scaled_trace`,每个扫描点重新缩放同一 arrival
形状。

WebUI 上传带 `arrival_time_ms` 的 CSV 时默认切换到持续模式和
`scaled_trace`。仍可手动切换为 `trace` 精确回放,或切换为
`constant`/`poisson` 仅使用其长度分布。

### 2.3 `scheduler`

| 字段 | 类型 / 默认值 | 说明 |
Expand Down
2 changes: 2 additions & 0 deletions simulator/tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ def test_defaults_page_and_simulation_api(self) -> None:
self.assertIn("data.length_datasets.forEach", page)
self.assertIn("/api/length-datasets/", page)
self.assertIn('value="scaled_trace"', page)
self.assertIn("function useTimestampedCsvDefaults", page)
self.assertIn("columns.includes('arrival_time_ms')", page)
self.assertIn("item.num_devices===afd.num_devices", page)
self.assertIn('value="prefill_token_greedy"', page)
self.assertIn('value="prefill_token_square_greedy"', page)
Expand Down
27 changes: 27 additions & 0 deletions simulator/tests/test_workload.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,33 @@ def test_scaled_trace_keeps_zero_gap_arrivals_simultaneous(self) -> None:

self.assertEqual([request.arrival_ms for request in workload], [0.0, 0.0])

def test_synthetic_arrival_uses_timestamped_csv_as_length_pool(self) -> None:
config = SimulationConfig.from_mapping(
{
"mode": "continuous",
"csv_text": (
"arrival_time_ms,input_length\n"
"100,128\n"
"125,256\n"
"160,512\n"
),
"arrival": {
"kind": "constant",
"qps": 2,
"duration_s": 1,
"warmup_s": 0,
},
}
)

workload = generate_workload(config)

self.assertEqual([request.arrival_ms for request in workload], [0.0, 500.0])
self.assertEqual(
[request.input_tokens for request in workload],
[128, 256],
)

def test_prefix_cache_sampling_is_deterministic_and_block_aligned(self) -> None:
raw = {
"mode": "fixed",
Expand Down
13 changes: 11 additions & 2 deletions simulator/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,21 @@ <h2>仿真参数</h2>
$('profile').textContent = `${metadata.model || 'DSV4-Flash'} · ${metadata.device || 'device unknown'} · ${afd.num_devices} die · ${afd.layer_count} 层`;
}

function useTimestampedCsvDefaults(text) {
const header = text.replace(/^\ufeff/, '').split(/\r?\n/, 1)[0];
const columns = header.split(',').map(column => column.trim());
if (!columns.includes('arrival_time_ms')) return;
$('mode').value = 'continuous';
$('arrival-kind').value = 'scaled_trace';
}

$('csv-file').addEventListener('change', async event => {
const file = event.target.files[0];
csvText = file ? await file.text() : null;
$('length-dataset').value = '';
$('csv-status').textContent = file ? `${file.name} · ${(file.size / 1024).toFixed(1)} KiB` : '未加载 CSV';
if (csvText) useTimestampedCsvDefaults(csvText);
updateControlState();
});

$('length-dataset').addEventListener('change', async event => {
Expand All @@ -356,8 +366,7 @@ <h2>仿真参数</h2>
const response = await fetch(`/api/length-datasets/${encodeURIComponent(datasetId)}`);
if (!response.ok) throw new Error(`加载长度数据集失败:HTTP ${response.status}`);
csvText = await response.text();
$('mode').value = 'continuous';
$('arrival-kind').value = 'scaled_trace';
useTimestampedCsvDefaults(csvText);
$('csv-status').textContent = `${dataset.label} · ${dataset.request_count} 条 · ${(csvText.length / 1024).toFixed(1)} KiB`;
updateControlState();
} catch (error) {
Expand Down
11 changes: 5 additions & 6 deletions simulator/workload.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,11 @@ def generate_workload(config: SimulationConfig) -> tuple[RequestSpec, ...]:
]
return _materialize_requests(raw_requests, arrivals, config.prefix_cache)

if csv_requests and all(item.arrival_time_ms is not None for item in csv_requests):
if (
csv_requests
and config.arrival.kind in {"trace", "scaled_trace"}
and all(item.arrival_time_ms is not None for item in csv_requests)
):
if config.arrival.kind == "scaled_trace":
raw_requests, arrivals = _scale_and_repeat_trace(
csv_requests,
Expand All @@ -141,11 +145,6 @@ def generate_workload(config: SimulationConfig) -> tuple[RequestSpec, ...]:
arrivals,
config.prefix_cache,
)
if config.arrival.kind != "trace":
raise ValueError(
"CSV arrival_time_ms requires arrival.kind=trace for exact replay "
"or scaled_trace for QPS-scaled replay"
)
first_arrival = float(csv_requests[0].arrival_time_ms or 0.0)
arrivals = [
float(item.arrival_time_ms or 0.0) - first_arrival for item in csv_requests
Expand Down