feat(concurrency): provider-wide lens budget, drop the model cap
Concurrency was two multiplicative gates in two processes: entrypoint.sh capped MODELS-at-once per provider (GADFLY_PROVIDER_CONCURRENCY) while each model's binary separately capped its own lenses (GADFLY_LENS_CONCURRENCY). A model therefore held its whole model-slot until its LAST lens finished, stalling the next model even with idle lens capacity. Collapse to one throttle: a provider-wide lens budget shared across all of that provider's models. entrypoint now runs every model in a lane at once and seeds a single cross-process permit pool per lane (a dir of N flock files, sized by GADFLY_PROVIDER_LENS_CONCURRENCY -> GADFLY_LENS_CONCURRENCY). Each lens pass (review+recheck) acquires a permit before it runs and releases it after, so a model winding down immediately yields its freed permits to another model's queued lenses. flock auto-releases on process death, so a killed/crashed model can't leak budget. - cmd/gadfly/lenssem.go: the flock permit pool (+ lenssem_test.go). - main.go: runSpecialists holds a shared permit per lens; fanout sized to the budget so a lone model can use all of it. Falls back to the in-process limit when no pool is set (local runs, tests). - entrypoint.sh: drop provider_cap/DEFAULT_CONC; run_lane runs all models and seeds the per-lane pool. - GADFLY_PROVIDER_CONCURRENCY / GADFLY_CONCURRENCY are now ignored; the reusable workflow marks provider_concurrency deprecated and stops forwarding it. Docs (README, CLAUDE.md, examples) updated per the maintenance rule. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
+39
-23
@@ -198,14 +198,18 @@ export GADFLY_FINDINGS_TOKEN="${GADFLY_FINDINGS_TOKEN:-}"
|
||||
# provider key envs (OPENAI_API_KEY, …) are inherited by run.sh and the binary.
|
||||
#
|
||||
# Concurrency: each PROVIDER is its own lane and lanes run in PARALLEL, so a fast
|
||||
# cloud provider isn't stuck behind a slow local box. Within a lane, at most
|
||||
# `cap` models run at once. cap = GADFLY_PROVIDER_CONCURRENCY's "provider=N"
|
||||
# entry, else GADFLY_CONCURRENCY (default 1). A model's provider is the spec's
|
||||
# first path segment ("m1pro/qwen3.6:35b-mlx" -> m1pro), or GADFLY_PROVIDER /
|
||||
# ollama-cloud for a bare id. Default (cap 1) keeps a single-provider pool fully
|
||||
# sequential, exactly as before.
|
||||
# cloud provider isn't stuck behind a slow local box. Within a lane ALL of the
|
||||
# provider's models run at once; the real throttle is a single PROVIDER-WIDE lens
|
||||
# budget (a shared permit pool, seeded per lane) that every model's lenses draw
|
||||
# from — so total lens passes in flight per provider is bounded, but a model
|
||||
# winding down to its last lens immediately yields its freed permits to another
|
||||
# model's queued lenses instead of holding a whole "model slot" (the old
|
||||
# GADFLY_PROVIDER_CONCURRENCY model cap, now removed, caused that tail stall). The
|
||||
# budget = GADFLY_PROVIDER_LENS_CONCURRENCY's "provider=N" entry, else
|
||||
# GADFLY_LENS_CONCURRENCY (default 1). A model's provider is the spec's first path
|
||||
# segment ("m1pro/qwen3.6:35b-mlx" -> m1pro), or GADFLY_PROVIDER / ollama-cloud
|
||||
# for a bare id.
|
||||
MODELS="${GADFLY_MODELS:-${OLLAMA_REVIEW_MODELS:-$DEFAULT_MODELS}}"
|
||||
DEFAULT_CONC="${GADFLY_CONCURRENCY:-1}"
|
||||
|
||||
# --- huge-PR downshift ------------------------------------------------------
|
||||
# A very large diff is what burns the model budget: every review step re-sends
|
||||
@@ -245,24 +249,33 @@ provider_of() { case "$1" in */*) echo "${1%%/*}";; *) echo "${GADFLY_PROVIDER:-
|
||||
STATUS_DIR="${WORKDIR}/status"
|
||||
status_file_for() { echo "${STATUS_DIR}/$(echo "$1" | tr -c '[:alnum:]._-' '_').json"; }
|
||||
|
||||
provider_cap() { # provider -> concurrency (override map "p=N,...", else default)
|
||||
# Root of the per-provider lens permit pools (one subdir per lane, seeded by
|
||||
# run_lane). Cleared up front so a reused WORKDIR can't leak stale permit files.
|
||||
LENS_SEM_ROOT="${WORKDIR}/lenssem"
|
||||
rm -rf "$LENS_SEM_ROOT" 2>/dev/null || true
|
||||
|
||||
provider_lens_cap() { # provider -> provider-wide lens budget (permit-pool size)
|
||||
local p="$1" item k v
|
||||
IFS=',' read -ra _caps <<< "${GADFLY_PROVIDER_CONCURRENCY:-}"
|
||||
for item in "${_caps[@]}"; do
|
||||
IFS=',' read -ra _lcaps <<< "${GADFLY_PROVIDER_LENS_CONCURRENCY:-}"
|
||||
for item in "${_lcaps[@]}"; do
|
||||
k="$(echo "${item%%=*}" | tr -d '[:space:]')"
|
||||
v="$(echo "${item#*=}" | tr -d '[:space:]')"
|
||||
if [ "$k" = "$p" ] && [ -n "$v" ]; then echo "$v"; return; fi
|
||||
done
|
||||
echo "$DEFAULT_CONC"
|
||||
echo "${GADFLY_LENS_CONCURRENCY:-1}"
|
||||
}
|
||||
|
||||
review_one() {
|
||||
local sf="" ff=""
|
||||
[ "${GADFLY_STATUS_BOARD:-1}" != "0" ] && sf="$(status_file_for "$1")"
|
||||
[ "$CONSOLIDATE" = "1" ] && ff="$(findings_file_for "$1")"
|
||||
PROVIDER=ollama MODEL="$1" GADFLY_BIN="/usr/local/bin/gadfly" GADFLY_REPO_DIR="$REPO_DIR" \
|
||||
local m="$1" sem_dir="${2:-}" sem_size="${3:-}" sf="" ff=""
|
||||
[ "${GADFLY_STATUS_BOARD:-1}" != "0" ] && sf="$(status_file_for "$m")"
|
||||
[ "$CONSOLIDATE" = "1" ] && ff="$(findings_file_for "$m")"
|
||||
# GADFLY_LENS_SEM_DIR/_SIZE point the binary at this provider's shared lens
|
||||
# permit pool (empty => the binary just uses its in-process lens limit). These
|
||||
# are inherited by the binary through run.sh's environment.
|
||||
PROVIDER=ollama MODEL="$m" GADFLY_BIN="/usr/local/bin/gadfly" GADFLY_REPO_DIR="$REPO_DIR" \
|
||||
GADFLY_STATUS_FILE="$sf" GADFLY_FINDINGS_OUT="$ff" GADFLY_CONSOLIDATE="$CONSOLIDATE" \
|
||||
bash "${SCRIPTS_DIR}/run.sh" || log "model $1 failed (continuing)"
|
||||
GADFLY_LENS_SEM_DIR="$sem_dir" GADFLY_LENS_SEM_SIZE="$sem_size" \
|
||||
bash "${SCRIPTS_DIR}/run.sh" || log "model $m failed (continuing)"
|
||||
# If the binary never wrote real status (run.sh skipped it: empty diff, no key,
|
||||
# binary missing), the pre-seed stays {started:0, done:false} and the board
|
||||
# would show this model "waiting to start" forever and never reach N/N. Mark
|
||||
@@ -313,16 +326,19 @@ for m in "${MODEL_LIST[@]}"; do
|
||||
case " $PROVIDERS " in *" $p "*) ;; *) PROVIDERS="${PROVIDERS}${PROVIDERS:+ }$p" ;; esac
|
||||
done
|
||||
|
||||
run_lane() { # $1=provider: run its models, at most `cap` at a time
|
||||
local p="$1" cap inflight=0 m
|
||||
cap="$(provider_cap "$p")"; [ "$cap" -ge 1 ] 2>/dev/null || cap=1
|
||||
run_lane() { # $1=provider: run ALL its models at once, throttled only by a shared
|
||||
# provider-wide lens permit pool (no per-model cap).
|
||||
local p="$1" budget sem_dir m
|
||||
budget="$(provider_lens_cap "$p")"; [ "$budget" -ge 1 ] 2>/dev/null || budget=1
|
||||
local mine=()
|
||||
for m in "${MODEL_LIST[@]}"; do [ "$(provider_of "$m")" = "$p" ] && mine+=("$m"); done
|
||||
log "lane ${p}: cap ${cap}; models: ${mine[*]}"
|
||||
# Seed this provider's lens permit pool: a directory the binary flocks N permit
|
||||
# files in (created lazily), one shared budget across every model in the lane.
|
||||
sem_dir="${LENS_SEM_ROOT}/$(echo "$p" | tr -c '[:alnum:]._-' '_')"
|
||||
mkdir -p "$sem_dir"
|
||||
log "lane ${p}: lens budget ${budget} shared across ${#mine[@]} model(s): ${mine[*]}"
|
||||
for m in "${mine[@]}"; do
|
||||
review_one "$m" &
|
||||
inflight=$((inflight+1))
|
||||
if [ "$inflight" -ge "$cap" ]; then wait -n 2>/dev/null || wait; inflight=$((inflight-1)); fi
|
||||
review_one "$m" "$sem_dir" "$budget" &
|
||||
done
|
||||
wait
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user