Skip to content

Configurable fine-tuning API (TrainingParameters + Optimizer)#289

Merged
bernardladenthin merged 2 commits into
bernardladenthin:mainfrom
vaiju1981:finetuner-api
Jul 2, 2026
Merged

Configurable fine-tuning API (TrainingParameters + Optimizer)#289
bernardladenthin merged 2 commits into
bernardladenthin:mainfrom
vaiju1981:finetuner-api

Conversation

@vaiju1981

Copy link
Copy Markdown

Summary

Builds a configurable fine-tuning API on top of the training proof of concept (#287): a
TrainingParameters builder — serialized as JSON across JNI, the same idiom as ModelParameters /
InferenceParameters — driving LlamaTrainer.finetune(TrainingParameters).

LlamaTrainer.finetune(
    TrainingParameters.builder()
        .modelPath(Path.of("base.gguf"))
        .trainingFile(Path.of("corpus.txt"))     // or .trainingText("...")
        .outputPath(Path.of("tuned.gguf"))
        .epochs(3).learningRate(1e-5f).optimizer(Optimizer.ADAMW)
        .lrMin(1e-6f).decayEpochs(3f).weightDecay(0.01f)
        .valSplit(0.1f).nCtx(512).nGpuLayers(0).nBatch(256).nUbatch(64)
        .build());

This PR includes the POC commit from #287, so it supersedes it — #287 can be closed in favor of
this (or merged first, in which case this reduces to the FineTuner delta).

What it exposes

Every training knob the ggml-opt path supports:

  • Corpus from inline text or a file (read natively, so a large corpus doesn't round-trip
    through the JVM heap)
  • Optimizer — AdamW / SGD (Optimizer enum → ggml_opt_optimizer_type)
  • Learning-rate schedule — lr0, lr_min, decay_epochs, weight_decay
  • Validation split, context size, GPU layers, logical/physical batch sizes

train_engine.cpp parses the JSON config, applies the knobs to common_params, and runs the same
llama_opt_init / llama_opt_epoch loop as upstream examples/training/finetune.cpp.

Verified

  • Native library builds and links against b9842; finetuneNative is exported; the library loads
    (NativeLibraryLoadSmokeTest).
  • TrainingParameters builder→JSON unit tests pass (3), model-free.
  • LlamaTrainer + TrainingParameters + Optimizer compile through the strict Error Prone / NullAway
    pipeline.
  • The actual training run is exercised by the model-gated LlamaTrainerIntegrationTest, which
    self-skips unless -Dnet.ladenthin.llama.train.model=… is set.

Deferred to a focused follow-up

  • Progress callbacks (native → Java per-epoch loss/progress). ggml_opt_epoch_callback has no
    userdata slot, so a Java callback needs a thread-local trampoline; since it can't be exercised
    without an actual training run, it is kept out of this change to get proper attention on its own.
  • LoRA-target selective fine-tuning (a custom llama_opt_param_filter rather than _all) — more
    involved; upstream's own finetune.cpp trains all parameters.

vaiju1981 added 2 commits July 1, 2026 12:21
Wire llama.cpp's ggml-opt training path into the JNI layer, mirroring upstream
examples/training/finetune.cpp: load a model, tokenize a text corpus into a
ggml-opt dataset, run llama_opt_init + llama_opt_epoch for N epochs, and write
the fine-tuned GGUF via llama_model_save_to_file.

- train_engine.{h,cpp} - self-contained native finetune(), independent of the
  inference server_context (loads its own model + context; forces no-mmap and an
  f32 KV cache, as training requires)
- LlamaTrainer - minimal Java entry point (static finetune(...) overloads)
- CMakeLists.txt - compile train_engine.cpp into libjllama

The ggml-opt / llama_opt symbols already link into the static libjllama with no
build-system change (verified with nm), so this is pure JNI + C++ wiring. The
finetuneNative symbol is exported, the library links and loads cleanly, and the
Java layer compiles through the strict Error Prone / NullAway pipeline.

Scope is deliberately a proof of concept: full-model fine-tuning is compute- and
memory-intensive and upstream training support is experimental. The actual
training run is exercised by a model-gated integration test that self-skips
unless -Dnet.ladenthin.llama.train.model is set. A richer FineTuner API (dataset
handling, optimizer / LoRA options, progress callbacks) can build on this base.
Build a real fine-tuning surface on top of the training POC: replace the
fixed-arg LlamaTrainer.finetune with a TrainingParameters builder serialized as
JSON across the JNI boundary (the same idiom as ModelParameters /
InferenceParameters).

Exposes the training knobs the ggml-opt path supports:
- corpus from inline text OR a file (read natively)
- optimizer selection (AdamW / SGD) via the Optimizer enum
- learning-rate schedule (lr0, lr_min, decay_epochs, weight_decay)
- validation split, context size, GPU layers, logical/physical batch sizes

train_engine parses the JSON config, applies the knobs to common_params, and
otherwise runs the same llama_opt_init / llama_opt_epoch loop as the POC.

Verified: the native library builds and links against b9842, finetuneNative is
exported, and the library loads; TrainingParameters builder->JSON unit tests
pass (3); the model-gated integration test now drives the run through
TrainingParameters and self-skips without a model.

Progress callbacks (native->Java per-epoch loss) are the planned next step: the
ggml_opt_epoch_callback has no userdata slot, so that needs a thread-local
trampoline and is kept out of this change until it can be exercised end to end.
@bernardladenthin bernardladenthin merged commit 9d45262 into bernardladenthin:main Jul 2, 2026
41 of 47 checks passed
bernardladenthin pushed a commit that referenced this pull request Jul 2, 2026
Brings the merged LlamaTrainer / TrainingParameters fine-tuning feature (#289)
onto the llama/ reactor structure. Git's directory-rename detection correctly
followed the src/ -> llama/src/ move: the 7 new training files
(train_engine.{cpp,h}, LlamaTrainer.java, args/Optimizer.java,
parameters/TrainingParameters.java, and the two tests) were placed under
llama/src/... and accepted there; llama/CMakeLists.txt auto-merged to keep the
`src/main/cpp/train_engine.cpp` source line (relative to llama/, so it compiles
as part of the core module). No root src/ or root CMakeLists.txt left behind.

Verified locally: reactor Java build green (`mvn -pl llama -am install` — the new
training files compile under Error Prone/NullAway + Lombok); TrainingParametersTest
(3 model-free tests) passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rt1paYztGJ2AKUuBuAGDXE
bernardladenthin pushed a commit that referenced this pull request Jul 2, 2026
…README

- TrainingParameters: keep class-level Lombok @builder @Getter but satisfy the
  strict javadoc build (failOnWarnings) — replace the raw-source-invisible
  {@link #builder()} with {@code builder()} and add an explicit private all-args
  constructor so javadoc sees a real constructor (no synthetic-default warning);
  Lombok generates the builder around it. Verified: compile + javadoc:jar clean,
  TrainingParametersTest (3) green.
- publish.yml: re-root the Java test jobs' crash-dump/surefire upload globs to
  llama/ (hs_err_pid*, *.hprof, target/surefire-reports/*) now that surefire runs
  against -f llama/pom.xml; core.* stays at workspace root (absolute core_pattern).
- README: add Jlama and LangChain4j to the Similar Projects section.
- Apply clang-format 22.1.5 (train_engine.cpp) and spotless (Java) from the #289 merge.
- .gitignore: catch llama/.jqwik-database (un-anchored pattern).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rt1paYztGJ2AKUuBuAGDXE
bernardladenthin pushed a commit that referenced this pull request Jul 2, 2026
The #289 merge introduced the first Lombok-@Builder-heavy class and it had not
cleared the strict quality gates:

SpotBugs (13 findings), all fixed while keeping class-level @builder:
- 10x MRC_METHOD_RETURNS_CONSTANT on Lombok's synthetic $default$*() methods:
  @Builder.Default emits these, and they are NOT tagged @lombok.Generated even
  with lombok.addLombokGeneratedAnnotation=true (a Lombok limitation), so
  SpotBugs cannot auto-skip them. Suppressed for the $default$* methods only.
- IMC_IMMATURE_CLASS_NO_TOSTRING: add @tostring (generated, @lombok.Generated).
- IMC_IMMATURE_CLASS_WRONG_FIELD_ORDER: move the static MAPPER above the
  instance fields.
- RCN_REDUNDANT_NULLCHECK in LlamaTrainer: the @NullMarked default treated the
  native finetuneNative() return as @nonnull; mark it @nullable (JNI can return
  null on success), making the `error != null` guard meaningful.

PIT (was 99% < 100% threshold):
- Optimizer.getNativeValue() showed as NO_COVERAGE because its only test lived in
  TrainingParametersTest (package parameters.*), outside PIT's args.*/value.*/
  exception.*/json.* targetTests. Move it to a dedicated args.OptimizerTest so
  PIT runs it. Score back to 100% (249/249).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rt1paYztGJ2AKUuBuAGDXE
@vaiju1981 vaiju1981 deleted the finetuner-api branch July 2, 2026 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants