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
12 changes: 12 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Revisions for `git blame` to skip (mechanical changes, no authorship signal).
#
# Enable locally: git config blame.ignoreRevsFile .git-blame-ignore-revs
# GitHub honors this file automatically on the default branch.
#
# IMPORTANT: each SHA must be the commit as it lands on `main`. These entries
# assume the PR merges with a *merge commit* (which preserves the SHA). A squash
# or rebase merge rewrites the SHA and would make the entry silently match
# nothing.

# Tree-wide clang-format (clang-format 22.1.5; see scripts/ci/clang-format.sh).
906d1088483418bbe347993fd909af054893fcd8
24 changes: 24 additions & 0 deletions .github/workflows/clang-format.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: clang-format

on:
pull_request:
branches: [main]
push:
branches: [main]

# Supersede in-flight runs on the same ref.
concurrency:
group: clang-format-${{ github.ref }}
cancel-in-progress: true

jobs:
clang-format:
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8.2.0
- name: clang-format gate
# Our C/C++ must match the pinned clang-format. Scope (vendored excluded)
# and the pinned version both live in scripts/ci/clang-format.sh.
run: scripts/ci/clang-format.sh --check
15 changes: 15 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ uv run --project scripts/envs/<family> scripts/<script>.py ...
cmake --build build --target transcribe-cli
```

## Formatting

- Format our C/C++ before committing. The formatter is pinned and fetched via
`uvx`, so do not rely on a system clang-format:

```bash
scripts/ci/clang-format.sh # format our tree in place (default)
scripts/ci/clang-format.sh --check # verify, no changes
```

- Scope is our code only. Vendored trees (`ggml/`, `src/third_party/`) and
verbatim upstream copies (`src/transcribe-unicode-data.cpp`) are never
formatted. CI gates our C/C++ in
`.github/workflows/clang-format.yml`.

## Verification

- End-to-end numerical checks:
Expand Down
21 changes: 18 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,23 @@ Formatting rules:
- Use `void * ptr` and `int & a` pointer/reference spacing.
- Use vertical alignment when it improves readability and batch editing.
- Clean up trailing whitespace.
- Use the root `.clang-format` for new files and touched hunks. It is based on
llama.cpp's clang-format profile. Do not reformat unrelated code as part of a
behavior change.
- Format with the pinned formatter rather than a system clang-format:

```bash
scripts/ci/clang-format.sh # format our tree in place
scripts/ci/clang-format.sh --check # verify only, no changes
```

It wraps clang-format `22.1.5` (fetched via `uvx`) against the root
`.clang-format`, a profile based on llama.cpp's. The version is pinned in the
script so local output matches CI byte-for-byte; bump it there and reformat
the tree in the same commit.
- Formatting scope is our C/C++ only. Vendored trees (`ggml/`,
`src/third_party/`) and verbatim upstream copies
(`src/transcribe-unicode-data.cpp`) are never reformatted.
- Do not reformat unrelated code as part of a behavior change. CI enforces this
through the `clang-format` workflow (`.github/workflows/clang-format.yml`),
which gates our C/C++ against the pinned formatter.

Naming rules:

Expand Down Expand Up @@ -206,6 +220,7 @@ Required before merge:

| Gate | Command / owner | Requirement |
| --- | --- | --- |
| Formatting | `scripts/ci/clang-format.sh --check` (CI: clang-format workflow) | All our C/C++ matches the pinned clang-format |
| Intake signoff | human review | Schema-valid intake; `reference_framework`, `architecture_pattern`, and `known_risks` reviewed; dtype/frontend/tokenizer gaps resolved or explicitly accepted |
| Preflight A | `uv run scripts/preflight.py --family <f> [--variant <v>] --gate A` | Pass, or warnings tied to accepted intake gaps |
| Preflight B | `uv run scripts/preflight.py --family <f> [--variant <v>] --gate B` | Pass after converter exists |
Expand Down
157 changes: 95 additions & 62 deletions examples/bench/batch_bench.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,30 +30,42 @@ namespace {

double now_ms() {
using namespace std::chrono;
return duration<double, std::milli>(
steady_clock::now().time_since_epoch())
.count();
return duration<double, std::milli>(steady_clock::now().time_since_epoch()).count();
}

transcribe_backend_request parse_backend(const char * s) {
if (!std::strcmp(s, "cpu")) return TRANSCRIBE_BACKEND_CPU;
if (!std::strcmp(s, "cpu_accel")) return TRANSCRIBE_BACKEND_CPU_ACCEL;
if (!std::strcmp(s, "metal")) return TRANSCRIBE_BACKEND_METAL;
if (!std::strcmp(s, "vulkan")) return TRANSCRIBE_BACKEND_VULKAN;
if (!std::strcmp(s, "cuda")) return TRANSCRIBE_BACKEND_CUDA;
if (!std::strcmp(s, "cpu")) {
return TRANSCRIBE_BACKEND_CPU;
}
if (!std::strcmp(s, "cpu_accel")) {
return TRANSCRIBE_BACKEND_CPU_ACCEL;
}
if (!std::strcmp(s, "metal")) {
return TRANSCRIBE_BACKEND_METAL;
}
if (!std::strcmp(s, "vulkan")) {
return TRANSCRIBE_BACKEND_VULKAN;
}
if (!std::strcmp(s, "cuda")) {
return TRANSCRIBE_BACKEND_CUDA;
}
return TRANSCRIBE_BACKEND_AUTO;
}

std::vector<int> parse_sizes(const std::string & csv) {
std::vector<int> out;
size_t i = 0;
size_t i = 0;
while (i < csv.size()) {
size_t j = csv.find(',', i);
if (j == std::string::npos) j = csv.size();
if (j == std::string::npos) {
j = csv.size();
}
const std::string tok = csv.substr(i, j - i);
if (!tok.empty()) {
const int n = std::atoi(tok.c_str());
if (n > 0) out.push_back(n);
if (n > 0) {
out.push_back(n);
}
}
i = j + 1;
}
Expand All @@ -63,94 +75,114 @@ std::vector<int> parse_sizes(const std::string & csv) {
std::string json_escape(const std::string & s) {
std::string o;
for (char c : s) {
if (c == '"' || c == '\\') { o += '\\'; o += c; }
else if (c == '\n') { o += "\\n"; }
else { o += c; }
if (c == '"' || c == '\\') {
o += '\\';
o += c;
} else if (c == '\n') {
o += "\\n";
} else {
o += c;
}
}
return o;
}

} // namespace
} // namespace

int main(int argc, char ** argv) {
std::string model_path;
std::string wav_path;
std::string out_path;
std::string sizes_csv = "1,2,4,8,16,32";
transcribe_backend_request backend = TRANSCRIBE_BACKEND_AUTO;
int iters = 3;
int n_threads = 0;
std::string model_path;
std::string wav_path;
std::string out_path;
std::string sizes_csv = "1,2,4,8,16,32";
transcribe_backend_request backend = TRANSCRIBE_BACKEND_AUTO;
int iters = 3;
int n_threads = 0;

for (int i = 1; i < argc; ++i) {
const std::string a = argv[i];
auto next = [&](const char * flag) -> const char * {
const std::string a = argv[i];
auto next = [&](const char * flag) -> const char * {
if (i + 1 >= argc) {
std::fprintf(stderr, "error: %s needs a value\n", flag);
std::exit(2);
}
return argv[++i];
};
if (a == "-m" || a == "--model") model_path = next(a.c_str());
else if (a == "--backend") backend = parse_backend(next(a.c_str()));
else if (a == "--batch-sizes") sizes_csv = next(a.c_str());
else if (a == "--iters") iters = std::atoi(next(a.c_str()));
else if (a == "--threads") n_threads = std::atoi(next(a.c_str()));
else if (a == "--out") out_path = next(a.c_str());
else if (!a.empty() && a[0] != '-') wav_path = a;
else { std::fprintf(stderr, "unknown arg: %s\n", a.c_str()); return 2; }
if (a == "-m" || a == "--model") {
model_path = next(a.c_str());
} else if (a == "--backend") {
backend = parse_backend(next(a.c_str()));
} else if (a == "--batch-sizes") {
sizes_csv = next(a.c_str());
} else if (a == "--iters") {
iters = std::atoi(next(a.c_str()));
} else if (a == "--threads") {
n_threads = std::atoi(next(a.c_str()));
} else if (a == "--out") {
out_path = next(a.c_str());
} else if (!a.empty() && a[0] != '-') {
wav_path = a;
} else {
std::fprintf(stderr, "unknown arg: %s\n", a.c_str());
return 2;
}
}
if (model_path.empty() || wav_path.empty()) {
std::fprintf(stderr,
"usage: %s -m model.gguf clip.wav [--backend B] "
"[--batch-sizes 1,2,4,8] [--iters N] [--threads N] [--out f.json]\n",
argv[0]);
"usage: %s -m model.gguf clip.wav [--backend B] "
"[--batch-sizes 1,2,4,8] [--iters N] [--threads N] [--out f.json]\n",
argv[0]);
return 2;
}
if (iters < 1) iters = 1;
if (iters < 1) {
iters = 1;
}

struct transcribe_model_load_params lp; transcribe_model_load_params_init(&lp);
struct transcribe_model_load_params lp;
transcribe_model_load_params_init(&lp);
lp.backend = backend;
struct transcribe_session_params sp; transcribe_session_params_init(&sp);
sp.n_threads = n_threads;
struct transcribe_session_params sp;
transcribe_session_params_init(&sp);
sp.n_threads = n_threads;
struct transcribe_session * s = nullptr;
if (auto st = transcribe_open(model_path.c_str(), &lp, &sp, &s);
st != TRANSCRIBE_OK)
{
if (auto st = transcribe_open(model_path.c_str(), &lp, &sp, &s); st != TRANSCRIBE_OK) {
std::fprintf(stderr, "open failed: %s\n", transcribe_status_string(st));
return 1;
}
const char * dev = transcribe_model_backend(transcribe_get_model(s));

std::vector<float> pcm;
std::string err;
std::string err;
if (!transcribe_cli::load_wav_mono_16k(wav_path, pcm, err)) {
std::fprintf(stderr, "wav: %s\n", err.c_str());
transcribe_session_free(s);
return 1;
}
const double audio_s = (double)pcm.size() / 16000.0;
const std::vector<int> sizes = parse_sizes(sizes_csv);
const double audio_s = (double) pcm.size() / 16000.0;
const std::vector<int> sizes = parse_sizes(sizes_csv);

std::string json = "[\n";
for (size_t si = 0; si < sizes.size(); ++si) {
const int N = sizes[si];
const int N = sizes[si];
std::vector<const float *> pcm_ptrs(N, pcm.data());
std::vector<int> n_samps(N, (int)pcm.size());
std::vector<int> n_samps(N, (int) pcm.size());

// Warmup (allocates the batch graph / buffers for this shape).
auto wst = transcribe_run_batch(s, pcm_ptrs.data(), n_samps.data(), N, nullptr);
auto wst = transcribe_run_batch(s, pcm_ptrs.data(), n_samps.data(), N, nullptr);
std::string row_err;
if (wst != TRANSCRIBE_OK) {
row_err = transcribe_status_string(wst);
}

const double t_sweep0 = now_ms();
double wall_sum = 0.0;
double wall_sum = 0.0;
for (int it = 0; it < iters && row_err.empty(); ++it) {
const double t0 = now_ms();
auto st = transcribe_run_batch(s, pcm_ptrs.data(), n_samps.data(), N, nullptr);
auto st = transcribe_run_batch(s, pcm_ptrs.data(), n_samps.data(), N, nullptr);
const double t1 = now_ms();
if (st != TRANSCRIBE_OK) { row_err = transcribe_status_string(st); break; }
if (st != TRANSCRIBE_OK) {
row_err = transcribe_status_string(st);
break;
}
wall_sum += (t1 - t0);
}
const double elapsed_s = (now_ms() - t_sweep0) / 1000.0;
Expand All @@ -159,24 +191,25 @@ int main(int argc, char ** argv) {

char buf[512];
std::snprintf(buf, sizeof(buf),
" {\"n_batch\": %d, \"wall_mean_ms\": %.3f, \"per_utt_ms\": %.4f, "
"\"audio_s\": %.3f, \"n_iters\": %d, \"elapsed_s\": %.4f, "
"\"backend\": \"%s\", \"error\": %s}%s\n",
N, wall_mean, per_utt, audio_s, iters, elapsed_s,
dev ? dev : "",
row_err.empty() ? "null" : ("\"" + json_escape(row_err) + "\"").c_str(),
(si + 1 < sizes.size()) ? "," : "");
" {\"n_batch\": %d, \"wall_mean_ms\": %.3f, \"per_utt_ms\": %.4f, "
"\"audio_s\": %.3f, \"n_iters\": %d, \"elapsed_s\": %.4f, "
"\"backend\": \"%s\", \"error\": %s}%s\n",
N, wall_mean, per_utt, audio_s, iters, elapsed_s, dev ? dev : "",
row_err.empty() ? "null" : ("\"" + json_escape(row_err) + "\"").c_str(),
(si + 1 < sizes.size()) ? "," : "");
json += buf;

std::fprintf(stderr,
"n_batch=%-4d per_utt=%8.2f ms wall=%9.2f ms %s\n",
N, per_utt, wall_mean, row_err.empty() ? "" : row_err.c_str());
std::fprintf(stderr, "n_batch=%-4d per_utt=%8.2f ms wall=%9.2f ms %s\n", N, per_utt, wall_mean,
row_err.empty() ? "" : row_err.c_str());
}
json += "]\n";

if (!out_path.empty()) {
FILE * f = std::fopen(out_path.c_str(), "w");
if (f) { std::fputs(json.c_str(), f); std::fclose(f); }
if (f) {
std::fputs(json.c_str(), f);
std::fclose(f);
}
std::fprintf(stderr, "wrote %s\n", out_path.c_str());
} else {
std::fputs(json.c_str(), stdout);
Expand Down
Loading
Loading