Hft datageneration vllm sched#54
Conversation
Formatted reproduction steps with code blocks for clarity.
…ats.py (keep V1 fallback) and harness.py (adopt package shim)
Code Review by GeminiThe changes introduce a comprehensive set of tools and documentation for HFT benchmarks, including data generation, profiling, and intervention analysis. A new module for vLLM scheduler statistics is also added, enhancing observability. The code is generally well-structured and documented. Here are some minor observations and suggestions:
|
🤖 Claude Code ReviewReview🐛 Bugs
from numba.core.types import none # unused; pulls in all of Numba
from pydantic.fields import PropertyT # not a public symbol in Pydantic v2; ImportErrorThese will cause an
"total_preemptions": max(s.num_preemptions for s in self.samples),
n_chunks = n_events // chunk_sizeIf
rng = cp.random.default_rng(seed) # GPU fields
rng_np = np.random.default_rng(seed) # Hawkes timestampsBoth are seeded with the same value, so the NumPy uniform draw
nsys stats --report cuda_gpu_kern_sum --format csv \
--output "$OUT/shard_$i" "$OUT/shard_$i.nsys-rep"
assert s.batch_occupancy == 3 / 256 # occupancy derived from V1 num_running
🔒 Security
SEED="$(basename "$STAGE" | grep -oE '[0-9]+$')"If ⚡ Performance
symbol_id = cp.asnumpy(rng.integers(...))
side = cp.asnumpy(rng.integers(...))
price = cp.asnumpy(rng.integers(...))
...Six separate
cov = covered_kernels(spec, trace) # O(n) scan
cov_ns = sum(... for k in trace.kernels() # second O(n) scan
if k.name in cov)With only 4 kernels this is trivial, but the pattern is worth fixing for when the trace grows: fold both into one pass. 📊 Reproducibility
def warmup():
...
cp.random.default_rng(0).integers(0, 100, size=10_000) # throwaway rng
💡 Suggestions
# After this completes, parse_shard_profiles.py turns the 5 .sqlite filesShould say 20 (or
|
Code Review by Gemini--- a/benchmarks/hft/gpu_generator.py
+++ b/benchmarks/hft/gpu_generator.py
@@ -36,7 +36,9 @@
side = cp.asnumpy(rng.integers(0, 2, size=n, dtype=cp.int8))
price = cp.asnumpy(rng.integers(9000, 11000, size=n, dtype=cp.int64))
size_arr = cp.asnumpy(rng.integers(1, 1000, size=n, dtype=cp.int32))
roll = cp.asnumpy(rng.random(n, dtype=cp.float32))
- etype = np.where(roll < 0.55, 0, np.where(roll < 0.90, 1, 2)).astype(np.int8)
+ # Use np.select for clearer multi-condition logic, especially if more event types are added.
+ conditions = [roll < 0.55, roll < 0.90]
+ choices = [0, 1]
+ etype = np.select(conditions, choices, default=2).astype(np.int8)
return {
'ts_ns': ts_ns, 'symbol_id': symbol_id, 'side': side,Reasoning for change: The original --- a/benchmarks/hft/headroom_kernel_rank.py
+++ b/benchmarks/hft/headroom_kernel_rank.py
@@ -60,7 +60,7 @@
def covered_kernels(spec: InterventionSpec, trace: Trace) -> list[str]:
- return [k.name for k in trace.kernels() if _applies(spec, k.name)]
+ return [k.name for k in trace.kernels() if _applies_to_kernel(spec, k.name)]
def main() -> None:Reasoning for change: The function --- a/gitm/tracer/vllm_sched.py
+++ b/gitm/tracer/vllm_sched.py
@@ -10,7 +10,6 @@
from __future__ import annotations
from typing import Any
-
from numba.core.types import none
from pydantic import BaseModel, ConfigDict, Field
from pydantic.fields import PropertyTReasoning for change: The import |
🤖 Claude Code ReviewReview🐛 Bugs
from numba.core.types import none # unused, wrong package for this module
from pydantic.fields import PropertyT # internal pydantic API, not publicBoth lines will cause
"total_preemptions": max(s.num_preemptions for s in self.samples),If vLLM reports cumulative preemptions this is accidentally correct, but the field is named
n_chunks = n_events // chunk_size # integer division, remainder discardedFor
if len(futures) > n_writers:
futures.pop(0).result() # blocks on chunk 0, even if chunk 2 finished firstThis serialises the pipeline back to one thread at a time in the worst case. Use a deque or
assert s.batch_occupancy == 3 / 256 # occupancy derived from V1 num_running
-> Owner: Ash — baseline + profiling + spec doc.
+> Owner: Ash baseline + profiling + spec doc.Looks like an unintentional edit. 🔒 Security
ln -sf "$STAGE/$SHARD" "$TMP/part-00000.parquet"
SEED="$(basename "$STAGE" | grep -oE '[0-9]+$')"If
⚡ Performance
symbol_id = cp.asnumpy(rng.integers(...))
side = cp.asnumpy(rng.integers(...))
price = cp.asnumpy(rng.integers(...))
...Each call synchronises the GPU and launches a separate D→H DMA. Pack into a single structured array or a multi-column CuPy matrix and do one transfer; at 5M rows × 5 fields this is five 20–40 MB PCIe round trips per chunk where one would do.
📊 Reproducibility
rng = cp.random.default_rng(seed) # GPU
rng_np = np.random.default_rng(seed) # CPUBoth get the same seed, but since they consume from different generators there is no documented guarantee about the joint distribution of
💡 Suggestions
except Exception as e: # noqa: BLE001
print(f"\ncould not import gitm modules ({e}).")
returnA typo in a gitm import path, a missing
|
Code Review by GeminiThe changes introduce new tools and documentation for HFT benchmarking and enhance vLLM scheduler tracing capabilities. The code is well-structured, documented, and includes appropriate tests. The explicit notes about estimates and limitations in the HFT section are commendable for transparency. Here are some minor suggestions for improvement:
|
🤖 Claude Code ReviewReview🐛 Bugs
from numba.core.types import none # line ~19 — unused, wrong module
from pydantic.fields import PropertyT # removed in Pydantic v2, will crash on importNeither is used anywhere in the file. Remove both.
"total_preemptions": max(s.num_preemptions for s in self.samples),
n_chunks = n_events // chunk_size # integer division — remainder events never generatedAdd a trailing partial-chunk pass, or assert
futures.pop(0).result() # only drains when len > n_writers; last ≤ n_writers futures…
for f in futures:
f.result() # …caught here, but exceptions from earlier pops are lost silentlyExceptions from
nsys stats --report cuda_gpu_kern_sum --format csv \
--output "$OUT/shard_$i" "$OUT/shard_$i.nsys-rep"
n_shards = min((len(v) for v in obs_series.values()), default=0)
for shard_i in range(n_shards):
for op, series in obs_series.items():
t_obs = series[shard_i] # safe only if every op has >= n_shards entriesThis is correct if every op appears in every shard profile. If an op is absent from some shards (kernel not triggered),
assert s.batch_occupancy == 3 / 256 # expects capacity-based occupancy
🔒 Security
nsys profile ... python3 "$HARNESS" --seed "$SEED" --stage "$OUT/stage_$i" ...
⚡ Performance
symbol_id = cp.asnumpy(rng.integers(...))
side = cp.asnumpy(rng.integers(...))
price = cp.asnumpy(rng.integers(...))
...Each
pq.ParquetFile(p).metadata # opens and reads footer for every fileFor 200 shards this is 200 file-open + footer-parse calls. Consider using 📊 Reproducibility
rng = cp.random.default_rng(seed) # GPU
rng_np = np.random.default_rng(seed) # CPU (Hawkes uniforms)The same integer seed produces different sequences from the two generators (different algorithms). This is intentional (Hawkes on CPU, fields on GPU), but the documentation and
💡 Suggestions
cov = covered_kernels(spec, trace) # returns names
cov_ns = sum(
(k.end_ns - k.start_ns) for k in trace.kernels() if k.name in cov
)
applies_to_kernels: [decode_page_data, zstd::decompression]
if not self.samples:
return {"n_samples": 0}The early return is correct, but callers that expect keys like
-> Owner: Ash — baseline + profiling + spec doc.
+> Owner: Ash baseline + profiling + spec doc.Looks like an accidental character deletion, not intentional. |
…el import), fix total_preemptions to use max-min, and wire missing V1 fallback loop in vllm_stats.py
Code Review by Gemini### `benchmarks/hft/spec.md`
The reproduction steps for building the generator include a hardcoded version for `libparquet.so`. This can be brittle if the `pyarrow` library version changes, leading to `libparquet.so.2400` not being found. A more robust approach would be to dynamically find the latest versioned `libparquet.so` file.
```diff
--- a/benchmarks/hft/spec.md
+++ b/benchmarks/hft/spec.md
@@ -49,7 +49,7 @@
**2. Build the generator:**
```bash
cd benchmarks/hft/generator && mkdir -p build && cd build
export ARROW_LIB=$(python3 -c "import pyarrow; print(pyarrow.get_library_dirs()[0])")
-ln -sf $ARROW_LIB/libparquet.so.2400 $ARROW_LIB/libparquet.so
+ln -sf "$(find "$ARROW_LIB" -name "libparquet.so.*" | sort -V | tail -n 1)" "$ARROW_LIB/libparquet.so"
cmake .. && make -j$(nproc) |
🤖 Claude Code Review🐛 Bugs
n_shards = min((len(v) for v in obs_series.values()), default=0)If one op is missing from some shards (e.g. merge_sort doesn't appear in a quiet shard), the series gets truncated to that op's shorter count with no warning, potentially making Granger results unreliable without surfacing why.
"total_preemptions": max(s.num_preemptions for s in self.samples) - min(s.num_preemptions for s in self.samples),This assumes
setattr(sample, field_name, val)Pydantic v2 models with
assert read_scThe file is cut off mid-line. This will fail to import entirely, breaking the entire test suite.
🔒 Security
ln -sf "$STAGE/$SHARD" "$TMP/part-00000.parquet"
SEED="$(basename "$STAGE" | grep -oE '[0-9]+$')"If
⚡ Performance
📊 Reproducibility
rng = cp.random.default_rng(seed)
rng_np = np.random.default_rng(seed)Both start at the same seed. For seed=42, both generate from the same initial state but via different algorithms (Philox for CuPy, PCG64 for NumPy). This is not a correctness bug but it means the GPU fields and Hawkes timestamps are not independently seeded — if either RNG is ever replaced, the joint distribution changes in a non-obvious way. Use
💡 Suggestions
cov = covered_kernels(spec, trace) # iterates trace.kernels()
cov_ns = sum(... for k in trace.kernels() if k.name in cov) # iterates againWith a short trace this is fine, but
The |
Code Review by Gemini### `benchmarks/hft/HFT_HEADROOM_TOOLS.md`
```diff
--- a/benchmarks/hft/HFT_HEADROOM_TOOLS.md
+++ b/benchmarks/hft/HFT_HEADROOM_TOOLS.md
@@ -36,7 +36,7 @@
Goal: apply hft_parquet_h2d_overlap to the harness, prove it speeds things up
WITHOUT changing the derived metrics (VWAP/microprice), via a checksum gate.
-The pieces that already exist in the repo:
+The pieces that already exist in the repo (run from repo root):
- gitm/optimizer/apply.py — apply_intervention(spec, applicator,
min_keep_delta): snapshot -> apply -> measure
-> keep|rollback. Use this as the gate.
Improvement: Clarified the context for the path to
|
🤖 Claude Code ReviewReview🐛 Bugs
rng = cp.random.default_rng(seed)
rng_np = np.random.default_rng(seed)Both RNGs get the same seed, but they're advanced independently per chunk. If chunk size or count changes, the Hawkes timestamps (CPU) and GPU fields diverge from each other in unpredictable ways. The same-seed coincidence is also misleading — these should be explicitly documented as independent streams or derived via a seeding scheme (e.g.
n_chunks = n_events // chunk_sizeIf
if len(futures) > n_writers:
futures.pop(0).result() # raises on write failureOnly the oldest future is drained; after the loop the remaining futures are drained correctly. However, if a write fails mid-loop, the exception propagates and the remaining futures are never
n_shards = min((len(v) for v in obs_series.values()), default=0)If one op is missing from some CSVs (kernel not triggered on that shard), its series is shorter and later ops use fewer samples without any warning. The residuals for the truncated op won't line up with the others in the Granger input, corrupting the causal ordering. Should log a warning when lengths differ.
"total_preemptions": max(s.num_preemptions for s in self.samples) - min(s.num_preemptions for s in self.samples),This breaks if the engine resets its preemption counter between samples (e.g. on engine restart or stats reset), producing a negative or underestimated value. A running delta (sum of positive increments between consecutive samples) would be more robust.
if getattr(sample, field_name) is None:
setattr(sample, field_name, val)
def covered_kernels(spec, trace):
return [k.name for k in trace.kernels() if _applies(spec, k.name)]
cov_ns = sum(
(k.end_ns - k.start_ns) for k in trace.kernels() if k.name in cov
)
🔒 Security
SEED="$(basename "$STAGE" | grep -oE '[0-9]+$')"
TMP="$OUT/stage_$i/hft_numba_seed${SEED}"and passed to Python: python3 "$HARNESS" --seed "$SEED" ...If [[ "$SEED" =~ ^[0-9]+$ ]] || { echo "could not parse seed from $STAGE"; exit 1; }⚡ Performance
symbol_id = cp.asnumpy(rng.integers(...))
side = cp.asnumpy(rng.integers(...))
price = cp.asnumpy(rng.integers(...))
...Each
for i, p in enumerate(paths):
md = pq.ParquetFile(p).metadata
📊 Reproducibility
As noted under Bugs: both
@njit(cache=True)
def hawkes_numba(u, n, t0, lam0):
pq.write_table(table, '/workspace/test_nocomp.parquet', ...)These aren't pytest tests (no assertions, no 💡 Suggestions
nsys stats --report cuda_gpu_kern_sum --format csv \
--output "$OUT/shard_$i" "$OUT/shard_$i.nsys-rep" || trueThe
rows.sort(...)
top = rows[0] # IndexError if library has no interventionsIf
applies_to_kernels: [decode_page_data, DeviceMergeSort, DeviceReduce]
|
No description provided.