From a42bf43ff78a31c439b3a81f24710f91b01c622a Mon Sep 17 00:00:00 2001 From: bobbyning Date: Fri, 25 Sep 2026 09:42:56 +0800 Subject: [PATCH 1/2] Honor GPU_CHUNK and SEARCH_TIMEOUT_MS envs in workerpoh flags. The node passes GPU_CHUNK/SEARCH_TIMEOUT_MS through workerEnv when it spawns a pool worker, and scripts/ops/worker_autostart.sh translates the same envs into -gpu-chunk/-search-timeout-ms flags on Linux. On the Windows spawn path only the env passthrough exists, but workerpoh read these two settings from flags alone: COORD_URL, COORD_TOKEN, WORKER_ID and HACKME_GPU_BACKEND all have env defaults, GPU_CHUNK and SEARCH_TIMEOUT_MS did not. So Windows installs (and any rig-profile tuning that exports GPU_CHUNK) silently ran the 1<<22 default chunk with no warning, and small-chunk launches are dominated by per-launch overhead on older GPUs. Give both flags env defaults matching the existing pattern: envIntMs for the timeout, new envUint64 helper for the chunk. Explicit flags still win; with envs unset the defaults are unchanged (4194304/2500). Verified: go vet, new table tests for both helpers (unset/whitespace/ zero/negative/garbage/overflow fall back), full package tests, and -h shows env-provided defaults. --- cmd/workerpoh/main.go | 16 +++++++++-- cmd/workerpoh/main_test.go | 56 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 cmd/workerpoh/main_test.go diff --git a/cmd/workerpoh/main.go b/cmd/workerpoh/main.go index 79fd205b..75f71ac1 100644 --- a/cmd/workerpoh/main.go +++ b/cmd/workerpoh/main.go @@ -278,6 +278,18 @@ func envIntMs(envKey string, fallback int) int { return x } +func envUint64(envKey string, fallback uint64) uint64 { + v := strings.TrimSpace(os.Getenv(envKey)) + if v == "" { + return fallback + } + x, err := strconv.ParseUint(v, 10, 64) + if err != nil || x == 0 { + return fallback + } + return x +} + func newWorkerHTTPClient(timeout time.Duration) *http.Client { if timeout < 5*time.Second { timeout = 5 * time.Second @@ -656,8 +668,8 @@ func main() { token = flag.String("token", strings.TrimSpace(os.Getenv("COORD_TOKEN")), "coordinator admin token") workerID = flag.String("worker", strings.TrimSpace(os.Getenv("WORKER_ID")), "worker id") batch = flag.Uint64("batch", 1<<22, "claim batch size") - gpuChunk = flag.Uint64("gpu-chunk", 1<<22, "GPU chunk size per Search() call") - searchTimeoutMS = flag.Int("search-timeout-ms", 2500, "Search() timeout per GPU chunk (ms)") + gpuChunk = flag.Uint64("gpu-chunk", envUint64("GPU_CHUNK", 1<<22), "GPU chunk size per Search() call (env GPU_CHUNK)") + searchTimeoutMS = flag.Int("search-timeout-ms", envIntMs("SEARCH_TIMEOUT_MS", 2500), "Search() timeout per GPU chunk (ms) (env SEARCH_TIMEOUT_MS)") gpuBackend = flag.String("gpu-backend", strings.TrimSpace(os.Getenv("HACKME_GPU_BACKEND")), "preferred GPU backend: auto|opencl|cuda") gpuDevice = flag.Int("gpu-device", -1, "preferred accelerator device index (-1 = auto)") gpuDisable = flag.Bool("gpu-disable", isTruthy(os.Getenv("HACKME_GPU_DISABLE")), "disable GPU and force CPU mode") diff --git a/cmd/workerpoh/main_test.go b/cmd/workerpoh/main_test.go new file mode 100644 index 00000000..1fb6fcca --- /dev/null +++ b/cmd/workerpoh/main_test.go @@ -0,0 +1,56 @@ +package main + +import "testing" + +func TestEnvUint64(t *testing.T) { + cases := []struct { + name string + set string + val string + want uint64 + }{ + {name: "unset returns fallback", set: "", want: 4194304}, + {name: "plain value", set: "GPU_CHUNK_TEST", val: "8388608", want: 8388608}, + {name: "whitespace trimmed", set: "GPU_CHUNK_TEST", val: " 16777216 ", want: 16777216}, + {name: "zero rejected", set: "GPU_CHUNK_TEST", val: "0", want: 4194304}, + {name: "negative rejected", set: "GPU_CHUNK_TEST", val: "-1", want: 4194304}, + {name: "garbage rejected", set: "GPU_CHUNK_TEST", val: "4M", want: 4194304}, + {name: "overflow rejected", set: "GPU_CHUNK_TEST", val: "99999999999999999999999", want: 4194304}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.set != "" { + t.Setenv(tc.set, tc.val) + } + if got := envUint64("GPU_CHUNK_TEST", 4194304); got != tc.want { + t.Fatalf("envUint64 = %d, want %d", got, tc.want) + } + }) + } +} + +func TestEnvIntMs(t *testing.T) { + cases := []struct { + name string + set string + val string + want int + }{ + {name: "unset returns fallback", set: "", want: 2500}, + {name: "plain value", set: "SEARCH_TIMEOUT_TEST", val: "12000", want: 12000}, + {name: "whitespace trimmed", set: "SEARCH_TIMEOUT_TEST", val: " 6000 ", want: 6000}, + {name: "zero allowed (explicit no sleep)", set: "SEARCH_TIMEOUT_TEST", val: "0", want: 0}, + {name: "negative rejected", set: "SEARCH_TIMEOUT_TEST", val: "-5", want: 2500}, + {name: "garbage rejected", set: "SEARCH_TIMEOUT_TEST", val: "2.5s", want: 2500}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.set != "" { + t.Setenv(tc.set, tc.val) + } + if got := envIntMs("SEARCH_TIMEOUT_TEST", 2500); got != tc.want { + t.Fatalf("envIntMs = %d, want %d", got, tc.want) + } + }) + } +} From e50dfff9a1ae0bbd283c74890a01cb90e55fb093 Mon Sep 17 00:00:00 2001 From: bobbyning Date: Fri, 25 Sep 2026 10:17:15 +0800 Subject: [PATCH 2/2] Reject SEARCH_TIMEOUT_MS=0 for the GPU search timeout. Review finding (qodo): envIntMs accepts zero, which for the search timeout produces an already-expired context for every GPU Search call, so each chunk fails and the worker silently degrades to CPU mining. Add envIntPositive (strictly positive) and use it for the -search-timeout-ms default, preserving envIntMs for callers where zero is a valid value (claim cooldown: 0 = no sleep). Regression test: SEARCH_TIMEOUT_MS=0 falls back to 2500; valid values still apply. --- cmd/workerpoh/main.go | 17 ++++++++++++++++- cmd/workerpoh/main_test.go | 30 +++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/cmd/workerpoh/main.go b/cmd/workerpoh/main.go index 75f71ac1..1462a202 100644 --- a/cmd/workerpoh/main.go +++ b/cmd/workerpoh/main.go @@ -278,6 +278,21 @@ func envIntMs(envKey string, fallback int) int { return x } +// envIntPositive is envIntMs for values where zero is invalid (e.g. the GPU +// search timeout: a zero-duration context is already expired, so every GPU +// Search would fail and silently fall back to CPU). +func envIntPositive(envKey string, fallback int) int { + v := strings.TrimSpace(os.Getenv(envKey)) + if v == "" { + return fallback + } + x, err := strconv.Atoi(v) + if err != nil || x <= 0 { + return fallback + } + return x +} + func envUint64(envKey string, fallback uint64) uint64 { v := strings.TrimSpace(os.Getenv(envKey)) if v == "" { @@ -669,7 +684,7 @@ func main() { workerID = flag.String("worker", strings.TrimSpace(os.Getenv("WORKER_ID")), "worker id") batch = flag.Uint64("batch", 1<<22, "claim batch size") gpuChunk = flag.Uint64("gpu-chunk", envUint64("GPU_CHUNK", 1<<22), "GPU chunk size per Search() call (env GPU_CHUNK)") - searchTimeoutMS = flag.Int("search-timeout-ms", envIntMs("SEARCH_TIMEOUT_MS", 2500), "Search() timeout per GPU chunk (ms) (env SEARCH_TIMEOUT_MS)") + searchTimeoutMS = flag.Int("search-timeout-ms", envIntPositive("SEARCH_TIMEOUT_MS", 2500), "Search() timeout per GPU chunk (ms) (env SEARCH_TIMEOUT_MS)") gpuBackend = flag.String("gpu-backend", strings.TrimSpace(os.Getenv("HACKME_GPU_BACKEND")), "preferred GPU backend: auto|opencl|cuda") gpuDevice = flag.Int("gpu-device", -1, "preferred accelerator device index (-1 = auto)") gpuDisable = flag.Bool("gpu-disable", isTruthy(os.Getenv("HACKME_GPU_DISABLE")), "disable GPU and force CPU mode") diff --git a/cmd/workerpoh/main_test.go b/cmd/workerpoh/main_test.go index 1fb6fcca..aad2fbb0 100644 --- a/cmd/workerpoh/main_test.go +++ b/cmd/workerpoh/main_test.go @@ -39,7 +39,7 @@ func TestEnvIntMs(t *testing.T) { {name: "unset returns fallback", set: "", want: 2500}, {name: "plain value", set: "SEARCH_TIMEOUT_TEST", val: "12000", want: 12000}, {name: "whitespace trimmed", set: "SEARCH_TIMEOUT_TEST", val: " 6000 ", want: 6000}, - {name: "zero allowed (explicit no sleep)", set: "SEARCH_TIMEOUT_TEST", val: "0", want: 0}, + {name: "zero allowed (cooldown semantics: no sleep)", set: "SEARCH_TIMEOUT_TEST", val: "0", want: 0}, {name: "negative rejected", set: "SEARCH_TIMEOUT_TEST", val: "-5", want: 2500}, {name: "garbage rejected", set: "SEARCH_TIMEOUT_TEST", val: "2.5s", want: 2500}, } @@ -54,3 +54,31 @@ func TestEnvIntMs(t *testing.T) { }) } } + +func TestEnvIntPositive(t *testing.T) { + cases := []struct { + name string + set string + val string + want int + }{ + {name: "unset returns fallback", set: "", want: 2500}, + {name: "plain value", set: "SEARCH_TIMEOUT_TEST", val: "12000", want: 12000}, + {name: "whitespace trimmed", set: "SEARCH_TIMEOUT_TEST", val: " 6000 ", want: 6000}, + // Regression: a zero GPU search timeout is an already-expired context, so + // every GPU Search would fail and silently fall back to CPU. + {name: "zero rejected (expired context guard)", set: "SEARCH_TIMEOUT_TEST", val: "0", want: 2500}, + {name: "negative rejected", set: "SEARCH_TIMEOUT_TEST", val: "-5", want: 2500}, + {name: "garbage rejected", set: "SEARCH_TIMEOUT_TEST", val: "2.5s", want: 2500}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.set != "" { + t.Setenv(tc.set, tc.val) + } + if got := envIntPositive("SEARCH_TIMEOUT_TEST", 2500); got != tc.want { + t.Fatalf("envIntPositive = %d, want %d", got, tc.want) + } + }) + } +}