diff --git a/docs/tests.md b/docs/tests.md index 011c273..21fb523 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -63,7 +63,7 @@ $env:LLAMA_GUI_LLAMA_BIN_DIR = 'C:\path\to\llama\bin' node tests/frontend/llama_flags_supported_unit.cjs --require-binaries ``` -An explicit `LLAMA_GUI_LLAMA_BIN_DIR` (or legacy `LLAMA_CPP_BIN_DIR`) requires both executables and disables fallback to other installations. Missing binaries, failed/timed-out `--help`, and unsupported flags fail the check. `--require-binaries` also requires an explicit directory. This checks advertised flag names, including negated booleans; it does not validate enum values or GPU execution. Fork-only flags are excluded. +An explicit `LLAMA_GUI_LLAMA_BIN_DIR` (or legacy `LLAMA_CPP_BIN_DIR`) requires both executables and disables fallback to other installations. Missing binaries, failed/timed-out `--help`, and unsupported flags fail the check. `--require-binaries` also requires an explicit directory. This checks advertised flag names, including negated booleans; it does not validate enum values or GPU execution. Fork-only flags are excluded. Temporary `introduced_after` metadata permits a newly merged flag to be absent only from known builds at or below the last verified unsupported tag; newer and unknown builds remain strict. These windows are tracked in `upstream-changes.md`. The Ubuntu and Windows/Python 3.13 CI jobs run `npm test`, including the unit and browser suites. The Ubuntu job also installs the `libgomp1` OpenMP runtime, downloads the exact CPU release in `tests/llama-cpp-pin.json`, verifies its SHA256, and sets the explicit directory for required binary compatibility. The Windows job uses the same optional binary check as the release workflow. Update the tag, asset, and checksum together from an official llama.cpp release, then run compatibility before accepting the new pin. No model or GPU is needed. diff --git a/docs/upstream-changes.md b/docs/upstream-changes.md index 7d3515f..5e505c5 100644 --- a/docs/upstream-changes.md +++ b/docs/upstream-changes.md @@ -4,6 +4,13 @@ Track announced llama.cpp changes that may require coordinated Llama-GUI updates ## Pending +### Draft physical batch size — release compatibility window + +- **Upstream:** [ggml-org/llama.cpp#30459](https://github.com/ggml-org/llama.cpp/pull/30459), merged October 14, 2026 as `3fb4767`. +- **Local handling:** Configure → Speculative Decoding exposes **Draft Physical Batch Size** (`--spec-draft-n-ubatch`) for server and CLI. Blank omits the argument; 0 inherits the target physical batch size. Smaller values can reduce draft compute-buffer VRAM. The setting is retained but omitted when speculation is disabled or only ngram methods are active. +- **Compatibility:** Verified the official b11617 Windows CPU server and CLI do not advertise this flag. Use a build containing the merge; leave the field blank on older binaries. The definition's temporary `introduced_after: "b11617"` allows known older builds (including the CI pin) to omit it in the compatibility check; newer and unknown builds remain strict. This records a last known unsupported build, not a promised first supported release. +- **Remaining:** Verify the first release advertising the flag, update the CI binary pin with its checksum, and remove `introduced_after` and its compatibility exemption once the supported floor includes it. + ### Direct reads for lazy embedding tables (`--lazy-mode on-direct`) - **Upstream:** [ggml-org/llama.cpp#28136](https://github.com/ggml-org/llama.cpp/pull/28136). diff --git a/tests/frontend/chat_popout_integration.cjs b/tests/frontend/chat_popout_integration.cjs index 3bfa590..9404da6 100644 --- a/tests/frontend/chat_popout_integration.cjs +++ b/tests/frontend/chat_popout_integration.cjs @@ -411,6 +411,13 @@ test("Recover chat here explains ownership contention and retries after the owne assert.equal(await owner.locator("#chat-input").inputValue(), "Keep this unsent draft during recovery."); await owner.close(); + // Closing the page can finish before Chromium releases its Web Lock. + // Recovery intentionally uses ifAvailable, so retry only after release. + await observer.waitForFunction(async () => { + const { held } = await navigator.locks.query(); + const name = window.LlamaGui.chatWindow._hostView.coordinator.lockName; + return !held.some(lock => lock.name === name); + }); await observer.locator(CHAT_RETURN_HERE).click(); await observer.locator("#chat-layout").waitFor({ state: "visible" }); assert.equal(await observer.locator("#chat-input").inputValue(), "Keep this unsent draft during recovery."); diff --git a/tests/frontend/flag_sync_smoke.cjs b/tests/frontend/flag_sync_smoke.cjs index 657326f..9658741 100644 --- a/tests/frontend/flag_sync_smoke.cjs +++ b/tests/frontend/flag_sync_smoke.cjs @@ -137,6 +137,26 @@ async function verifyReasoningPreserve(page) { await page.fill("#config-search", ""); } +async function verifyDraftBatchSize(page) { + await selectSection(page, "configure"); + await page.fill("#config-search", "draft physical batch"); + const input = page.locator("#flag-draft_ubatch_size"); + await input.waitFor({ state: "visible" }); + assert.equal(await input.inputValue(), ""); + assert.equal(await input.getAttribute("min"), "0"); + await page.evaluate(() => window.LlamaGui.flagCore.setFlagValue("spec_type", "draft-mtp")); + await input.fill("8"); + assert.equal(await page.evaluate(() => window.LlamaGui.flagCore.getFlagValues().draft_ubatch_size), 8); + assert.match(await page.textContent("#command-preview-text"), /--spec-draft-n-ubatch 8(?: |$)/); + await page.fill("#config-search", "context"); + await page.fill("#config-search", "draft physical batch"); + assert.equal(await input.inputValue(), "8", "Configure rebuild preserves the shared value"); + await input.fill("0"); + assert.match(await page.textContent("#command-preview-text"), /--spec-draft-n-ubatch 0(?: |$)/); + await input.fill(""); + assert.doesNotMatch(await page.textContent("#command-preview-text"), /--spec-draft-n-ubatch/); +} + async function verifyNgramSimple(page) { await selectSection(page, "configure"); await page.fill("#config-search", "ngram"); @@ -4580,6 +4600,7 @@ for (const phase of ["startup", "reload"]) { } for (const [name, verify = name] of [ + ["draft physical batch size", verifyDraftBatchSize], ["preset imports and safe notifications"], ["mirrored launch controls and Configure search"], ["API key controls and masked command previews"], diff --git a/tests/frontend/launch_args_unit.cjs b/tests/frontend/launch_args_unit.cjs index 6b8428b..18d6b64 100644 --- a/tests/frontend/launch_args_unit.cjs +++ b/tests/frontend/launch_args_unit.cjs @@ -787,6 +787,21 @@ function launchResult() { } } +for (const tool of ["llama-server", "llama-cli"]) { + for (const specType of ["auto", "none", "ngram-simple", "draft-mtp", "draft-simple"]) { + for (const size of [undefined, 0, 8]) { + vm.runInContext(`window.LlamaGui.flagCore.setCurrentToolValue(${JSON.stringify(tool)}); + window.LlamaGui.flagCore.applyFlagValues({ spec_type: ${JSON.stringify(specType)}, + draft_ubatch_size: ${JSON.stringify(size)} });`, context); + const args = Array.from(flatLaunchArgs()); + const index = args.indexOf("--spec-draft-n-ubatch"); + const expected = size !== undefined && ["draft-mtp", "draft-simple"].includes(specType); + assert.equal(index >= 0, expected, `${tool}: draft batch ${size} with ${specType}`); + if (expected) assert.equal(args[index + 1], String(size)); + } + } +} + for (const tool of ["llama-server", "llama-cli"]) { vm.runInContext(` window.LlamaGui.flagCore.setCurrentToolValue(${JSON.stringify(tool)}); @@ -802,7 +817,7 @@ for (const tool of ["llama-server", "llama-cli"]) { vm.runInContext(`window.LlamaGui.flagCore.applyFlagValues({ spec_type: "none", model_draft: "draft.gguf", hf_repo_draft: "org/draft", draft_sampling: "probabilistic", - draft_max: 8, draft_min: 2, draft_p_min: 0.5, draft_p_split: 0.1, + draft_max: 8, draft_min: 2, draft_ubatch_size: 8, draft_p_min: 0.5, draft_p_split: 0.1, gpu_layers_draft: "all", draft_device: "CUDA0", draft_cache_type_k: "q8_0", draft_cache_type_v: "q8_0", ngram_simple: true, ngram_mod: true, ngram_map_k4v: true, @@ -824,6 +839,7 @@ for (const tool of ["llama-server", "llama-cli"]) { assert.equal(args[args.indexOf("--spec-type") + 1], "ngram-mod,ngram-map-k4v,ngram-simple"); assert.equal(args[args.indexOf("-md") + 1], "draft.gguf", "switching back restores saved draft arguments"); assert.equal(args[args.indexOf("--spec-draft-sampling") + 1], "probabilistic", "switching back restores draft sampling"); + assert.equal(args[args.indexOf("--spec-draft-n-ubatch") + 1], "8", "switching back restores the draft batch size"); vm.runInContext(`window.LlamaGui.flagCore.setMultipleFlagValues({ ngram_simple: false, ngram_mod: false, ngram_map_k4v: false, })`, context); diff --git a/tests/frontend/llama_flags_runner_unit.cjs b/tests/frontend/llama_flags_runner_unit.cjs index 450767a..a4e11d4 100644 --- a/tests/frontend/llama_flags_runner_unit.cjs +++ b/tests/frontend/llama_flags_runner_unit.cjs @@ -130,4 +130,18 @@ assert.match(oldBuild.output, /--no-legacy/); const unknownBuild = run({ ...removedPinned, version: { status: 0, stdout: "custom fork build\n" } }); assert.equal(unknownBuild.exitCode, 1, "an unparseable binary version must keep the strict check"); +const pendingFlag = [{ id: "draft_ubatch_size", flag: "--spec-draft-n-ubatch", tool: "both", introduced_after: "b11617" }]; +for (const build of [11515, 11617, 11618, null]) { + const result = run({ + ...pinned, flagDefs: pendingFlag, + version: { status: 0, stdout: build === null ? "custom fork" : `build ${build}` }, + }); + assert.equal(result.exitCode, build !== null && build <= 11617 ? 0 : 1, + "only known older builds may omit a newly merged flag"); +} +assert.equal(run({ ...pinned, flagDefs: pendingFlag, + help: { status: 0, stdout: "--spec-draft-n-ubatch N" }, + version: { status: 0, stdout: "build 11618" }, +}).exitCode, 0); + console.log("llama flag runner selection, strict failures and compatibility checks passed"); diff --git a/tests/frontend/llama_flags_supported_unit.cjs b/tests/frontend/llama_flags_supported_unit.cjs index a579d9a..44d9e65 100644 --- a/tests/frontend/llama_flags_supported_unit.cjs +++ b/tests/frontend/llama_flags_supported_unit.cjs @@ -134,6 +134,7 @@ function collectUnsupportedFlags(flags, advertisedByTool, buildByTool) { // from upstream builds on purpose; never hold them against a binary. if (flag.fork_only) continue; const removedInBuild = parseRemovedInBuild(flag.removed_in); + const introducedAfterBuild = parseRemovedInBuild(flag.introduced_after); for (const option of [flag.flag, flag.false_flag].filter(Boolean)) { for (const tool of expectedTools(flag)) { const advertised = advertisedByTool[tool]; @@ -143,6 +144,12 @@ function collectUnsupportedFlags(flags, advertisedByTool, buildByTool) { // docs/upstream-changes.md) stay in the GUI for older // builds; builds at or above the removal tag may lack them. const build = buildByTool[tool]; + // Merged flags awaiting a release may be absent from known + // older builds. Unknown/newer builds must advertise them. + if (introducedAfterBuild !== null && build !== null && build <= introducedAfterBuild) { + console.log(`llama flag compatibility: ${tool}: ${option} requires a build after b${introducedAfterBuild} (see docs/upstream-changes.md)`); + continue; + } if (removedInBuild !== null && build !== null && build >= removedInBuild) { removedExemptions.push({ tool, id: flag.id, option, build, removedInBuild }); continue; diff --git a/ui/js/flags/definitions-speculative.js b/ui/js/flags/definitions-speculative.js index 9d308da..7260db2 100644 --- a/ui/js/flags/definitions-speculative.js +++ b/ui/js/flags/definitions-speculative.js @@ -38,6 +38,18 @@ const FLAG_DEFINITIONS_SPECULATIVE = [ min: 0, placeholder: "llama.cpp default", }, + { + id: "draft_ubatch_size", + flag: "--spec-draft-n-ubatch", + introduced_after: "b11617", + category: "speculative", + type: "int", + label: "Draft Physical Batch Size", + desc: "Physical maximum batch size of the draft context. Smaller values can reduce draft compute-buffer VRAM use. 0 inherits the main model's Physical Batch Size (-ub). Requires a build containing PR #30459 (not available in b11617 or earlier); leave blank for older builds.", + tool: "both", + min: 0, + placeholder: "0 (inherit main batch size)", + }, { id: "draft_p_min", flag: "--spec-draft-p-min", diff --git a/ui/js/flags/helpers.js b/ui/js/flags/helpers.js index b3b0883..a65878d 100644 --- a/ui/js/flags/helpers.js +++ b/ui/js/flags/helpers.js @@ -101,6 +101,7 @@ function shouldOmitSpeculativeFlag(f, values) { const draftModelOnlyFlags = new Set([ "draft_max", "draft_min", + "draft_ubatch_size", "draft_p_min", "draft_p_split", "gpu_layers_draft",