File size: 10,848 Bytes
d4c2896
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#!/usr/bin/env bash
# Definitive final chain (uli decision): 25 configs + conditional q5-dflash.
# Replaces F1/F2/F3 (legacy INTACT in scripts/old/, experiments/runs/old/).
# Greedy T=0, non-thinking (--reasoning off), persistent llama-server via
# bench_accept.py, idempotent resume per config, "FINAL RUN DONE" marker.
#
# Usage:
#   SMOKE=1  bash scripts/run_final_chain.sh   # smoke: --max-prompts 2 per config
#   bash scripts/run_final_chain.sh            # full chain (~67-75 h β‰ˆ 3 nights)
#   DRY_RUN=1 bash scripts/run_final_chain.sh  # prints the matrix (26) and exits
#
# Exit: 0 with no FAILED configs; 1 if any FAILED (check queue-final.log and errors.jsonl).
set -uo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO"
source scripts/spec-env.sh
set +e  # spec-env.sh enables set -e; the chain handles errors via exit codes

LOG="experiments/runs/queue-final.log"
log() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG"; }

SAMPLES="experiments/prompts/f1-sample.jsonl"
NTOK=256
Q="models/Qwen3-8B"
G="models/gemma-4-12b-it"
DK="models/drafts"
BENCH="python scripts/bench_accept.py"

# SMOKE=1 β†’ only 2 prompts per config (smoke gate of the final-run-matrix spec).
MAXP=()
[ "${SMOKE:-0}" = "1" ] && MAXP=(--max-prompts 2)

OK_COUNT=0
FAILED_COUNT=0
SKIP_COUNT=0
OOM_OK=1
G8_CHECKED=0
Q5_DFLASH_OK=0

