436 lines
20 KiB
C
436 lines
20 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file bench_tt_prefill_quantum.c
|
|
* @brief THE THREE MEASUREMENTS PHASE 4 IS GATED ON, on Ornith's own strips.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* WHY THIS EXISTS BEFORE ANY PREFILL CODE
|
|
* ---------------------------------------------------------------------------
|
|
* A batched prefill for this model is not the llama one. Ornith is 24 Gated-Delta-Net layers
|
|
* and 8 attention layers, and BOTH mixers are single-position on the card by construction:
|
|
* the GDN op advances a carried state, and `pn_tt_rope` / `pn_tt_kv_append` / flash decode
|
|
* each take ONE position. So a 32-row pass can batch the norms, the projections, the SwiGLU
|
|
* and the residual adds, and it must then hand each of the 32 rows to a mixer that reads
|
|
* LOGICAL ROW 0. There is no on-card row gather, so the only way to reach row `r` is to bring
|
|
* the strip down and put row `r` back up as row 0.
|
|
*
|
|
* That makes the whole design turn on ONE number nobody here has measured: **what a
|
|
* WHOLE-TILE download costs against a row-0 download.** The row-0 reader moves 128 B a tile
|
|
* (two 64 B chunks); the batched reader moves 2048 B. If the link is transaction-bound the
|
|
* batched read is nearly free per token and a host-mediated row select is affordable; if it
|
|
* is byte-bound at the row-0 rate the design is dead before it is written, because Phase A
|
|
* spent this whole program removing exactly this traffic.
|
|
*
|
|
* Three things are timed, all on the strips the shipped forward already owns:
|
|
*
|
|
* 1. **TRANSFERS** — `pn_tt_activation_{upload,download}` (row 0) against
|
|
* `..._rows` (32 rows), at Ornith's four real widths. Reported as µs and as B/s in both
|
|
* directions, because this project has one measured figure for the card->host direction
|
|
* (~5 MB/s at 128 B a tile) and none at all for whole tiles.
|
|
*
|
|
* 2. **THE TILE QUANTUM, MEASURED RATHER THAN INHERITED** — the same projection with rows
|
|
* 1..31 ZERO and with rows 1..31 carrying real activations. The descriptor is identical
|
|
* either way, so the claim "32 positions cost what one position costs" is a claim about
|
|
* DATA DEPENDENCE, not about descriptors — and this card has already produced one
|
|
* 3.7x data-dependent timing (PHASE CONV, 1222.7 µs on real activations against 4571 on
|
|
* a degenerate stream). A tile quantum that holds for a `[K+1, N]` verify strip of
|
|
* near-duplicate rows need not hold for 32 unrelated prompt positions.
|
|
*
|
|
* 3. **THE SEQUENTIAL TERMS** — one GDN block (the strip bridge, six descriptors) and one
|
|
* on-card attention block, timed alone. These are the terms a 32-row pass CANNOT batch,
|
|
* so they set the ceiling of the whole item: 32 rows cost `flat + 32 * sequential`.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* PROVING IT CAN FAIL
|
|
* ---------------------------------------------------------------------------
|
|
* `--selftest` runs the transfer arms against a KNOWN answer: it uploads 32 distinct rows,
|
|
* reads them back with the batched reader and compares, and reads row 0 with the row-0 reader
|
|
* and compares that. A bench whose transfers do not move the right bytes is measuring the
|
|
* cost of being wrong. It also checks that `pn_tt_activation_upload_rows` re-zeroes rows
|
|
* `rows..31`, which is the strip invariant every Mt=1 op downstream depends on.
|
|
*
|
|
* @status Working
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#include "../include/pantheon.h"
|
|
#include "../src/core/engine.h"
|
|
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#ifndef USE_TT
|
|
int main(void)
|
|
{
|
|
fprintf(stderr, "bench_tt_prefill_quantum: built without USE_TT — nothing to measure.\n");
|
|
return 77;
|
|
}
|
|
#else
|
|
|
|
#include "../src/models/qwen35moe.h"
|
|
#include "../src/models/qwen35moe_tt_internal.h"
|
|
|
|
#define BENCH_ROWS 32u
|
|
|
|
static double bench_now(void)
|
|
{
|
|
struct timespec ts;
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
|
|
}
|
|
|
|
/** A deterministic stream. Distinct per (row, column) so a transfer that moves row 0
|
|
* everywhere, or transposes, cannot pass the self-test. */
|
|
static void bench_fill(float *values, uint32_t rows, uint32_t width, uint32_t salt)
|
|
{
|
|
for (uint32_t r = 0; r < rows; r++)
|
|
for (uint32_t c = 0; c < width; c++)
|
|
values[(size_t)r * width + c] =
|
|
(float)((int)((r * 7919u + c * 104729u + salt) % 2003u) - 1000) * 0.0009765625f;
|
|
}
|
|
|
|
/** Time `reps` calls of a transfer and print µs and B/s. `bytes` is the payload the link
|
|
* actually moved, which for the row-0 reader is 128 B a tile and NOT `width * 4`. */
|
|
static void bench_report(const char *what, uint32_t width, uint32_t rows, double seconds,
|
|
int reps, double bytes_per_call)
|
|
{
|
|
double per = seconds / (double)reps;
|
|
|
|
printf("QUANTUM %-28s width %5u rows %2u %9.1f us/call %8.2f MB/s (%.0f B/call)\n",
|
|
what, width, rows, per * 1e6, bytes_per_call / per / 1e6, bytes_per_call);
|
|
}
|
|
|
|
/** The card-side bytes a strip transfer moves: whole tiles for the batched pair, two 64-byte
|
|
* chunks a tile for the row-0 pair. bf16 strips are 2048 B a tile. */
|
|
static double bench_bytes(uint32_t tiles, uint32_t rows, uint32_t tile_bytes)
|
|
{
|
|
return rows > 1u ? (double)tiles * (double)tile_bytes : (double)tiles * 128.0;
|
|
}
|
|
|
|
static int bench_transfers(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, int reps)
|
|
{
|
|
struct { const char *name; const pn_tt_activation *strip; uint32_t width; } arms[] = {
|
|
{ "hidden (n_embd)", &g->card.hidden[0], (uint32_t)cfg->n_embd },
|
|
{ "ssm_qkv (conv_dim)", &g->card.ssm_qkv, g->card.ssm_qkv.tile_count * 32u },
|
|
{ "ssm_gated", &g->card.ssm_gated, g->card.ssm_gated.tile_count * 32u },
|
|
{ "ffn_swiglu (n_ff)", &g->card.ffn_swiglu, g->card.ffn_swiglu.tile_count * 32u },
|
|
};
|
|
size_t widest = 0;
|
|
float *host;
|
|
|
|
for (size_t i = 0; i < sizeof(arms) / sizeof(arms[0]); i++)
|
|
if (arms[i].width > widest)
|
|
widest = arms[i].width;
|
|
host = calloc((size_t)BENCH_ROWS * widest, sizeof(float));
|
|
if (!host)
|
|
return -1;
|
|
|
|
for (size_t i = 0; i < sizeof(arms) / sizeof(arms[0]); i++) {
|
|
const pn_tt_activation *strip = arms[i].strip;
|
|
uint32_t width = arms[i].width;
|
|
uint32_t tiles = strip->tile_count;
|
|
double mark;
|
|
|
|
if (tiles == 0u)
|
|
continue;
|
|
bench_fill(host, BENCH_ROWS, width, (uint32_t)i);
|
|
|
|
mark = bench_now();
|
|
for (int r = 0; r < reps; r++)
|
|
if (pn_tt_activation_upload(g->device, strip, host, width) != 0)
|
|
goto fail;
|
|
bench_report("upload row0", width, 1u, bench_now() - mark, reps,
|
|
bench_bytes(tiles, 1u, 2048u));
|
|
|
|
mark = bench_now();
|
|
for (int r = 0; r < reps; r++)
|
|
if (pn_tt_activation_upload_rows(g->device, strip, host, width, BENCH_ROWS) != 0)
|
|
goto fail;
|
|
bench_report("upload 32 rows", width, BENCH_ROWS, bench_now() - mark, reps,
|
|
bench_bytes(tiles, BENCH_ROWS, 2048u));
|
|
|
|
mark = bench_now();
|
|
for (int r = 0; r < reps; r++)
|
|
if (pn_tt_activation_download(g->device, strip, host, width) != 0)
|
|
goto fail;
|
|
bench_report("download row0", width, 1u, bench_now() - mark, reps,
|
|
bench_bytes(tiles, 1u, 2048u));
|
|
|
|
mark = bench_now();
|
|
for (int r = 0; r < reps; r++)
|
|
if (pn_tt_activation_download_rows(g->device, strip, host, width, BENCH_ROWS) != 0)
|
|
goto fail;
|
|
bench_report("download 32 rows", width, BENCH_ROWS, bench_now() - mark, reps,
|
|
bench_bytes(tiles, BENCH_ROWS, 2048u));
|
|
}
|
|
free(host);
|
|
return 0;
|
|
fail:
|
|
free(host);
|
|
fprintf(stderr, "QUANTUM REFUSED: a transfer failed\n");
|
|
return -1;
|
|
}
|
|
|
|
/** THE SELF-TEST. Without it the timings above are the cost of moving unknown bytes. */
|
|
static int bench_selftest(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg)
|
|
{
|
|
const pn_tt_activation *strip = &g->card.hidden[0];
|
|
uint32_t width = (uint32_t)cfg->n_embd;
|
|
float *sent = calloc((size_t)BENCH_ROWS * width, sizeof(float));
|
|
float *got = calloc((size_t)BENCH_ROWS * width, sizeof(float));
|
|
int bad_rows = 0, bad_row0 = 0, bad_zero = 0, rc = -1;
|
|
|
|
if (!sent || !got)
|
|
goto done;
|
|
bench_fill(sent, BENCH_ROWS, width, 0x5eedu);
|
|
if (pn_tt_activation_upload_rows(g->device, strip, sent, width, BENCH_ROWS) != 0)
|
|
goto done;
|
|
if (pn_tt_activation_download_rows(g->device, strip, got, width, BENCH_ROWS) != 0)
|
|
goto done;
|
|
/* bf16 on the card, fp32 here: the round trip is a TRUNCATION, so agreement is to the
|
|
* bf16 grid and not to the bit. A row that came back as another row would be off by far
|
|
* more than that, which is what this is looking for. */
|
|
for (uint32_t r = 0; r < BENCH_ROWS; r++)
|
|
for (uint32_t c = 0; c < width; c++) {
|
|
double a = sent[(size_t)r * width + c], b = got[(size_t)r * width + c];
|
|
if (fabs(a - b) > 0.02 * (fabs(a) + 1e-3))
|
|
bad_rows++;
|
|
}
|
|
memset(got, 0, (size_t)BENCH_ROWS * width * sizeof(float));
|
|
if (pn_tt_activation_download(g->device, strip, got, width) != 0)
|
|
goto done;
|
|
for (uint32_t c = 0; c < width; c++)
|
|
if (fabs(sent[c] - got[c]) > 0.02 * (fabs(sent[c]) + 1e-3))
|
|
bad_row0++;
|
|
|
|
/* THE STRIP INVARIANT: an upload of 4 rows must leave rows 4..31 ZERO, or a later Mt=1
|
|
* op reads a previous pass's positions as padding. */
|
|
if (pn_tt_activation_upload_rows(g->device, strip, sent, width, 4u) != 0)
|
|
goto done;
|
|
memset(got, 0, (size_t)BENCH_ROWS * width * sizeof(float));
|
|
if (pn_tt_activation_download_rows(g->device, strip, got, width, BENCH_ROWS) != 0)
|
|
goto done;
|
|
for (uint32_t r = 4; r < BENCH_ROWS; r++)
|
|
for (uint32_t c = 0; c < width; c++)
|
|
if (got[(size_t)r * width + c] != 0.0f)
|
|
bad_zero++;
|
|
|
|
printf("QUANTUM selftest 32-row round trip %s (%d bad) row-0 reader %s (%d bad) "
|
|
"rows 4..31 zeroed after a 4-row upload %s (%d bad)\n",
|
|
bad_rows == 0 ? "OK" : "BAD", bad_rows, bad_row0 == 0 ? "OK" : "BAD", bad_row0,
|
|
bad_zero == 0 ? "OK" : "BAD", bad_zero);
|
|
rc = (bad_rows || bad_row0 || bad_zero) ? -1 : 0;
|
|
done:
|
|
free(sent);
|
|
free(got);
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* THE TILE QUANTUM. The same four SSM projections, run with rows 1..31 zero and then with
|
|
* rows 1..31 carrying real activations. Identical descriptors; the only variable is the DATA
|
|
* the FPU multiplies in the 31 rows it was already multiplying.
|
|
*/
|
|
static int bench_quantum(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, int reps)
|
|
{
|
|
const pn_qwen35_tt_layer *d = NULL;
|
|
pn_qwen35_tt_strips *s = &g->card;
|
|
uint32_t width = (uint32_t)cfg->n_embd;
|
|
float *host = calloc((size_t)BENCH_ROWS * width, sizeof(float));
|
|
double padded, live;
|
|
int rc = -1;
|
|
|
|
for (int i = 0; i < g->n_layers; i++)
|
|
if (!g->layers[i].is_attention) { d = &g->layers[i]; break; }
|
|
if (!host || !d)
|
|
goto done;
|
|
|
|
/* WARM UP FIRST, AND IT IS NOT A COURTESY. The first flush after arming retires and
|
|
* replays the matmul residency ("a chained projection did not complete — retiring the
|
|
* residency ... and replaying 4 projection(s)"), which lands entirely on whichever arm
|
|
* runs first and reads as a 277x tile-quantum effect that is not one. Two discarded
|
|
* rounds put both arms on a warm residency. */
|
|
bench_fill(host, 1u, width, 11u);
|
|
if (pn_tt_activation_upload(g->device, &s->normed, host, width) != 0)
|
|
goto done;
|
|
for (int warm = 0; warm < 2; warm++) {
|
|
(void)pn_qwen35_tt_project(g, &d->attn_qkv, &s->normed, &s->ssm_qkv, false);
|
|
(void)pn_qwen35_tt_project(g, &d->attn_gate, &s->normed, &s->ssm_z, false);
|
|
(void)pn_qwen35_tt_project(g, &d->ssm_alpha, &s->normed, &s->ssm_alpha, false);
|
|
(void)pn_qwen35_tt_project(g, &d->ssm_beta, &s->normed, &s->ssm_beta, false);
|
|
(void)pn_tt_matvec_flush(g->device);
|
|
}
|
|
padded = bench_now();
|
|
for (int r = 0; r < reps; r++) {
|
|
if (pn_qwen35_tt_project(g, &d->attn_qkv, &s->normed, &s->ssm_qkv, false) != 0 ||
|
|
pn_qwen35_tt_project(g, &d->attn_gate, &s->normed, &s->ssm_z, false) != 0 ||
|
|
pn_qwen35_tt_project(g, &d->ssm_alpha, &s->normed, &s->ssm_alpha, false) != 0 ||
|
|
pn_qwen35_tt_project(g, &d->ssm_beta, &s->normed, &s->ssm_beta, false) != 0 ||
|
|
pn_tt_matvec_flush(g->device) != 0)
|
|
goto done;
|
|
}
|
|
padded = bench_now() - padded;
|
|
|
|
bench_fill(host, BENCH_ROWS, width, 11u);
|
|
if (pn_tt_activation_upload_rows(g->device, &s->normed, host, width, BENCH_ROWS) != 0)
|
|
goto done;
|
|
live = bench_now();
|
|
for (int r = 0; r < reps; r++) {
|
|
if (pn_qwen35_tt_project(g, &d->attn_qkv, &s->normed, &s->ssm_qkv, false) != 0 ||
|
|
pn_qwen35_tt_project(g, &d->attn_gate, &s->normed, &s->ssm_z, false) != 0 ||
|
|
pn_qwen35_tt_project(g, &d->ssm_alpha, &s->normed, &s->ssm_alpha, false) != 0 ||
|
|
pn_qwen35_tt_project(g, &d->ssm_beta, &s->normed, &s->ssm_beta, false) != 0 ||
|
|
pn_tt_matvec_flush(g->device) != 0)
|
|
goto done;
|
|
}
|
|
live = bench_now() - live;
|
|
|
|
printf("QUANTUM ssm projections x4 1 live row %.1f us 32 live rows %.1f us "
|
|
"ratio %.4f (per position: %.1f us -> %.1f us)\n",
|
|
1e6 * padded / reps, 1e6 * live / reps, live / padded,
|
|
1e6 * padded / reps, 1e6 * live / reps / (double)BENCH_ROWS);
|
|
rc = 0;
|
|
done:
|
|
free(host);
|
|
return rc;
|
|
}
|
|
|
|
/** The two terms a 32-row pass cannot batch, timed alone: one GDN block and, if it is armed,
|
|
* one on-card attention block. `32 * these + the flat part` is the whole item's ceiling. */
|
|
static int bench_sequential(pn_qwen35_tt *g, pn_qwen35moe_model *m,
|
|
const pn_qwen35moe_config *cfg, int reps)
|
|
{
|
|
pn_qwen35_tt_strips *s = &g->card;
|
|
const pn_qwen35_tt_layer *d_ssm = NULL, *d_attn = NULL;
|
|
int ssm_layer = -1, attn_layer = -1;
|
|
double mark;
|
|
|
|
for (int i = 0; i < g->n_layers; i++) {
|
|
if (!g->layers[i].is_attention && !d_ssm) { d_ssm = &g->layers[i]; ssm_layer = i; }
|
|
if (g->layers[i].is_attention && !d_attn) { d_attn = &g->layers[i]; attn_layer = i; }
|
|
}
|
|
/* NON-FATAL, and reported as a refusal rather than a zero. These two blocks are reached
|
|
* here OUTSIDE a forward — no embedding, no norms, no carried state at position 0 — so a
|
|
* residency that will not take a descriptor in that context is a fact about the bench and
|
|
* not about the block. The residency counters in a real run are the authority on their
|
|
* cost; this is a cross-check, so a refusal must read as one. */
|
|
if (d_ssm && g->gdn_strip) {
|
|
const pn_qwen35moe_ssm_layer *l =
|
|
pn_qwen35moe_model_ssm_layer(m, pn_qwen35moe_model_layer_index(m, ssm_layer));
|
|
int failed = 0;
|
|
|
|
memset(g->h_alpha, 0, sizeof(float) * 32u);
|
|
memset(g->h_beta, 0, sizeof(float) * 32u);
|
|
mark = bench_now();
|
|
for (int r = 0; r < reps && l && !failed; r++)
|
|
failed = pn_qwen35_tt_gdn_strip_layer(g, l, cfg,
|
|
pn_qwen35moe_model_layer_index(m, ssm_layer),
|
|
&s->ssm_qkv, &s->ssm_z, g->h_beta,
|
|
g->h_alpha, &s->ssm_gated) != 0;
|
|
if (failed || !l)
|
|
printf("QUANTUM gdn strip block REFUSED outside a forward — read it off the "
|
|
"residency counters instead\n");
|
|
else
|
|
printf("QUANTUM gdn strip block %.1f us/call -> %.4f s over 24 layers\n",
|
|
1e6 * (bench_now() - mark) / reps, 24.0 * (bench_now() - mark) / reps);
|
|
}
|
|
if (d_attn && g->attn_on_card) {
|
|
int idx = pn_qwen35moe_model_layer_index(m, attn_layer);
|
|
int failed = 0;
|
|
|
|
mark = bench_now();
|
|
for (int r = 0; r < reps && !failed; r++)
|
|
failed = pn_qwen35_tt_attn_block_card(g, d_attn, cfg, idx, r + 1, r + 2) != 0;
|
|
if (failed)
|
|
printf("QUANTUM attention block REFUSED outside a forward — read it off the "
|
|
"residency counters instead\n");
|
|
else
|
|
printf("QUANTUM attention block %.1f us/call -> %.4f s over 8 layers "
|
|
"(at trivial context)\n",
|
|
1e6 * (bench_now() - mark) / reps, 8.0 * (bench_now() - mark) / reps);
|
|
} else {
|
|
printf("QUANTUM attention block NOT ON CARD on this arm\n");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
const char *model_path = NULL;
|
|
int reps = 20;
|
|
pn_model *model;
|
|
pn_qwen35moe_model *m;
|
|
pn_qwen35_tt *g;
|
|
const pn_qwen35moe_config *cfg;
|
|
int rc = 1;
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
if (strcmp(argv[i], "--reps") == 0 && i + 1 < argc)
|
|
reps = atoi(argv[++i]);
|
|
else if (!model_path)
|
|
model_path = argv[i];
|
|
}
|
|
if (!model_path || reps < 1) {
|
|
fprintf(stderr, "usage: %s <model.gguf> [--reps N]\n", argv[0]);
|
|
return 2;
|
|
}
|
|
model = pn_load(model_path);
|
|
if (!model) {
|
|
fprintf(stderr, "QUANTUM REFUSED: cannot load %s\n", model_path);
|
|
return 2;
|
|
}
|
|
/* THE VACUOUS-RUN GUARD. Timing host memcpys against each other would answer nothing. */
|
|
if (model->forward != pn_qwen35moe_forward_tt) {
|
|
fprintf(stderr, "QUANTUM REFUSED: the engine did not select the qwen35 Tenstorrent "
|
|
"forward. Set PN_TT_ARTIFACT and check the card.\n");
|
|
pn_model_free(model);
|
|
return 2;
|
|
}
|
|
m = (pn_qwen35moe_model *)model->weights;
|
|
g = (pn_qwen35_tt *)pn_qwen35moe_model_gpu(m);
|
|
cfg = pn_qwen35moe_model_config(m);
|
|
if (!g || !cfg || !g->resident) {
|
|
fprintf(stderr, "QUANTUM REFUSED: no resident arm — there are no card strips to "
|
|
"time.\n");
|
|
pn_model_free(model);
|
|
return 2;
|
|
}
|
|
printf("QUANTUM model n_embd %d n_layer %d strips: hidden %u ssm_qkv %u ssm_z %u "
|
|
"ssm_gated %u ffn_swiglu %u tiles; gdn_strip %d attn_on_card %d\n",
|
|
cfg->n_embd, cfg->n_layer, g->card.hidden[0].tile_count, g->card.ssm_qkv.tile_count,
|
|
g->card.ssm_z.tile_count, g->card.ssm_gated.tile_count,
|
|
g->card.ffn_swiglu.tile_count, (int)g->gdn_strip, (int)g->attn_on_card);
|
|
|
|
/* ORDER IS LOAD-BEARING, AND IT WAS MEASURED THE HARD WAY. A resident triad has an IDLE
|
|
* SPIN LIMIT — the transport says so itself ("leaves core 71 resident until its idle spin
|
|
* limit, measured at over 40 s") — and the transfer arms below take ~40 s of link with no
|
|
* descriptor in them. Running them first retired the matmul residency mid-bench and every
|
|
* projection after it came back `resident matmul Mt=1 Kt=128 Nt=256 refused`, which reads
|
|
* as a tile-quantum result and is a bench artefact. The card arms run FIRST, while the
|
|
* residencies are warm, and the transfers run last. */
|
|
if (bench_selftest(g, cfg) != 0) {
|
|
fprintf(stderr, "QUANTUM REFUSED: the transfers do not move the right bytes; the "
|
|
"timings below would be the cost of being wrong.\n");
|
|
goto done;
|
|
}
|
|
if (bench_quantum(g, cfg, reps) != 0)
|
|
goto done;
|
|
if (bench_sequential(g, m, cfg, reps) != 0)
|
|
goto done;
|
|
if (bench_transfers(g, cfg, reps) != 0)
|
|
goto done;
|
|
rc = 0;
|
|
done:
|
|
printf("QUANTUM VERDICT %s\n", rc == 0 ? "MEASURED" : "REFUSED");
|
|
pn_model_free(model);
|
|
return rc;
|
|
}
|
|
|
|
#endif /* USE_TT */
|