inferencekit-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- inferencekit/__init__.py +9 -0
- inferencekit/__main__.py +5 -0
- inferencekit/analysis/__init__.py +1 -0
- inferencekit/analysis/ncu_csv.py +284 -0
- inferencekit/analysis/nsys_sqlite.py +518 -0
- inferencekit/canonical.py +104 -0
- inferencekit/checkpoint.py +396 -0
- inferencekit/cli.py +274 -0
- inferencekit/cuda/bw_peak.cu +252 -0
- inferencekit/cuda/torch_bw.py +60 -0
- inferencekit/engines/__init__.py +7 -0
- inferencekit/engines/adapters.py +190 -0
- inferencekit/engines/clip/__init__.py +13 -0
- inferencekit/engines/clip/comfyui.py +749 -0
- inferencekit/engines/clip/diffusers_ltx.py +601 -0
- inferencekit/engines/clip/noise.py +46 -0
- inferencekit/engines/clip/official_ltx.py +316 -0
- inferencekit/engines/clip/server.py +542 -0
- inferencekit/engines/clip/stepdump.py +182 -0
- inferencekit/engines/gates.py +94 -0
- inferencekit/engines/sglang_worker.py +86 -0
- inferencekit/engines/vllm_worker.py +129 -0
- inferencekit/evidence.py +427 -0
- inferencekit/gpuinfo.py +93 -0
- inferencekit/hardware.py +135 -0
- inferencekit/mcp_server.py +178 -0
- inferencekit/ops/__init__.py +30 -0
- inferencekit/ops/bench.py +184 -0
- inferencekit/ops/bench_audio.py +384 -0
- inferencekit/ops/bench_clip.py +819 -0
- inferencekit/ops/bench_image.py +393 -0
- inferencekit/ops/bench_rounds.py +297 -0
- inferencekit/ops/bench_serve.py +866 -0
- inferencekit/ops/ceiling.py +135 -0
- inferencekit/ops/compare.py +378 -0
- inferencekit/ops/correctness.py +153 -0
- inferencekit/ops/doctor.py +486 -0
- inferencekit/ops/gates.py +1772 -0
- inferencekit/ops/gates_clip.py +930 -0
- inferencekit/ops/intake.py +1321 -0
- inferencekit/ops/ledger.py +749 -0
- inferencekit/ops/manifest.py +309 -0
- inferencekit/ops/ncu.py +183 -0
- inferencekit/ops/nsys.py +224 -0
- inferencekit/ops/prompts.py +316 -0
- inferencekit/ops/quality.py +2161 -0
- inferencekit/ops/quality_clip.py +690 -0
- inferencekit/ops/quality_clip_steps.py +341 -0
- inferencekit/ops/report.py +317 -0
- inferencekit/ops/review.py +119 -0
- inferencekit/ops/rocprof.py +89 -0
- inferencekit/ops/roofline.py +392 -0
- inferencekit/ops/store.py +61 -0
- inferencekit/proc.py +172 -0
- inferencekit/prompts/ATTRIBUTION.md +56 -0
- inferencekit/prompts/__init__.py +167 -0
- inferencekit/prompts/build.py +518 -0
- inferencekit/prompts/dev-load.jsonl.gz +0 -0
- inferencekit/prompts/dev-load.manifest.json +212 -0
- inferencekit/prompts/dev.jsonl.gz +0 -0
- inferencekit/prompts/dev.manifest.json +214 -0
- inferencekit/prompts/exclusions.json +54 -0
- inferencekit/quality/__init__.py +23 -0
- inferencekit/quality/client.py +66 -0
- inferencekit/quality/data/gsm8k_LICENSE.txt +21 -0
- inferencekit/quality/fake.py +329 -0
- inferencekit/quality/gsm8k.py +224 -0
- inferencekit/quality/logprobs.py +106 -0
- inferencekit/quality/metrics.py +235 -0
- inferencekit/quality/niah.py +63 -0
- inferencekit/quality/parquet.py +296 -0
- inferencekit/quality/sampling.py +262 -0
- inferencekit/quality/step_replay.py +217 -0
- inferencekit/quality/video_metrics.py +304 -0
- inferencekit/registry.py +206 -0
- inferencekit/review/__init__.py +6 -0
- inferencekit/review/diff.py +163 -0
- inferencekit/review/media.py +181 -0
- inferencekit/review/page.py +920 -0
- inferencekit/review/rundir.py +844 -0
- inferencekit/roofline_headers.py +504 -0
- inferencekit/schemas/evidence.v1.json +130 -0
- inferencekit/schemas/evidence.v2.json +499 -0
- inferencekit/schemas/intake.v1.json +276 -0
- inferencekit/schemas/run-manifest.v1.json +118 -0
- inferencekit/serve/__init__.py +8 -0
- inferencekit/serve/audio_client.py +330 -0
- inferencekit/serve/client.py +446 -0
- inferencekit/serve/clip_client.py +351 -0
- inferencekit/serve/clip_rounds.py +429 -0
- inferencekit/serve/image_client.py +373 -0
- inferencekit/serve/loadgen.py +136 -0
- inferencekit/serve/metrics.py +203 -0
- inferencekit/serve/profiles.py +105 -0
- inferencekit/serve/prompts_io.py +125 -0
- inferencekit/serve/rounds.py +1111 -0
- inferencekit/serve/servers.py +897 -0
- inferencekit/stats.py +249 -0
- inferencekit_cli-0.1.0.dist-info/METADATA +233 -0
- inferencekit_cli-0.1.0.dist-info/RECORD +104 -0
- inferencekit_cli-0.1.0.dist-info/WHEEL +4 -0
- inferencekit_cli-0.1.0.dist-info/entry_points.txt +2 -0
- inferencekit_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
- inferencekit_cli-0.1.0.dist-info/licenses/NOTICE +9 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// Practical DRAM bandwidth of the GPU: the "100%" that decode kernels should be
|
|
2
|
+
// compared against (datasheet bandwidth is a theoretical peak no kernel reaches).
|
|
3
|
+
//
|
|
4
|
+
// Tests, each printed as one JSON object per line on stdout:
|
|
5
|
+
// device name, SM count, L2 size, bus width, memory clock, theoretical GB/s
|
|
6
|
+
// read read-only stream over N bytes (16-byte non-caching loads, 4 in flight
|
|
7
|
+
// per thread); L2 is flushed before every timed run so small buffers
|
|
8
|
+
// measure DRAM, not cache
|
|
9
|
+
// copy read N/2 bytes + write N/2 bytes (counts N bytes moved)
|
|
10
|
+
// graph_read a CUDA graph of 32 back-to-back launches, each reading its own S-byte
|
|
11
|
+
// region: the best a kernel of that size can do. The gap to "read" is
|
|
12
|
+
// the per-launch ramp-up and tail, which is why small decode kernels
|
|
13
|
+
// rarely reach peak bandwidth. With PDL (programmatic dependent launch,
|
|
14
|
+
// sm_90+) the next launch starts its prologue while the previous drains.
|
|
15
|
+
//
|
|
16
|
+
// Build: nvcc -O3 -std=c++17 -arch=native -o bw_peak bw_peak.cu
|
|
17
|
+
// Usage: bw_peak [--sizes-mib 8,32,128,512,1024] [--graph-mib 1,4,16] [--iters 30]
|
|
18
|
+
#include <cuda_runtime.h>
|
|
19
|
+
|
|
20
|
+
#include <algorithm>
|
|
21
|
+
#include <cstdint>
|
|
22
|
+
#include <cstdio>
|
|
23
|
+
#include <cstdlib>
|
|
24
|
+
#include <cstring>
|
|
25
|
+
#include <string>
|
|
26
|
+
#include <vector>
|
|
27
|
+
|
|
28
|
+
#define CHECK(x) \
|
|
29
|
+
do { \
|
|
30
|
+
cudaError_t err_ = (x); \
|
|
31
|
+
if (err_ != cudaSuccess) { \
|
|
32
|
+
std::fprintf(stderr, "%s:%d %s: %s\n", __FILE__, __LINE__, #x, cudaGetErrorString(err_)); \
|
|
33
|
+
std::exit(1); \
|
|
34
|
+
} \
|
|
35
|
+
} while (0)
|
|
36
|
+
|
|
37
|
+
// 16-byte load that bypasses L1 (we never reuse the data, so caching it only costs).
|
|
38
|
+
__device__ __forceinline__ uint4 load_stream(const uint4* p) {
|
|
39
|
+
#if __CUDA_ARCH__ >= 700
|
|
40
|
+
uint4 v;
|
|
41
|
+
asm volatile("ld.global.nc.L1::no_allocate.v4.u32 {%0,%1,%2,%3}, [%4];"
|
|
42
|
+
: "=r"(v.x), "=r"(v.y), "=r"(v.z), "=r"(v.w)
|
|
43
|
+
: "l"(p));
|
|
44
|
+
return v;
|
|
45
|
+
#else
|
|
46
|
+
return __ldg(p);
|
|
47
|
+
#endif
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Grid-stride read. UNROLL independent loads are issued before any is used, so each
|
|
51
|
+
// thread keeps several memory requests in flight (memory-level parallelism).
|
|
52
|
+
template <int UNROLL>
|
|
53
|
+
__global__ void read_kernel(const uint4* __restrict__ p, size_t n16, unsigned* sink, int pdl) {
|
|
54
|
+
#if __CUDA_ARCH__ >= 900
|
|
55
|
+
if (pdl) asm volatile("griddepcontrol.launch_dependents;" ::: "memory");
|
|
56
|
+
#endif
|
|
57
|
+
unsigned acc = 0;
|
|
58
|
+
const size_t stride = (size_t)gridDim.x * blockDim.x;
|
|
59
|
+
size_t i = (size_t)blockIdx.x * blockDim.x + threadIdx.x;
|
|
60
|
+
for (; i + (UNROLL - 1) * stride < n16; i += UNROLL * stride) {
|
|
61
|
+
uint4 v[UNROLL];
|
|
62
|
+
#pragma unroll
|
|
63
|
+
for (int u = 0; u < UNROLL; ++u) v[u] = load_stream(p + i + u * stride);
|
|
64
|
+
#pragma unroll
|
|
65
|
+
for (int u = 0; u < UNROLL; ++u) acc ^= v[u].x ^ v[u].y ^ v[u].z ^ v[u].w;
|
|
66
|
+
}
|
|
67
|
+
for (; i < n16; i += stride) {
|
|
68
|
+
const uint4 v = load_stream(p + i);
|
|
69
|
+
acc ^= v.x ^ v.y ^ v.z ^ v.w;
|
|
70
|
+
}
|
|
71
|
+
#if __CUDA_ARCH__ >= 900
|
|
72
|
+
if (pdl) asm volatile("griddepcontrol.wait;" ::: "memory");
|
|
73
|
+
#endif
|
|
74
|
+
if (acc == 0x9e3779b9u) sink[0] = acc; // data-dependent store: the compiler cannot drop the loads
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
__global__ void copy_kernel(const uint4* __restrict__ a, uint4* __restrict__ b, size_t n16) {
|
|
78
|
+
const size_t stride = (size_t)gridDim.x * blockDim.x;
|
|
79
|
+
for (size_t i = (size_t)blockIdx.x * blockDim.x + threadIdx.x; i < n16; i += stride) b[i] = a[i];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Writes a buffer larger than L2 so the next timed kernel starts with a cold cache.
|
|
83
|
+
__global__ void flush_kernel(float* f, size_t n) {
|
|
84
|
+
const size_t stride = (size_t)gridDim.x * blockDim.x;
|
|
85
|
+
for (size_t i = (size_t)blockIdx.x * blockDim.x + threadIdx.x; i < n; i += stride) f[i] += 1.0f;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static float median(std::vector<float> v) {
|
|
89
|
+
std::sort(v.begin(), v.end());
|
|
90
|
+
return v[v.size() / 2];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static std::vector<size_t> parse_list(const char* s) {
|
|
94
|
+
std::vector<size_t> out;
|
|
95
|
+
std::string str(s);
|
|
96
|
+
size_t pos = 0;
|
|
97
|
+
while (pos < str.size()) {
|
|
98
|
+
size_t comma = str.find(',', pos);
|
|
99
|
+
if (comma == std::string::npos) comma = str.size();
|
|
100
|
+
out.push_back(std::strtoull(str.substr(pos, comma - pos).c_str(), nullptr, 10));
|
|
101
|
+
pos = comma + 1;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
int main(int argc, char** argv) {
|
|
107
|
+
std::vector<size_t> sizes_mib = {8, 32, 128, 512, 1024};
|
|
108
|
+
std::vector<size_t> graph_mib = {1, 4, 16};
|
|
109
|
+
int iters = 30;
|
|
110
|
+
for (int a = 1; a + 1 < argc; a += 2) {
|
|
111
|
+
if (!std::strcmp(argv[a], "--sizes-mib")) sizes_mib = parse_list(argv[a + 1]);
|
|
112
|
+
else if (!std::strcmp(argv[a], "--graph-mib")) graph_mib = parse_list(argv[a + 1]);
|
|
113
|
+
else if (!std::strcmp(argv[a], "--iters")) iters = std::max(5, std::atoi(argv[a + 1]));
|
|
114
|
+
}
|
|
115
|
+
const int launches_per_graph = 32;
|
|
116
|
+
|
|
117
|
+
int dev = 0, sms = 0, l2 = 0, bus_bits = 0, mem_khz = 0, cc_major = 0, cc_minor = 0;
|
|
118
|
+
CHECK(cudaSetDevice(dev));
|
|
119
|
+
CHECK(cudaDeviceGetAttribute(&sms, cudaDevAttrMultiProcessorCount, dev));
|
|
120
|
+
CHECK(cudaDeviceGetAttribute(&l2, cudaDevAttrL2CacheSize, dev));
|
|
121
|
+
CHECK(cudaDeviceGetAttribute(&cc_major, cudaDevAttrComputeCapabilityMajor, dev));
|
|
122
|
+
CHECK(cudaDeviceGetAttribute(&cc_minor, cudaDevAttrComputeCapabilityMinor, dev));
|
|
123
|
+
// Memory clock and bus width are informational; some toolkits deprecate them, so ignore errors.
|
|
124
|
+
if (cudaDeviceGetAttribute(&bus_bits, cudaDevAttrGlobalMemoryBusWidth, dev) != cudaSuccess) bus_bits = 0;
|
|
125
|
+
if (cudaDeviceGetAttribute(&mem_khz, cudaDevAttrMemoryClockRate, dev) != cudaSuccess) mem_khz = 0;
|
|
126
|
+
cudaGetLastError();
|
|
127
|
+
cudaDeviceProp prop;
|
|
128
|
+
CHECK(cudaGetDeviceProperties(&prop, dev));
|
|
129
|
+
// DDR: two transfers per clock. bytes/s = 2 * clock(Hz) * bus width(bytes).
|
|
130
|
+
const double theoretical_gbs = 2.0 * mem_khz * 1e3 * (bus_bits / 8.0) / 1e9;
|
|
131
|
+
std::printf("{\"test\":\"device\",\"name\":\"%s\",\"cc\":\"%d.%d\",\"sms\":%d,\"l2_bytes\":%d,\"bus_bits\":%d,"
|
|
132
|
+
"\"mem_clock_khz\":%d,\"theoretical_gbs\":%.1f}\n",
|
|
133
|
+
prop.name, cc_major, cc_minor, sms, l2, bus_bits, mem_khz, theoretical_gbs);
|
|
134
|
+
|
|
135
|
+
size_t max_bytes = 0;
|
|
136
|
+
for (size_t m : sizes_mib) max_bytes = std::max(max_bytes, m << 20);
|
|
137
|
+
for (size_t m : graph_mib) max_bytes = std::max(max_bytes, (m << 20) * launches_per_graph);
|
|
138
|
+
size_t free_b = 0, total_b = 0;
|
|
139
|
+
CHECK(cudaMemGetInfo(&free_b, &total_b));
|
|
140
|
+
const size_t flush_bytes = std::max<size_t>(size_t(4) * l2, size_t(64) << 20);
|
|
141
|
+
if (2 * max_bytes + flush_bytes > free_b * 9 / 10) {
|
|
142
|
+
std::fprintf(stderr, "not enough free GPU memory for %zu MiB buffers\n", max_bytes >> 20);
|
|
143
|
+
return 2;
|
|
144
|
+
}
|
|
145
|
+
uint4 *a, *b;
|
|
146
|
+
unsigned* sink;
|
|
147
|
+
float* flush;
|
|
148
|
+
CHECK(cudaMalloc(&a, max_bytes));
|
|
149
|
+
CHECK(cudaMalloc(&b, max_bytes));
|
|
150
|
+
CHECK(cudaMalloc(&sink, 4));
|
|
151
|
+
CHECK(cudaMalloc(&flush, flush_bytes));
|
|
152
|
+
CHECK(cudaMemset(a, 1, max_bytes));
|
|
153
|
+
CHECK(cudaMemset(b, 0, max_bytes));
|
|
154
|
+
CHECK(cudaMemset(flush, 0, flush_bytes));
|
|
155
|
+
cudaStream_t s;
|
|
156
|
+
CHECK(cudaStreamCreateWithFlags(&s, cudaStreamNonBlocking));
|
|
157
|
+
cudaEvent_t e0, e1;
|
|
158
|
+
CHECK(cudaEventCreate(&e0));
|
|
159
|
+
CHECK(cudaEventCreate(&e1));
|
|
160
|
+
auto flush_l2 = [&]() { flush_kernel<<<sms * 4, 512, 0, s>>>(flush, flush_bytes / 4); };
|
|
161
|
+
|
|
162
|
+
// ---- read and copy over several buffer sizes
|
|
163
|
+
for (size_t mib : sizes_mib) {
|
|
164
|
+
const size_t bytes = mib << 20;
|
|
165
|
+
const size_t n16 = bytes / 16;
|
|
166
|
+
for (int blocks_per_sm : {4, 8}) {
|
|
167
|
+
std::vector<float> t_read, t_copy;
|
|
168
|
+
for (int it = 0; it < iters + 3; ++it) { // first 3 iterations are warm-up
|
|
169
|
+
float ms = 0;
|
|
170
|
+
flush_l2();
|
|
171
|
+
CHECK(cudaEventRecord(e0, s));
|
|
172
|
+
read_kernel<4><<<sms * blocks_per_sm, 512, 0, s>>>(a, n16, sink, 0);
|
|
173
|
+
CHECK(cudaEventRecord(e1, s));
|
|
174
|
+
CHECK(cudaEventSynchronize(e1));
|
|
175
|
+
CHECK(cudaEventElapsedTime(&ms, e0, e1));
|
|
176
|
+
if (it >= 3) t_read.push_back(ms);
|
|
177
|
+
flush_l2();
|
|
178
|
+
CHECK(cudaEventRecord(e0, s));
|
|
179
|
+
copy_kernel<<<sms * blocks_per_sm, 512, 0, s>>>(a, b, n16 / 2);
|
|
180
|
+
CHECK(cudaEventRecord(e1, s));
|
|
181
|
+
CHECK(cudaEventSynchronize(e1));
|
|
182
|
+
CHECK(cudaEventElapsedTime(&ms, e0, e1));
|
|
183
|
+
if (it >= 3) t_copy.push_back(ms);
|
|
184
|
+
}
|
|
185
|
+
const float mr = median(t_read), mc = median(t_copy);
|
|
186
|
+
const float br = *std::min_element(t_read.begin(), t_read.end());
|
|
187
|
+
std::printf("{\"test\":\"read\",\"bytes\":%zu,\"blocks_per_sm\":%d,\"iters\":%d,\"ms_median\":%.5f,"
|
|
188
|
+
"\"gbs\":%.1f,\"gbs_best\":%.1f}\n",
|
|
189
|
+
bytes, blocks_per_sm, iters, mr, bytes / mr / 1e6, bytes / br / 1e6);
|
|
190
|
+
std::printf("{\"test\":\"copy\",\"bytes\":%zu,\"blocks_per_sm\":%d,\"iters\":%d,\"ms_median\":%.5f,"
|
|
191
|
+
"\"gbs\":%.1f}\n",
|
|
192
|
+
bytes, blocks_per_sm, iters, mc, bytes / mc / 1e6);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---- launch-sized reads inside a CUDA graph
|
|
197
|
+
#if CUDART_VERSION >= 12030 // PDL edges inside captured graphs need CUDA 12.3+
|
|
198
|
+
const bool pdl_ok = cc_major >= 9;
|
|
199
|
+
#else
|
|
200
|
+
const bool pdl_ok = false;
|
|
201
|
+
#endif
|
|
202
|
+
for (size_t mib : graph_mib) {
|
|
203
|
+
const size_t bytes = mib << 20;
|
|
204
|
+
for (int pdl = 0; pdl <= (pdl_ok ? 1 : 0); ++pdl) {
|
|
205
|
+
cudaGraph_t graph;
|
|
206
|
+
cudaGraphExec_t exec;
|
|
207
|
+
CHECK(cudaStreamBeginCapture(s, cudaStreamCaptureModeGlobal));
|
|
208
|
+
for (int l = 0; l < launches_per_graph; ++l) {
|
|
209
|
+
const uint4* src = a + (size_t)l * (bytes / 16); // each launch reads its own region
|
|
210
|
+
#if CUDART_VERSION >= 11080
|
|
211
|
+
cudaLaunchConfig_t cfg = {};
|
|
212
|
+
cfg.gridDim = dim3(sms * 2);
|
|
213
|
+
cfg.blockDim = dim3(512);
|
|
214
|
+
cfg.stream = s;
|
|
215
|
+
cudaLaunchAttribute attr[1];
|
|
216
|
+
attr[0].id = cudaLaunchAttributeProgrammaticStreamSerialization;
|
|
217
|
+
attr[0].val.programmaticStreamSerializationAllowed = 1;
|
|
218
|
+
cfg.attrs = attr;
|
|
219
|
+
cfg.numAttrs = pdl ? 1 : 0;
|
|
220
|
+
CHECK(cudaLaunchKernelEx(&cfg, read_kernel<1>, src, bytes / 16, sink, pdl));
|
|
221
|
+
#else
|
|
222
|
+
read_kernel<1><<<sms * 2, 512, 0, s>>>(src, bytes / 16, sink, 0);
|
|
223
|
+
#endif
|
|
224
|
+
}
|
|
225
|
+
CHECK(cudaStreamEndCapture(s, &graph));
|
|
226
|
+
CHECK(cudaGraphInstantiateWithFlags(&exec, graph, 0));
|
|
227
|
+
std::vector<float> t;
|
|
228
|
+
for (int it = 0; it < iters + 3; ++it) {
|
|
229
|
+
float ms = 0;
|
|
230
|
+
flush_l2();
|
|
231
|
+
CHECK(cudaEventRecord(e0, s));
|
|
232
|
+
CHECK(cudaGraphLaunch(exec, s));
|
|
233
|
+
CHECK(cudaEventRecord(e1, s));
|
|
234
|
+
CHECK(cudaEventSynchronize(e1));
|
|
235
|
+
CHECK(cudaEventElapsedTime(&ms, e0, e1));
|
|
236
|
+
if (it >= 3) t.push_back(ms);
|
|
237
|
+
}
|
|
238
|
+
const float ms = median(t);
|
|
239
|
+
const double total = (double)bytes * launches_per_graph;
|
|
240
|
+
std::printf("{\"test\":\"graph_read\",\"bytes_per_launch\":%zu,\"launches\":%d,\"pdl\":%d,\"iters\":%d,"
|
|
241
|
+
"\"us_per_launch\":%.2f,\"gbs\":%.1f}\n",
|
|
242
|
+
bytes, launches_per_graph, pdl, iters, ms * 1e3 / launches_per_graph, total / ms / 1e6);
|
|
243
|
+
CHECK(cudaGraphExecDestroy(exec));
|
|
244
|
+
CHECK(cudaGraphDestroy(graph));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
CHECK(cudaFree(a));
|
|
248
|
+
CHECK(cudaFree(b));
|
|
249
|
+
CHECK(cudaFree(sink));
|
|
250
|
+
CHECK(cudaFree(flush));
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Bandwidth fallback with PyTorch, used when nvcc is not available.
|
|
2
|
+
|
|
3
|
+
Prints the same JSON lines as bw_peak.cu ("device", "read", "copy"). It runs in its
|
|
4
|
+
own process (``python torch_bw.py --sizes-mib ...``) so the caller never imports
|
|
5
|
+
torch or holds a CUDA context. Expect a few percent below bw_peak.cu: a reduction
|
|
6
|
+
kernel also does arithmetic and a final cross-block step, a pure load loop does not.
|
|
7
|
+
Standard library + torch only; does not import inferencekit.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import statistics
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> int:
|
|
16
|
+
ap = argparse.ArgumentParser()
|
|
17
|
+
ap.add_argument("--sizes-mib", default="8,32,128,512,1024")
|
|
18
|
+
ap.add_argument("--iters", type=int, default=30)
|
|
19
|
+
args = ap.parse_args()
|
|
20
|
+
import torch
|
|
21
|
+
|
|
22
|
+
if not torch.cuda.is_available():
|
|
23
|
+
print(json.dumps({"test": "error", "message": "torch.cuda is not available"}))
|
|
24
|
+
return 2
|
|
25
|
+
props = torch.cuda.get_device_properties(0)
|
|
26
|
+
l2 = getattr(props, "L2_cache_size", 0) or (64 << 20)
|
|
27
|
+
print(json.dumps({"test": "device", "name": props.name, "cc": f"{props.major}.{props.minor}",
|
|
28
|
+
"sms": props.multi_processor_count, "l2_bytes": l2, "theoretical_gbs": None}))
|
|
29
|
+
flush = torch.empty(max(4 * l2, 64 << 20) // 4, dtype=torch.float32, device="cuda")
|
|
30
|
+
start, end = torch.cuda.Event(enable_timing=True), torch.cuda.Event(enable_timing=True)
|
|
31
|
+
|
|
32
|
+
def timed(fn) -> float:
|
|
33
|
+
times = []
|
|
34
|
+
for it in range(args.iters + 3):
|
|
35
|
+
flush.add_(1.0) # evict L2 so the timed op reads DRAM
|
|
36
|
+
start.record()
|
|
37
|
+
fn()
|
|
38
|
+
end.record()
|
|
39
|
+
end.synchronize()
|
|
40
|
+
if it >= 3:
|
|
41
|
+
times.append(start.elapsed_time(end))
|
|
42
|
+
return statistics.median(times)
|
|
43
|
+
|
|
44
|
+
for mib in [int(x) for x in args.sizes_mib.split(",") if x]:
|
|
45
|
+
nbytes = mib << 20
|
|
46
|
+
x = torch.ones(nbytes // 4, dtype=torch.float32, device="cuda")
|
|
47
|
+
y = torch.empty(nbytes // 8, dtype=torch.float32, device="cuda")
|
|
48
|
+
ms = timed(lambda: torch.sum(x))
|
|
49
|
+
print(json.dumps({"test": "read", "bytes": nbytes, "iters": args.iters, "ms_median": ms,
|
|
50
|
+
"gbs": nbytes / ms / 1e6, "method": "torch.sum"}))
|
|
51
|
+
half = x[: nbytes // 8]
|
|
52
|
+
ms = timed(lambda: y.copy_(half))
|
|
53
|
+
print(json.dumps({"test": "copy", "bytes": nbytes, "iters": args.iters, "ms_median": ms,
|
|
54
|
+
"gbs": nbytes / ms / 1e6, "method": "Tensor.copy_"}))
|
|
55
|
+
del x, y
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Benchmark adapters: one per engine.
|
|
2
|
+
|
|
3
|
+
vLLM and SGLang run in a *worker script* executed by the engine's own interpreter
|
|
4
|
+
(``--python /path/to/venv/bin/python``), so inferencekit never has to share a virtual
|
|
5
|
+
environment with an engine and the engine's GPU memory is freed when the worker
|
|
6
|
+
exits. llama.cpp and ``cmd`` are external binaries/commands.
|
|
7
|
+
"""
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Run one engine's benchmark and return a common result shape.
|
|
2
|
+
|
|
3
|
+
Every adapter returns::
|
|
4
|
+
|
|
5
|
+
{"engine": {"name", "version", "commit"}, "flags": {...}, "rounds": [round, ...],
|
|
6
|
+
"tokens": [...] or None, "log_text": str, "limitations": [...], "files": [(path, role)]}
|
|
7
|
+
|
|
8
|
+
where each round is ``{"index", "t_start", "t_end", "ttft_ms", "decode_tok_s",
|
|
9
|
+
"tpot_ms", "gen_tokens"}`` (times in epoch seconds; None when unknown).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
import shlex
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from inferencekit.evidence import redact, redact_text
|
|
21
|
+
from inferencekit.proc import run, which
|
|
22
|
+
from inferencekit.registry import OpError
|
|
23
|
+
|
|
24
|
+
WORKER_DIR = Path(__file__).resolve().parent
|
|
25
|
+
MARK = "INFERKIT_RESULT "
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _redact_file(path: Path) -> None:
|
|
29
|
+
"""Engine logs can echo configuration; scrub secret-looking env values before keeping them."""
|
|
30
|
+
if path.is_file():
|
|
31
|
+
path.write_text(redact_text(path.read_text(errors="replace")))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def run_python_worker(engine: str, python: str, cfg: dict, staging: Path, timeout: int) -> dict:
|
|
35
|
+
"""vLLM / SGLang: run the worker script under the engine's interpreter."""
|
|
36
|
+
cfg_path = staging / "worker_config.json"
|
|
37
|
+
cfg_path.write_text(json.dumps(cfg, indent=2)) # the worker needs the real values while it runs
|
|
38
|
+
out_path, log_path = staging / "worker_stdout.txt", staging / "engine.log"
|
|
39
|
+
r = run([python, str(WORKER_DIR / f"{engine}_worker.py"), str(cfg_path)], timeout=timeout,
|
|
40
|
+
stdout_path=out_path, stderr_path=log_path)
|
|
41
|
+
# The kept copy must not carry a token typed into --engine-args ({"hf_token": ...}):
|
|
42
|
+
# redact() blanks secret-looking keys, redact_text secret env values and the home dir.
|
|
43
|
+
cfg_path.write_text(redact_text(json.dumps(redact(cfg), indent=2)))
|
|
44
|
+
_redact_file(log_path)
|
|
45
|
+
_redact_file(out_path)
|
|
46
|
+
lines = [ln for ln in r.stdout.splitlines() if ln.startswith(MARK)]
|
|
47
|
+
if not lines:
|
|
48
|
+
tail = log_path.read_text(errors="replace")[-1500:] if log_path.is_file() else r.stderr[-1500:]
|
|
49
|
+
raise OpError(f"{engine} worker failed (exit {r.returncode}); log tail:\n{tail}")
|
|
50
|
+
result = json.loads(lines[-1][len(MARK):])
|
|
51
|
+
result["log_text"] = log_path.read_text(errors="replace") if log_path.is_file() else ""
|
|
52
|
+
result["files"] = [(log_path, "engine_log"), (cfg_path, "worker_config")]
|
|
53
|
+
result.setdefault("limitations", [])
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# --------------------------------------------------------------------------- llama.cpp
|
|
58
|
+
|
|
59
|
+
def _llama_flag(key: str) -> str:
|
|
60
|
+
"""{'ngl': 99} -> '-ngl'; {'flash-attn': 1} -> '--flash-attn'; keys with dashes pass through."""
|
|
61
|
+
if key.startswith("-"):
|
|
62
|
+
return key
|
|
63
|
+
return ("-" if len(key) <= 3 else "--") + key
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def run_llamacpp(model: str, prompt_len: int, gen_len: int, rounds: int, engine_args: dict, staging: Path,
|
|
67
|
+
timeout: int) -> dict:
|
|
68
|
+
bench = which("llama-bench")
|
|
69
|
+
if bench:
|
|
70
|
+
return _run_llama_bench(bench, model, prompt_len, gen_len, rounds, engine_args, staging, timeout)
|
|
71
|
+
cli = which("llama-cli")
|
|
72
|
+
if cli:
|
|
73
|
+
return _run_llama_cli(cli, model, prompt_len, gen_len, rounds, engine_args, staging, timeout)
|
|
74
|
+
raise OpError("llama-bench / llama-cli not found on PATH (build llama.cpp with -DGGML_CUDA=ON)")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _run_llama_bench(bench, model, prompt_len, gen_len, rounds, engine_args, staging, timeout) -> dict:
|
|
78
|
+
extra = []
|
|
79
|
+
for k, v in engine_args.items():
|
|
80
|
+
extra += [_llama_flag(k), str(v)]
|
|
81
|
+
argv = [bench, "-m", model, "-p", str(prompt_len), "-n", str(gen_len), "-r", str(rounds), "-o", "json", *extra]
|
|
82
|
+
out_path, log_path = staging / "llama_bench.json", staging / "llama_bench.log"
|
|
83
|
+
t0 = time.time()
|
|
84
|
+
r = run(argv, timeout=timeout, stdout_path=out_path, stderr_path=log_path)
|
|
85
|
+
t1 = time.time()
|
|
86
|
+
_redact_file(log_path)
|
|
87
|
+
if not r.ok:
|
|
88
|
+
raise OpError(f"llama-bench failed (exit {r.returncode}): {redact_text(r.stderr[-1000:])}")
|
|
89
|
+
entries = json.loads(r.stdout)
|
|
90
|
+
pp = next((e for e in entries if e.get("n_prompt", 0) > 0 and e.get("n_gen", 0) == 0), None)
|
|
91
|
+
tg = next((e for e in entries if e.get("n_gen", 0) > 0 and e.get("n_prompt", 0) == 0), None)
|
|
92
|
+
if tg is None:
|
|
93
|
+
raise OpError("llama-bench output has no generation (tg) test")
|
|
94
|
+
tg_ts = tg.get("samples_ts") or [tg.get("avg_ts")]
|
|
95
|
+
pp_ns = (pp or {}).get("samples_ns") or ([pp["avg_ns"]] if pp and pp.get("avg_ns") else [])
|
|
96
|
+
out_rounds = []
|
|
97
|
+
for i, ts in enumerate(tg_ts):
|
|
98
|
+
ttft = pp_ns[i] / 1e6 if i < len(pp_ns) else None
|
|
99
|
+
out_rounds.append({"index": i, "t_start": None, "t_end": None, "ttft_ms": ttft, "decode_tok_s": ts,
|
|
100
|
+
"tpot_ms": 1e3 / ts if ts else None, "gen_tokens": gen_len})
|
|
101
|
+
keys = ("n_gpu_layers", "flash_attn", "type_k", "type_v", "n_batch", "n_ubatch", "n_threads", "backends",
|
|
102
|
+
"split_mode", "gpu_info", "model_type")
|
|
103
|
+
flags = {k: tg.get(k) for k in keys if k in tg}
|
|
104
|
+
limitations = [
|
|
105
|
+
"llama-bench runs prefill (pp) and generation (tg) as separate tests: TTFT here is the pp time without "
|
|
106
|
+
"sampling, and tg decodes from an empty KV cache unless a depth (-d) is passed",
|
|
107
|
+
f"rounds ran back to back inside one llama-bench call ({t0:.0f}-{t1:.0f} epoch s): no per-round timestamps",
|
|
108
|
+
]
|
|
109
|
+
if not tg.get("samples_ts"):
|
|
110
|
+
limitations.append("this llama-bench build reports only the average over repetitions (n=1)")
|
|
111
|
+
return {"engine": {"name": "llama.cpp", "version": str(tg.get("build_number") or "") or None,
|
|
112
|
+
"commit": tg.get("build_commit")},
|
|
113
|
+
"flags": flags, "rounds": out_rounds, "tokens": None, "log_text": r.stderr, "limitations": limitations,
|
|
114
|
+
"files": [(out_path, "raw_output"), (log_path, "engine_log")]}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
_PROMPT_EVAL = re.compile(r"prompt eval time\s*=\s*([\d.]+)\s*ms\s*/\s*(\d+)\s*tokens")
|
|
118
|
+
_EVAL = re.compile(r"(?<!prompt )eval time\s*=\s*([\d.]+)\s*ms\s*/\s*(\d+)\s*(?:runs|tokens)")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _run_llama_cli(cli, model, prompt_len, gen_len, rounds, engine_args, staging, timeout) -> dict:
|
|
122
|
+
extra = []
|
|
123
|
+
for k, v in engine_args.items():
|
|
124
|
+
extra += [_llama_flag(k), str(v)]
|
|
125
|
+
prompt = " ".join(["hello"] * prompt_len) # roughly one token per word; exact length is not controlled
|
|
126
|
+
out_rounds, logs = [], []
|
|
127
|
+
for i in range(rounds):
|
|
128
|
+
argv = [cli, "-m", model, "-p", prompt, "-n", str(gen_len), "--temp", "0", "--ignore-eos", "-no-cnv", *extra]
|
|
129
|
+
t_start = time.time()
|
|
130
|
+
r = run(argv, timeout=timeout)
|
|
131
|
+
if not r.ok and "no-cnv" in r.stderr: # newer builds dropped the flag
|
|
132
|
+
r = run([a for a in argv if a != "-no-cnv"], timeout=timeout)
|
|
133
|
+
t_end = time.time()
|
|
134
|
+
logs.append(r.stderr)
|
|
135
|
+
pe, de = _PROMPT_EVAL.search(r.stderr), _EVAL.search(r.stderr)
|
|
136
|
+
if not de:
|
|
137
|
+
raise OpError(f"could not find llama.cpp timing lines in llama-cli output (exit {r.returncode})")
|
|
138
|
+
ms, runs = float(de.group(1)), int(de.group(2))
|
|
139
|
+
out_rounds.append({"index": i, "t_start": t_start, "t_end": t_end,
|
|
140
|
+
"ttft_ms": float(pe.group(1)) if pe else None,
|
|
141
|
+
"decode_tok_s": runs / (ms / 1e3) if ms > 0 else None,
|
|
142
|
+
"tpot_ms": ms / runs if runs else None, "gen_tokens": runs + 1})
|
|
143
|
+
log_path = staging / "llama_cli.log"
|
|
144
|
+
log_path.write_text(redact_text("\n\n".join(logs)))
|
|
145
|
+
return {"engine": {"name": "llama.cpp", "version": None, "commit": None}, "flags": dict(engine_args),
|
|
146
|
+
"rounds": out_rounds, "tokens": None, "log_text": "\n".join(logs),
|
|
147
|
+
"limitations": ["llama-cli fallback (no llama-bench): prompt length is approximate and TTFT is the "
|
|
148
|
+
"reported prompt-eval time"],
|
|
149
|
+
"files": [(log_path, "engine_log")]}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# --------------------------------------------------------------------------- any command
|
|
153
|
+
|
|
154
|
+
def run_cmd(cmd: str, rounds: int, round_offset: int, timeout: int) -> dict:
|
|
155
|
+
"""Run ``cmd`` once per round; it must print a JSON line containing decode_tok_s.
|
|
156
|
+
|
|
157
|
+
Optional keys in that line: ttft_ms, tpot_ms, gen_tokens, tokens (list of ids),
|
|
158
|
+
flags (dict, e.g. {"cuda_graphs": true, "attention_backend": "FLASH_ATTN"}),
|
|
159
|
+
engine ({"name", "version", "commit"}).
|
|
160
|
+
"""
|
|
161
|
+
argv = shlex.split(cmd)
|
|
162
|
+
out_rounds, last = [], {}
|
|
163
|
+
for k in range(rounds):
|
|
164
|
+
t_start = time.time()
|
|
165
|
+
r = run(argv, timeout=timeout)
|
|
166
|
+
t_end = time.time()
|
|
167
|
+
if not r.ok:
|
|
168
|
+
raise OpError(f"command failed in round {k} (exit {r.returncode}): {redact_text(r.stderr[-800:])}")
|
|
169
|
+
found = None
|
|
170
|
+
for line in reversed(r.stdout.splitlines()):
|
|
171
|
+
line = line.strip()
|
|
172
|
+
if line.startswith("{") and "decode_tok_s" in line:
|
|
173
|
+
try:
|
|
174
|
+
found = json.loads(line)
|
|
175
|
+
break
|
|
176
|
+
except ValueError:
|
|
177
|
+
continue
|
|
178
|
+
if found is None:
|
|
179
|
+
raise OpError(f"round {k}: no JSON line with decode_tok_s in the command's stdout")
|
|
180
|
+
last = found
|
|
181
|
+
tok_s = found.get("decode_tok_s")
|
|
182
|
+
out_rounds.append({"index": round_offset + k, "t_start": t_start, "t_end": t_end,
|
|
183
|
+
"ttft_ms": found.get("ttft_ms"), "decode_tok_s": tok_s,
|
|
184
|
+
"tpot_ms": found.get("tpot_ms") or (1e3 / tok_s if tok_s else None),
|
|
185
|
+
"gen_tokens": found.get("gen_tokens")})
|
|
186
|
+
engine = last.get("engine") if isinstance(last.get("engine"), dict) else {}
|
|
187
|
+
return {"engine": {"name": engine.get("name") or "cmd", "version": engine.get("version"),
|
|
188
|
+
"commit": engine.get("commit")},
|
|
189
|
+
"flags": last.get("flags") if isinstance(last.get("flags"), dict) else {},
|
|
190
|
+
"rounds": out_rounds, "tokens": last.get("tokens"), "log_text": "", "limitations": [], "files": []}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Video clip engines behind the clip HTTP contract, and a ComfyUI client.
|
|
2
|
+
|
|
3
|
+
server.py POST /v1/clips and GET /health around a pipeline object; runs in the pipeline's own
|
|
4
|
+
virtual environment (standard library plus whatever the pipeline imports).
|
|
5
|
+
noise.py the initial-noise recipe: drawn on the CPU from one seed, so every engine starts from
|
|
6
|
+
the same tensors.
|
|
7
|
+
diffusers_ltx.py LTX-2.x through Diffusers (text-to-video and image-to-video), BF16 reference mode.
|
|
8
|
+
official_ltx.py the official LTX-2 pipelines package, when it is installed (guarded import).
|
|
9
|
+
comfyui.py a client for a ComfyUI server (POST /prompt with a pinned API-format workflow), and the
|
|
10
|
+
``comfyui`` backend bench clip drives.
|
|
11
|
+
|
|
12
|
+
Nothing here is imported by the rest of inferencekit unless a clip command asks for it.
|
|
13
|
+
"""
|