# Matrix of 25 configs (spec final-run-matrix): name|target|draft|spec_type|p_min|out
# (draft/spec_type/p_min empty in solo). out = experiments/runs/final-<name>.
MATRIX=(
    # Qwen3-8B Q4_K_M (8)
    "final-qwen-q4-solo|$Q-Q4_K_M.gguf||||experiments/runs/final-qwen-q4-solo"
    "final-qwen-q4-vanilla17b|$Q-Q4_K_M.gguf|$DK/Qwen3-1.7B-Q4_K_M.gguf|draft-simple||experiments/runs/final-qwen-q4-vanilla17b"
    "final-qwen-q4-eagle3|$Q-Q4_K_M.gguf|$DK/Qwen3-8B-speculator.eagle3-F16.gguf|draft-eagle3||experiments/runs/final-qwen-q4-eagle3"
    "final-qwen-q4-dflash|$Q-Q4_K_M.gguf|$DK/dflash_qwen3_8b_block7.gguf|draft-dflash||experiments/runs/final-qwen-q4-dflash"
    "final-qwen-q4-dspark-p0|$Q-Q4_K_M.gguf|$DK/dspark_qwen3_8b_block7.gguf|draft-dspark|0.0|experiments/runs/final-qwen-q4-dspark-p0"
    "final-qwen-q4-dspark-p2|$Q-Q4_K_M.gguf|$DK/dspark_qwen3_8b_block7.gguf|draft-dspark|0.2|experiments/runs/final-qwen-q4-dspark-p2"
    "final-qwen-q4-dspark-p4|$Q-Q4_K_M.gguf|$DK/dspark_qwen3_8b_block7.gguf|draft-dspark|0.4|experiments/runs/final-qwen-q4-dspark-p4"
    "final-qwen-q4-dspark-p6|$Q-Q4_K_M.gguf|$DK/dspark_qwen3_8b_block7.gguf|draft-dspark|0.6|experiments/runs/final-qwen-q4-dspark-p6"
    # Qwen3-8B Q5_K_M (3)
    "final-qwen-q5-solo|$Q-Q5_K_M.gguf||||experiments/runs/final-qwen-q5-solo"
    "final-qwen-q5-eagle3|$Q-Q5_K_M.gguf|$DK/Qwen3-8B-speculator.eagle3-F16.gguf|draft-eagle3||experiments/runs/final-qwen-q5-eagle3"
    "final-qwen-q5-dspark-p0|$Q-Q5_K_M.gguf|$DK/dspark_qwen3_8b_block7.gguf|draft-dspark|0.0|experiments/runs/final-qwen-q5-dspark-p0"
    # Qwen3-8B Q8_0 (3)
    "final-qwen-q8-solo|$Q-Q8_0.gguf||||experiments/runs/final-qwen-q8-solo"
    "final-qwen-q8-eagle3|$Q-Q8_0.gguf|$DK/Qwen3-8B-speculator.eagle3-F16.gguf|draft-eagle3||experiments/runs/final-qwen-q8-eagle3"
    "final-qwen-q8-dspark-p0|$Q-Q8_0.gguf|$DK/dspark_qwen3_8b_block7.gguf|draft-dspark|0.0|experiments/runs/final-qwen-q8-dspark-p0"
    # Gemma 4 12B Q4_K_M (5)
    "final-gemma-q4-solo|$G-Q4_K_M.gguf||||experiments/runs/final-gemma-q4-solo"
    "final-gemma-q4-mtp|$G-Q4_K_M.gguf|$DK/mtp-gemma-4-12b-it-Q8_0.gguf|draft-mtp||experiments/runs/final-gemma-q4-mtp"
    "final-gemma-q4-dflash-f16|$G-Q4_K_M.gguf|$DK/gemma-4-12B-it-DFlash-F16.gguf|draft-dflash||experiments/runs/final-gemma-q4-dflash-f16"
    "final-gemma-q4-dflash-q4|$G-Q4_K_M.gguf|$DK/gemma-4-12B-it-DFlash-Q4_K_M.gguf|draft-dflash||experiments/runs/final-gemma-q4-dflash-q4"
    "final-gemma-q4-dflash-q8|$G-Q4_K_M.gguf|$DK/gemma-4-12B-it-DFlash-Q8_0.gguf|draft-dflash||experiments/runs/final-gemma-q4-dflash-q8"
    # Gemma 4 12B Q5_K_M (3)
    "final-gemma-q5-solo|$G-Q5_K_M.gguf||||experiments/runs/final-gemma-q5-solo"
    "final-gemma-q5-mtp|$G-Q5_K_M.gguf|$DK/mtp-gemma-4-12b-it-Q8_0.gguf|draft-mtp||experiments/runs/final-gemma-q5-mtp"
    "final-gemma-q5-dflash-f16|$G-Q5_K_M.gguf|$DK/gemma-4-12B-it-DFlash-F16.gguf|draft-dflash||experiments/runs/final-gemma-q5-dflash-f16"
    # Gemma 4 12B Q8_0 (3) β€” block after OOM-CHECK
    "final-gemma-q8-solo|$G-Q8_0.gguf||||experiments/runs/final-gemma-q8-solo"
    "final-gemma-q8-mtp|$G-Q8_0.gguf|$DK/mtp-gemma-4-12b-it-Q8_0.gguf|draft-mtp||experiments/runs/final-gemma-q8-mtp"
    "final-gemma-q8-dflash-f16|$G-Q8_0.gguf|$DK/gemma-4-12B-it-DFlash-F16.gguf|draft-dflash||experiments/runs/final-gemma-q8-dflash-f16|-ub 512"
)

# Files present and β‰₯ 100 MB (the quantized DFlash drafts 422M/752M fit in;
# lesson F3 #4: the 1 GB threshold stalled downloads for 2 h).
have_file() {
    local f="$1"
    [ -f "$f" ] && [ "$(stat -c%s "$f" 2>/dev/null || echo 0)" -gt 100000000 ]
}

