Repository: ggml-org/llama.cpp
Pull request: Include speculative decoding stats when timings_per_token is enabled

New fields added to the `timings` object:

  - draft_n           : number of draft tokens generated
  - draft_n_accepted  : number of draft tokens accepted

## Sample of output 

These were done on an M1 Pro, 32GB

### Server: 
```
$ ./build/bin/llama-server --no-mmap --no-warmup \
  --model /Volumes/devdisk/llama-swap/models/Qwen2.5-7B-Instruct-Q4_K_M.gguf \
  --model-draft /Volumes/devdisk/llama-swap/models/qwen2.5-0.5b-instruct-q8_0.gguf \
  -ngl 99 -ngld 99 \
  --draft-max 16 --draft-min 4 --draft-p-min 0.4
```

### streaming off

```
$ curl -s http://localhost:8080/v1/chat/completions \
     -H "Content-Type: application/json" \
     -d '{"model":"qwen","max_tokens":200, "timings_per_token":true, "messages": [{"role": "user","content": "write a story about dogs"}]}' | jq .timings

{
  "prompt_n": 1,
  "prompt_ms": 228.522,
  "prompt_per_token_ms": 228.522,
  "prompt_per_second": 4.375946298387026,
  "predicted_n": 200,
  "predicted_ms": 11082.606,
  "predicted_per_token_ms": 55.41303,
  "predicted_per_second": 18.0462970532382,
  "draft_n": 347,
  "draft_n_accepted": 65
}
```

### streaming on 

```
$ curl -sn http://localhost:8080/v1/chat/completions \
    -H "Content-Type: application/json" \
    -d '{"model":"qwen","max_tokens":200, "stream":true, "timings_per_token":true, "messages": [{"role": "user","content": "write a story about dogs"}]}' | jq -cR 'sub("^data: "; "") | fromjson? | .timings | {predicted_n, predicted_per_second, draft_n, draft_n_accepted, draft_accept_ratio: (if .draft_n > 0 then .draft_n_accepted / .draft_n else null end)}'

{"predicted_n":1,"predicted_per_second":null,"draft_n":null,"draft_n_accepted":null,"draft_accept_ratio":null}
{"predicted_n":8,"predicted_per_second":null,"draft_n":16,"draft_n_accepted":6,"draft_accept_ratio":0.375}
{"predicted_n":8,"predicted_per_second":null,"draft_n":16,"draft_n_accepted":6,"draft_accept_ratio":0.375}
{"predicted_n":8,"predicted_per_second":null,"draft_n":16,"draft_n_accepted":6,"draft_accept_ratio":0.375}
{"predicted_n":8,"predicted_per_second":null,"draft_n":16,"draft_n_accepted":6,"draft_accept_ratio":0.375}
{"predicted_n":8,"predicted_per_second":null,"draft_n":16,"draft_n_accepted":6,"draft_accept_ratio":0.375}
... 
{"predicted_n":197,"predicted_per_second":17.71554376197935,"draft_n":335,"draft_n_accepted":72,"draft_accept_ratio":0.21492537313432836}
{"predicted_n":198,"predicted_per_second":17.740989615697014,"draft_n":335,"draft_n_accepted":72,"draft_accept_ratio":0.21492537313432836}
{"predicted_n":199,"predicted_per_second":17.76501315459052,"draft_n":335,"draft_n_accepted":72,"draft_accept_ratio":0.21492537313432836}
{"predicted_n":200,"predicted_per_second":17.79046198182722,"draft_n":335,"draft_n_accepted":72,"draft_accept_ratio":0.21492537313432836}
{"predicted_n":200,"predicted_per_second":17.790439826817185,"draft_n":335,"draft_n_accepted":72,"draft_accept_ratio":0.21492537313432836}
```


Changed files:
filename: examples/server/server.cpp
patch: @@ -489,8 +489,12 @@ struct result_timings {
     double predicted_per_token_ms;
     double predicted_per_second;
 
+    // Optional speculative metrics - only included when > 0
+    int32_t draft_n = 0;
+    int32_t draft_n_accepted = 0;
+
     json to_json() const {
-        return {
+        json base = {
             {"prompt_n",               prompt_n},
             {"prompt_ms",              prompt_ms},
             {"prompt_per_token_ms",    prompt_per_token_ms},
@@ -501,6 +505,13 @@ struct result_timings {
             {"predicted_per_token_ms", predicted_per_token_ms},
             {"predicted_per_second",   predicted_per_second},
         };
+
+        if (draft_n > 0) {
+            base["draft_n"] = draft_n;
+            base["draft_n_accepted"] = draft_n_accepted;
+        }
+
+        return base;
     }
 };
 
@@ -1299,6 +1310,10 @@ struct server_slot {
 
     std::function<void(int)> callback_on_release;
 
+    // Speculative decoding stats
+    int32_t n_draft_total = 0;      // Total draft tokens generated
+    int32_t n_draft_accepted = 0;   // Draft tokens actually accepted
+
     void reset() {
         SLT_DBG(*this, "%s", "\n");
 
@@ -1315,6 +1330,10 @@ struct server_slot {
 
         generated_tokens.clear();
         generated_token_probs.clear();
+
+        // clear speculative decoding stats
+        n_draft_total = 0;
+        n_draft_accepted = 0;
     }
 
     bool is_non_causal() const {
@@ -1381,6 +1400,12 @@ struct server_slot {
         timings.predicted_per_token_ms = t_token_generation / n_decoded;
         timings.predicted_per_second = 1e3 / t_token_generation * n_decoded;
 
+        // Add speculative metrics
+        if (n_draft_total > 0) {
+            timings.draft_n = n_draft_total;
+            timings.draft_n_accepted = n_draft_accepted;
+        }
+
         return timings;
     }
 
@@ -1428,6 +1453,15 @@ struct server_slot {
                 t_prompt_processing, n_prompt_tokens_processed, t_prompt, n_prompt_second,
                 t_token_generation, n_decoded, t_gen, n_gen_second,
                 t_prompt_processing + t_token_generation, n_prompt_tokens_processed + n_decoded);
+
+        if (n_draft_total > 0) {
+            const float draft_ratio = (float) n_draft_accepted / n_draft_total;
+            SLT_INF(*this,
+                    "\n"
+                    "draft acceptance rate = %0.5f (%5d accepted / %5d generated)\n",
+                    draft_ratio, n_draft_accepted, n_draft_total
+            );
+        }
     }
 
     json to_json() const {
@@ -3290,6 +3324,9 @@ struct server_context {
 
                 llama_tokens draft = common_speculative_gen_draft(slot.spec, params_spec, slot.cache_tokens, id);
 
+                // keep track of total number of tokens generated in the draft
+                slot.n_draft_total += draft.size();
+
                 // ignore small drafts
                 if (slot.params.speculative.n_min > (int) draft.size()) {
                     SLT_DBG(slot, "ignoring small draft: %d < %d\n", (int) draft.size(), slot.params.speculative.n_min);
@@ -3315,6 +3352,9 @@ struct server_context {
                 slot.n_past    += ids.size();
                 slot.n_decoded += ids.size();
 
+                // update how many tokens out of draft was accepted
+                slot.n_draft_accepted += ids.size() - 1;
+
                 slot.cache_tokens.push_back(id);
                 slot.cache_tokens.insert(slot.cache_tokens.end(), ids.begin(), ids.end() - 1);