Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,44 @@ The release procedure (prompt template and step-by-step instructions) lives in [

---

## [Unreleased]

### Added
- **`flashAttn` works.** The knob was documented and settable since it was introduced, but could not
be forwarded: `--flash-attn` takes a mandatory `on|off|auto` and `net.ladenthin:llama` offered only
a bare-flag setter, which emitted the key alone and made llama.cpp's parser consume the following
argv token — the load then died naming a flag the user never set. 1.2.0 shipped a refusal at plan
time rather than that diagnostic.

`net.ladenthin:llama` **5.2.0** adds `ModelParameters.setFlashAttn(FlashAttn)`, so the provider now
emits `--flash-attn on` when the knob is set. `false` still emits nothing, which leaves llama.cpp's
own `auto` default in force — that is the correct behaviour for an unconfigured run, and it is what
the knob's javadoc now says.

**The knob is exercised against a real model for the first time.** `flashAttn` moves out of
`LlamaCppJniKnobSweepTest`'s `NOT_SWEPT` list into a real sweep case; until now only the refusal had
ever been executed.

### Removed
- The plan-time and provider-side refusals (`EngineSupport.validateFlashAttnIsNotRequested`,
`LlamaCppJniAiGenerationProvider.FLASH_ATTN_UNSUPPORTED_MESSAGE`) and the three tests that pinned
them.

### Changed
- **`lazyMode` replaces `tensorReadLazy` — breaking.** llama.cpp b10731 renamed `--tensor-read-lazy`
to `-lzm` / `--lazy-mode` with no alias, and `net.ladenthin:llama` followed the rename rather than
papering over it, so the model-definition field, its `srcmorph.llama.*` surface, the
`LlamaCppJniConfig` accessor and the sweep case move with it. Carrying the old name would leave the
configuration describing a flag that no longer exists.

Worth knowing why this is a real hazard and not cosmetics: a **new Java name paired with an old
native** produces `Failed to parse model parameters` at load time and nothing earlier catches it.
The knob sweep did — it is what surfaced the mismatch here.

- **`net.ladenthin:llama` 5.1.0 → 5.2.0-SNAPSHOT.** Deliberately a snapshot: the binding change this
release depends on is not yet published. Building srcmorph therefore requires that snapshot to be
resolvable, and CI stays red until it is — recorded here so nobody mistakes it for a regression.

## [1.2.0] - 2026-09-01

### Added
Expand Down
29 changes: 1 addition & 28 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,41 +47,14 @@ recorded in git history and `crossrepostatus.md`, not here.
its fix: the class generates no mutants, so neither the bug nor the tests that now cover it move
the number. Do not read that stability as reassurance; PIT structurally could not have caught this.
It is now worth adding, because the class finally has model-free coverage of the parts that matter
(`buildInferenceParameters`, `warnOnTruncatedAnswer`, `logPromptCacheReuse`, `tensorReadLazyMode`,
(`buildInferenceParameters`, `warnOnTruncatedAnswer`, `logPromptCacheReuse`, `lazyMode`,
`cacheType`). **Measure before committing to it**: the real-model tests take ~4-16 s each, and PIT
re-runs every test covering a mutated line, so mutants in `model()` — the long `ModelParameters`
chain — could make the run far slower than the current few minutes. If it does, the answer is
probably `excludedTestClasses` for the real-model tests plus mutants restricted to the pure paths,
not dropping the idea. Deliberately out of scope for 1.2.0: it is a build-time question, not a
correctness one.

- **Wire `flashAttn` up for real after `net.ladenthin:llama` 5.2.0 ships.** 1.2.0 *refuses* the knob
rather than forwarding it, in two places (`LlamaCppJniAiGenerationProvider.model()` for the direct
API path, `EngineSupport.validateFlashAttnIsNotRequested` for the plan phase), because the binding
cannot currently spell it: `-fa` takes a mandatory `[on|off|auto]`, `enableFlashAttn()` routes
through `setFlag` which stores `null`, and the argv renderer then emits the key with no token after
it — so llama.cpp swallows the *next* argv token and the load dies naming a flag the user never set
(`error: unknown value for --flash-attn: '--reasoning-format'`, reproduced against the shipped fat
jar). No workaround exists downstream: `putScalar` is `protected`.

When 5.2.0 lands with a value-taking setter: bump `llama.version`, replace both guards with the real
call, delete `FLASH_ATTN_UNSUPPORTED_MESSAGE` and the four tests that pin the refusal (two in
`EngineSupportTest`, one in `LlamaCppJniAiGenerationProviderTest`, and
`LlamaCppJniKnobSweepTest#flashAttn_isRefusedRatherThanSilentlyDropped`), and restore the
`flashAttn`/`cacheTypeV` guidance in `srcmorph-maven-plugin/README.md`. Then move `flashAttn` out of
`LlamaCppJniKnobSweepTest.NOT_SWEPT` into a real sweep case — that is the step that finally sets the
knob against a real model, which the refusal has never done. The sweep's completeness check keeps
the bookkeeping honest: it fails if the knob is neither swept nor excluded, so it cannot be left
half-migrated.

**The trap to avoid: bumping `llama.version` alone changes nothing visible.** The guards keep
throwing, the knob keeps looking broken, and every gate stays green — so this has to be an explicit
line on the bump checklist rather than something a version bump surfaces on its own. The upstream
fix is tracked in java-llama.cpp (`ModelParameters` needs `setFlashAttn(on|off|auto)` mirroring the
`CacheType` enum pattern, `enableFlashAttn()` deprecated, and
`ModelParametersExtendedTest.testToArrayComplexCombination` corrected — it currently pins the broken
9-token argv shape as correct).

- **`enable_thinking` is sent unconditionally, including at its own default.**
`LlamaCppJniAiGenerationProvider.model()` always puts `enable_thinking` into
`chatTemplateKwargs`, at whatever `chatTemplateEnableThinking` says — and its default is `true`.
Expand Down
2 changes: 1 addition & 1 deletion srcmorph-maven-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ below are the shipped values (`AiGenerationConfig.DEFAULT_*`).
| `batchSize` | `-1` | Logical maximum batch size (`--batch-size`); `-1` = leave default. A prefill-throughput knob, and prefill is what dominates an indexing run: every file is one large prompt with a short answer. Must be positive when set |
| `ubatchSize` | `-1` | Physical maximum batch size (`--ubatch-size`); `-1` = leave default. Bounds how much of a logical batch reaches the backend at once, so it trades prefill speed against peak memory. Must be positive when set |
| `threadsBatch` | `-1` | Threads for batch/prompt processing (`--threads-batch`); `-1` = reuse `threads`. Prefill parallelises differently from decode, so the optimum often is not the decode thread count — on a machine with efficiency cores, more threads help prompt processing while hurting generation. Must be positive when set |
| `tensorReadLazy` | *(empty)* | On-demand reading of lazy-loadable tensors (`--tensor-read-lazy`): `off`, `auto` or `on`; empty leaves the default. Trades resident memory for disk reads and shortens model load time; **requires mmap** |
| `lazyMode` | *(empty)* | On-demand reading of lazy-loadable tensors (`--tensor-read-lazy`): `off`, `auto` or `on`; empty leaves the default. Trades resident memory for disk reads and shortens model load time; **requires mmap** |
| `mainGpu` | `-1` | Primary GPU index (`--main-gpu`); `-1` = leave default. Matters on multi-GPU hosts (e.g. a Vulkan build enumerates every GPU) |
| `devices` | *(empty)* | Explicit device selection (`--device`), comma-separated backend device names (e.g. `Vulkan1`); takes precedence over `mainGpu` |
| `chatTemplateEnableThinking` | `true` | Enable the chat template's thinking mode |
Expand Down
16 changes: 14 additions & 2 deletions srcmorph/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,20 @@ SPDX-License-Identifier: Apache-2.0
never had an effect, and upstream llama.cpp defaults use_jinja to true, so chat
templating is unchanged. The rest of the provider's API surface (LlamaModel,
InferenceParameters, ModelParameters, ChatResponse/Timings/Pair, ChatResponseParser,
ReasoningFormat) is untouched by 5.1.0. -->
<llama.version>5.1.0</llama.version>
ReasoningFormat) is untouched by 5.1.0.
5.2.0 is a BREAKING bump this module needs, not an optional one: the binding removed
ModelParameters.enableFlashAttn()/ModelFlag.FLASH_ATTN (--flash-attn takes a mandatory
on|off|auto value since llama.cpp b10273, so the valueless flag emitted a broken argv)
and renamed setTensorReadLazy/TensorReadLazyMode to setLazyMode/LazyMode (llama.cpp
b10731 renamed --tensor-read-lazy to --lazy-mode with no alias). The provider calls
both, so it cannot compile against 5.1.0 any more.
This deliberately names the RELEASE, not 5.2.0-SNAPSHOT: a snapshot would need a
<repositories> entry for the Central snapshot channel (this repo declares none -- the
distributionManagement/snapshotRepository below is deploy-only) AND would block every
srcmorph release, since Central rejects a release with a -SNAPSHOT dependency. The
build therefore stays red until net.ladenthin:llama:5.2.0 is published, and goes green
with no further edit the moment it is. -->
<llama.version>5.2.0</llama.version>
<!-- Native build selector for net.ladenthin:llama. Empty = the main jar (CPU, Ninja default).
The gpu-cuda / gpu-vulkan profiles set a GPU classifier; or override with
-Dllama.classifier=cuda13-windows-x86-64. GPU builds need their runtime on PATH
Expand Down
2 changes: 1 addition & 1 deletion srcmorph/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
* file</em>, and irrelevant to <em>running the result</em>. On the module path every llama.cpp-backed
* path, including the plan-time {@code GgufModelInspector}, failed for want of a readable module.
* It is deliberately <b>not</b> {@code transitive}: the binding's types appear only in
* package-private members ({@code tensorReadLazyMode}, {@code cacheType},
* package-private members ({@code lazyMode}, {@code cacheType},
* {@code buildInferenceParameters}), never in an exported signature — {@code LlamaCppJniConfigFactory}
* returns srcmorph's own {@code LlamaCppJniConfig} — so no consumer needs the module transitively.
* Should a llama type ever surface in an exported signature, this must become
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ public AiGenerationConfig() {
* <p>Deferring tensor reads can shorten model load time, which matters here because a run loads
* and unloads one model per model group and {@code calibrate} preflights every model in turn.</p>
*/
public static final String DEFAULT_TENSOR_READ_LAZY = "";
public static final String DEFAULT_LAZY_MODE = "";

/**
* Default repeat-penalty window ({@code --repeat-last-n}). {@code -1} (default) means "do not set
Expand Down Expand Up @@ -280,17 +280,18 @@ public AiGenerationConfig() {
/**
* Default Flash Attention setting ({@code --flash-attn}).
*
* <p><b>Enabling this is currently refused</b> &mdash; see
* {@code LlamaCppJniAiGenerationProvider.FLASH_ATTN_UNSUPPORTED_MESSAGE}. The binding emits
* {@code --flash-attn} without the {@code [on|off|auto]} value llama.cpp requires, so the parser
* swallows the following argv token and the model load dies naming an unrelated flag. srcmorph fails
* at plan time instead, until a binding release exposes a value-taking setter.</p>
* <p>{@code true} emits {@code --flash-attn on}; {@code false} emits nothing at all, which leaves
* llama.cpp's own default in force — and that default is {@code auto}, not off ({@code common.h}:
* {@code flash_attn_type = LLAMA_FLASH_ATTN_TYPE_AUTO}). An earlier version of this javadoc said
* {@code false} "leaves it off, as llama.cpp does"; both halves were wrong. Enabling it lowers
* KV-cache memory and is the precondition for quantizing the V cache
* ({@link #DEFAULT_CACHE_TYPE_V}).</p>
*
* <p>{@code false} therefore emits nothing at all, which leaves llama.cpp's own default in force —
* and that default is {@code auto}, not off ({@code common.h}: {@code flash_attn_type =
* LLAMA_FLASH_ATTN_TYPE_AUTO}). An earlier version of this javadoc said {@code false} "leaves it off,
* as llama.cpp does"; both halves were wrong. Once the knob works, enabling it lowers KV-cache memory
* and is the precondition for quantizing the V cache ({@link #DEFAULT_CACHE_TYPE_V}).</p>
* <p>The value matters, and that is why the knob went through a release refusing to be set at all:
* {@code --flash-attn} takes a mandatory {@code [on|off|auto]}, and the binding used to offer only a
* bare-flag setter, which emitted the key alone and made llama.cpp's parser consume the next argv
* token — killing the load while naming a flag the user never set. {@code net.ladenthin:llama}
* 5.2.0 added {@code ModelParameters.setFlashAttn(FlashAttn)}, which is what the provider uses.</p>
*/
public static final boolean DEFAULT_FLASH_ATTN = false;

Expand Down Expand Up @@ -395,7 +396,7 @@ public AiGenerationConfig() {
private int cpuMoeLayers = DEFAULT_CPU_MOE_LAYERS;
private int cpuFfnLayers = DEFAULT_CPU_FFN_LAYERS;
private int kvUnifiedPerSlot = DEFAULT_KV_UNIFIED_PER_SLOT;
private String tensorReadLazy = DEFAULT_TENSOR_READ_LAZY;
private String lazyMode = DEFAULT_LAZY_MODE;
private int repeatLastN = DEFAULT_REPEAT_LAST_N;
private String cacheTypeK = DEFAULT_CACHE_TYPE_K;
private String cacheTypeV = DEFAULT_CACHE_TYPE_V;
Expand Down Expand Up @@ -811,17 +812,17 @@ public void setKvUnifiedPerSlot(final int kvUnifiedPerSlot) {
*
* @return {@code off}, {@code auto}, {@code on}, or empty to leave the default
*/
public String getTensorReadLazy() {
return tensorReadLazy;
public String getLazyMode() {
return lazyMode;
}

/**
* Sets the tensor-read laziness ({@code --tensor-read-lazy}).
*
* @param tensorReadLazy {@code off}, {@code auto}, {@code on}, or empty/{@code null} to leave the default
* @param lazyMode {@code off}, {@code auto}, {@code on}, or empty/{@code null} to leave the default
*/
public void setTensorReadLazy(final @Nullable String tensorReadLazy) {
this.tensorReadLazy = tensorReadLazy != null ? tensorReadLazy : DEFAULT_TENSOR_READ_LAZY;
public void setLazyMode(final @Nullable String lazyMode) {
this.lazyMode = lazyMode != null ? lazyMode : DEFAULT_LAZY_MODE;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public AiModelDefinition() {
private int cpuMoeLayers = AiGenerationConfig.DEFAULT_CPU_MOE_LAYERS;
private int cpuFfnLayers = AiGenerationConfig.DEFAULT_CPU_FFN_LAYERS;
private int kvUnifiedPerSlot = AiGenerationConfig.DEFAULT_KV_UNIFIED_PER_SLOT;
private String tensorReadLazy = AiGenerationConfig.DEFAULT_TENSOR_READ_LAZY;
private String lazyMode = AiGenerationConfig.DEFAULT_LAZY_MODE;
private int repeatLastN = AiGenerationConfig.DEFAULT_REPEAT_LAST_N;
private String cacheTypeK = AiGenerationConfig.DEFAULT_CACHE_TYPE_K;
private String cacheTypeV = AiGenerationConfig.DEFAULT_CACHE_TYPE_V;
Expand Down Expand Up @@ -496,17 +496,17 @@ public void setKvUnifiedPerSlot(final int kvUnifiedPerSlot) {
*
* @return {@code off}, {@code auto}, {@code on}, or empty to leave the default
*/
public String getTensorReadLazy() {
return tensorReadLazy;
public String getLazyMode() {
return lazyMode;
}

/**
* Sets the tensor-read laziness ({@code --tensor-read-lazy}) for this model.
*
* @param tensorReadLazy {@code off}, {@code auto}, {@code on}, or empty/{@code null} to leave the default
* @param lazyMode {@code off}, {@code auto}, {@code on}, or empty/{@code null} to leave the default
*/
public void setTensorReadLazy(final @Nullable String tensorReadLazy) {
this.tensorReadLazy = tensorReadLazy != null ? tensorReadLazy : AiGenerationConfig.DEFAULT_TENSOR_READ_LAZY;
public void setLazyMode(final @Nullable String lazyMode) {
this.lazyMode = lazyMode != null ? lazyMode : AiGenerationConfig.DEFAULT_LAZY_MODE;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ private static AiGenerationConfig toConfig(final AiModelDefinition definition) {
config.setCpuMoeLayers(definition.getCpuMoeLayers());
config.setCpuFfnLayers(definition.getCpuFfnLayers());
config.setKvUnifiedPerSlot(definition.getKvUnifiedPerSlot());
config.setTensorReadLazy(definition.getTensorReadLazy());
config.setLazyMode(definition.getLazyMode());
config.setRepeatLastN(definition.getRepeatLastN());
config.setCacheTypeK(definition.getCacheTypeK());
config.setCacheTypeV(definition.getCacheTypeV());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import net.ladenthin.srcmorph.provider.AiGenerationProviderFactory;
import net.ladenthin.srcmorph.provider.GgufModelInfo;
import net.ladenthin.srcmorph.provider.GgufModelInspector;
import net.ladenthin.srcmorph.provider.LlamaCppJniAiGenerationProvider;
import net.ladenthin.srcmorph.provider.LlamaCppJniConfig;
import net.ladenthin.srcmorph.provider.LlamaCppJniConfigFactory;
import org.jspecify.annotations.Nullable;
Expand Down Expand Up @@ -198,34 +197,10 @@ static void validateRoutedModelPaths(
+ " (checked before any model is loaded; fix the modelPath, or set"
+ " generationProvider to mock for a model-free run)");
}
validateFlashAttnIsNotRequested(aiDefinitionKey, modelConfig);
validateAgainstTheModelItself(aiDefinitionKey, modelConfig, Paths.get(modelPath));
}
}

/**
* Refuses {@code flashAttn} at plan time, before any model is loaded or any file written.
*
* <p>The knob is documented and settable but cannot work with the pinned binding: {@code -fa} takes a
* mandatory {@code [on|off|auto]} value and {@code ModelParameters.enableFlashAttn()} emits the flag
* without one, so llama.cpp swallows the following argv token and the load dies naming a flag the
* user never set. Refusing here rather than in the provider means a multi-model run fails before its
* first group generates, instead of after.</p>
*
* @param aiDefinitionKey the model definition being validated, for the message
* @param modelConfig its resolved configuration
* @throws SrcMorphException when {@code flashAttn} is enabled
*/
private static void validateFlashAttnIsNotRequested(
final String aiDefinitionKey, final AiGenerationConfig modelConfig) throws SrcMorphException {
// TODO: remove together with the provider-side guard once net.ladenthin:llama 5.2.0 exposes a
// value-taking Flash Attention setter -- see the TODO in LlamaCppJniAiGenerationProvider.model().
if (modelConfig.isFlashAttn()) {
throw new SrcMorphException("aiDefinition '" + aiDefinitionKey + "': "
+ LlamaCppJniAiGenerationProvider.FLASH_ATTN_UNSUPPORTED_MESSAGE);
}
}

/**
* Checks the configuration against what the GGUF file itself declares, still without loading it.
*
Expand Down
Loading
Loading