run_job() {
    local name="$1" out="$2"
    shift 2
    local rc
    log "JOB START: $name"
    $BENCH --config-name "$name" --prompts "$SAMPLES" --n-tokens "$NTOK" \
        --out "$out" --resume "${MAXP[@]}" "$@"
    rc=$?
    if [ "$rc" -eq 3 ]; then
        log "JOB LOCK: $name (exit 3 = another runner on this --out) β€” wait 300s and retry once"
        sleep 300
        $BENCH --config-name "$name" --prompts "$SAMPLES" --n-tokens "$NTOK" \
            --out "$out" --resume "${MAXP[@]}" "$@"
        rc=$?
        if [ "$rc" -eq 3 ]; then
            log "CHAIN ABORT: $name still locked (another live chain) β€” aborting to avoid overwriting results"
            exit 3
        fi
    fi
    case "$rc" in
        0) log "JOB DONE: $name (exit 0)"; OK_COUNT=$((OK_COUNT + 1)) ;;
        2) log "JOB DONE WITH FAILURES: $name (exit 2) β€” prompts in errors.jsonl"; FAILED_COUNT=$((FAILED_COUNT + 1)) ;;
        *) log "JOB FAILED: $name (exit $rc)"; FAILED_COUNT=$((FAILED_COUNT + 1)) ;;
    esac
    return "$rc"
}

run_config() {
    local entry="$1"
    local name target draft stype pmin out extra
    IFS='|' read -r name target draft stype pmin out extra <<< "$entry"
    [ -n "$out" ] || out="experiments/runs/final-$name"
    if ! have_file "$target"; then
        log "SKIP: $name (target not available: $target)"
        SKIP_COUNT=$((SKIP_COUNT + 1))
        return
    fi
    if [ -n "$draft" ] && ! have_file "$draft"; then
        log "SKIP: $name (draft not available: $draft)"
        SKIP_COUNT=$((SKIP_COUNT + 1))
        return
    fi
    local args=()
    if [ -n "$draft" ]; then
        args+=(--draft "$draft" --spec-type "$stype")
        [ -n "$pmin" ] && args+=(--spec-draft-p-min "$pmin")
    fi
    [ -n "$extra" ] && args+=(--extra "$extra")
    run_job "$name" "$out" --model "$target" "${args[@]}"
}

# OOM-CHECK before the Gemma Q8 block (legacy pattern): llama-cli with the
# worst case g8+dflash-f16 (superset of g8-solo). Failure β†’ SKIP the 3 g8 configs.
oom_check_g8() {
    log "OOM-CHECK: gemma q8 + dflash f16 (llama-cli -ub 512, 1 prompt n=8) β€” worst case of the g8 block"
    if "$LLAMA_CPP_BIN/llama-cli" -m "$G-Q8_0.gguf" -md "$DK/gemma-4-12B-it-DFlash-F16.gguf" \
        --spec-type draft-dflash -ngl 99 -ngld 99 -n 8 -p "Hello." -ub 512 \
        --no-conversation --single-turn --reasoning off -c 2048 -t 8 --seed 42 \
        < /dev/null > /tmp/opencode/oom-check-final.log 2>&1; then
        log "OOM-CHECK: OK (gemma q8 usable with -ub 512)"
        OOM_OK=1
    else
        log "OOM-CHECK: FAILED β†’ SKIP the 3 g8 configs (see /tmp/opencode/oom-check-final.log; fallback: ctx 1024 or draft Q4/Q8)"
        OOM_OK=0
    fi
}

