#include "transcribe-debug.h"
#include "ggml.h"
#include "ggml-backend.h"
#include <cmath>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <ios>
#include <limits>
#include <new>
#include <string>
#include <vector>
namespace transcribe::debug {
namespace {
bool g_initialized = false;
bool g_enabled = false;
std::string g_dump_dir;
std::vector<std::string> g_name_prefix_stack;
std::string make_prefixed_name(const char * name) {
if (g_name_prefix_stack.empty()) return std::string(name);
return g_name_prefix_stack.back() + name;
}
bool name_is_safe(const char * name) {
if (name == nullptr || name[0] == '\0') {
return false;
}
for (const char * p = name; *p; ++p) {
const char c = *p;
if (c == '/' || c == '\\') {
return false;
}
if (static_cast<unsigned char>(c) < 0x20) {
return false;
}
}
return true;
}
std::vector<int64_t> row_major_shape(const ggml_tensor * t) {
std::vector<int64_t> out;
int last = GGML_MAX_DIMS - 1;
while (last > 0 && t->ne[last] == 1) {
--last;
}
out.reserve(static_cast<size_t>(last + 1));
for (int i = last; i >= 0; --i) {
out.push_back(t->ne[i]);
}
return out;
}
void write_json_float(std::ofstream & js, double v) {
if (std::isnan(v) || std::isinf(v)) {
js << "null";
} else {
js << v;
}
}
void warn(const char * fmt, ...) {
std::va_list ap;
va_start(ap, fmt);
std::fprintf(stderr, "transcribe-debug: ");
std::vfprintf(stderr, fmt, ap);
std::fprintf(stderr, "\n");
va_end(ap);
}
}
bool init() {
if (g_initialized) {
return g_enabled;
}
g_initialized = true;
g_enabled = false;
const char * env = std::getenv("TRANSCRIBE_DUMP_DIR");
if (env == nullptr || env[0] == '\0') {
return false;
}
g_dump_dir = env;
g_enabled = true;
std::fprintf(stderr,
"transcribe-debug: dumping tensors to %s\n",
g_dump_dir.c_str());
return true;
}
bool enabled() {
return g_initialized && g_enabled;
}
const char * dump_dir() {
return enabled() ? g_dump_dir.c_str() : nullptr;
}
void push_name_prefix(const char * prefix) {
if (!enabled() || prefix == nullptr) return;
g_name_prefix_stack.emplace_back(prefix);
}
void pop_name_prefix() {
if (!enabled()) return;
if (!g_name_prefix_stack.empty()) g_name_prefix_stack.pop_back();
}
void mark_tensor_for_dump(ggml_tensor * tensor) {
if (enabled() && tensor != nullptr) {
ggml_set_output(tensor);
}
}
void dump_tensor(const char * name,
const ggml_tensor * tensor,
const char * stage)
{
if (!enabled()) {
return;
}
if (tensor == nullptr) {
warn("dump_tensor(\"%s\"): null tensor", name ? name : "(null)");
return;
}
if (!name_is_safe(name)) {
warn("dump_tensor: rejecting unsafe name \"%s\"",
name ? name : "(null)");
return;
}
if (tensor->type != GGML_TYPE_F32) {
warn("dump_tensor(\"%s\"): only fp32 supported, got %s",
name, ggml_type_name(tensor->type));
return;
}
const size_t nbytes = ggml_nbytes(tensor);
if (nbytes == 0) {
warn("dump_tensor(\"%s\"): tensor has 0 bytes", name);
return;
}
if (nbytes % sizeof(float) != 0) {
warn("dump_tensor(\"%s\"): nbytes (%zu) not a multiple of sizeof(float)",
name, nbytes);
return;
}
std::vector<uint8_t> host;
try {
host.resize(nbytes);
} catch (const std::bad_alloc &) {
warn("dump_tensor(\"%s\"): malloc failed for %zu bytes",
name, nbytes);
return;
}
ggml_backend_tensor_get(tensor, host.data(), 0, nbytes);
const size_t n_elem = nbytes / sizeof(float);
const float * fdata = reinterpret_cast<const float *>(host.data());
float vmin = std::numeric_limits<float>::infinity();
float vmax = -std::numeric_limits<float>::infinity();
double vsum = 0.0;
for (size_t i = 0; i < n_elem; ++i) {
const float v = fdata[i];
if (v < vmin) vmin = v;
if (v > vmax) vmax = v;
vsum += static_cast<double>(v);
}
const double vmean = vsum / static_cast<double>(n_elem);
const std::string full_name = make_prefixed_name(name);
const std::string f32_path = g_dump_dir + "/" + full_name + ".f32";
const std::string json_path = g_dump_dir + "/" + full_name + ".json";
{
std::ofstream f32(f32_path, std::ios::binary | std::ios::trunc);
if (!f32) {
warn("dump_tensor(\"%s\"): cannot open %s for write",
name, f32_path.c_str());
return;
}
f32.write(reinterpret_cast<const char *>(host.data()),
static_cast<std::streamsize>(nbytes));
if (!f32) {
warn("dump_tensor(\"%s\"): write failed for %s",
name, f32_path.c_str());
return;
}
}
{
std::ofstream js(json_path, std::ios::trunc);
if (!js) {
warn("dump_tensor(\"%s\"): cannot open %s for write",
name, json_path.c_str());
return;
}
const std::vector<int64_t> shape = row_major_shape(tensor);
js << "{\n";
js << " \"name\": \"" << name << "\",\n";
if (stage != nullptr && stage[0] != '\0') {
js << " \"stage\": \"" << stage << "\",\n";
}
js << " \"shape\": [";
for (size_t i = 0; i < shape.size(); ++i) {
if (i) js << ", ";
js << shape[i];
}
js << "],\n";
js << " \"dtype\": \"f32\",\n";
js << " \"layout\": \"row-major\",\n";
js.precision(9);
js << " \"min\": "; write_json_float(js, vmin); js << ",\n";
js << " \"max\": "; write_json_float(js, vmax); js << ",\n";
js << " \"mean\": "; write_json_float(js, vmean); js << ",\n";
js << " \"source\": { \"kind\": \"cpp\" }\n";
js << "}\n";
if (!js) {
warn("dump_tensor(\"%s\"): write failed for %s",
name, json_path.c_str());
return;
}
}
}
void dump_host_f32(const char * name,
const float * data,
long long n_elem,
const long long * shape,
int n_dims,
const char * stage)
{
if (!enabled()) {
return;
}
if (data == nullptr || n_elem <= 0) {
warn("dump_host_f32(\"%s\"): null data or zero elements",
name ? name : "(null)");
return;
}
if (!name_is_safe(name)) {
warn("dump_host_f32: rejecting unsafe name \"%s\"",
name ? name : "(null)");
return;
}
if (shape == nullptr || n_dims <= 0) {
warn("dump_host_f32(\"%s\"): missing shape", name);
return;
}
long long shape_product = 1;
for (int i = 0; i < n_dims; ++i) {
if (shape[i] <= 0) {
warn("dump_host_f32(\"%s\"): non-positive shape[%d]=%lld",
name, i, shape[i]);
return;
}
shape_product *= shape[i];
}
if (shape_product != n_elem) {
warn("dump_host_f32(\"%s\"): shape product %lld != n_elem %lld",
name, shape_product, n_elem);
return;
}
const size_t nbytes = static_cast<size_t>(n_elem) * sizeof(float);
float vmin = std::numeric_limits<float>::infinity();
float vmax = -std::numeric_limits<float>::infinity();
double vsum = 0.0;
for (long long i = 0; i < n_elem; ++i) {
const float v = data[i];
if (v < vmin) vmin = v;
if (v > vmax) vmax = v;
vsum += static_cast<double>(v);
}
const double vmean = vsum / static_cast<double>(n_elem);
const std::string full_name = make_prefixed_name(name);
const std::string f32_path = g_dump_dir + "/" + full_name + ".f32";
const std::string json_path = g_dump_dir + "/" + full_name + ".json";
{
std::ofstream f32(f32_path, std::ios::binary | std::ios::trunc);
if (!f32) {
warn("dump_host_f32(\"%s\"): cannot open %s for write",
name, f32_path.c_str());
return;
}
f32.write(reinterpret_cast<const char *>(data),
static_cast<std::streamsize>(nbytes));
if (!f32) {
warn("dump_host_f32(\"%s\"): write failed for %s",
name, f32_path.c_str());
return;
}
}
{
std::ofstream js(json_path, std::ios::trunc);
if (!js) {
warn("dump_host_f32(\"%s\"): cannot open %s for write",
name, json_path.c_str());
return;
}
js << "{\n";
js << " \"name\": \"" << full_name << "\",\n";
if (stage != nullptr && stage[0] != '\0') {
js << " \"stage\": \"" << stage << "\",\n";
}
js << " \"shape\": [";
for (int i = 0; i < n_dims; ++i) {
if (i) js << ", ";
js << shape[i];
}
js << "],\n";
js << " \"dtype\": \"f32\",\n";
js << " \"layout\": \"row-major\",\n";
js.precision(9);
js << " \"min\": "; write_json_float(js, vmin); js << ",\n";
js << " \"max\": "; write_json_float(js, vmax); js << ",\n";
js << " \"mean\": "; write_json_float(js, vmean); js << ",\n";
js << " \"source\": { \"kind\": \"cpp\" }\n";
js << "}\n";
if (!js) {
warn("dump_host_f32(\"%s\"): write failed for %s",
name, json_path.c_str());
return;
}
}
}
}