# Conditional q5-dflash (spec): decision with REAL sizes + KV ctx 2048 +
# overhead vs free VRAM; the decision (numbers + reason) stays in queue-final.log.
q5_dflash_decide() {
    local target="$Q-Q5_K_M.gguf" draft="$DK/dflash_qwen3_8b_block7.gguf"
    local tsize dsize free_mib need
    tsize=$(( $(stat -c%s "$target" 2>/dev/null || echo 0) / 1048576 ))
    dsize=$(( $(stat -c%s "$draft" 2>/dev/null || echo 0) / 1048576 ))
    if ! have_file "$target" || ! have_file "$draft"; then
        log "q5-dflash: excluded (missing files: $target / $draft)"
        return 1
    fi
    free_mib=$(nvidia-smi --query-gpu=memory.free --format=csv,noheader,nounits 2>/dev/null | head -n1 | tr -d ' ')
    local kv_mib=1024 overhead_mib=512
    need=$((tsize + dsize + kv_mib + overhead_mib))
    if [ -n "$free_mib" ] && [ "$free_mib" -ge "$need" ]; then
        log "q5-dflash: FITS β€” target ${tsize}MiB + draft ${dsize}MiB + KV ${kv_mib}MiB + overhead ${overhead_mib}MiB = ${need}MiB ≀ free VRAM ${free_mib}MiB"
        return 0
    fi
    log "q5-dflash: DOES NOT FIT β€” target ${tsize}MiB + draft ${dsize}MiB + KV ${kv_mib}MiB + overhead ${overhead_mib}MiB = ${need}MiB > free VRAM ${free_mib:-unknown}MiB β†’ excluded"
    return 1
}

log "FINAL CHAIN START (SMOKE=${SMOKE:-0}) β€” ${#MATRIX[@]} configs + conditional q5-dflash"

# DRY_RUN: prints the matrix (26 configs) without touching the GPU and exits.
if [ "${DRY_RUN:-0}" = "1" ]; then
    echo "DRY-RUN: final-run matrix (${#MATRIX[@]} + 1 conditional):"
    for entry in "${MATRIX[@]}"; do
        IFS='|' read -r dname dtarget ddraft dstype dpmin dout <<< "$entry"
        echo "  $dname | $dtarget | ${ddraft:-β€”} | ${dstype:-β€”} | ${dpmin:-β€”}"
    done
    echo "  final-qwen-q5-dflash (conditional β€” decided by real VRAM at runtime)"
    echo "DRY-RUN: end ($((${#MATRIX[@]} + 1)) configs)"
    exit 0
fi

if q5_dflash_decide; then
    Q5_DFLASH_OK=1
fi

T0=$(date +%s)
for entry in "${MATRIX[@]}"; do
    name="${entry%%|*}"
    case "$name" in
        final-qwen-q5-dspark-p0)
            # Inserts the conditional config at the end of the Q5 block.
            if [ "$Q5_DFLASH_OK" = "1" ]; then
                run_config "final-qwen-q5-dflash|$Q-Q5_K_M.gguf|$DK/dflash_qwen3_8b_block7.gguf|draft-dflash||experiments/runs/final-qwen-q5-dflash"
            fi
            ;;
        final-gemma-q8-*)
            if [ "$G8_CHECKED" -eq 0 ]; then
                oom_check_g8
                G8_CHECKED=1
            fi
            if [ "$OOM_OK" != "1" ]; then
                log "SKIP: $name (OOM-CHECK g8 failed; fallback: ctx 1024 or draft Q4/Q8)"
                SKIP_COUNT=$((SKIP_COUNT + 1))
                continue
            fi
            ;;
    esac
    run_config "$entry"
done

DUR=$(( $(date +%s) - T0 ))
log "FINAL RUN DONE (duration ${DUR}s β‰ˆ $((DUR / 3600))h $((DUR % 3600 / 60))m)"
log "FINAL SUMMARY: configs OK=$OK_COUNT FAILED=$FAILED_COUNT SKIP=$SKIP_COUNT"
if [ "$FAILED_COUNT" -eq 0 ] && [ "$SKIP_COUNT" -eq 0 ]; then
    log "FINAL RUN: exit 0 (no FAILED or SKIP)"
    exit 0
fi
log "FINAL RUN: exit 1 (FAILED=$FAILED_COUNT SKIP=$SKIP_COUNT β€” check queue-final.log and errors.jsonl)"
exit 1