Status¶
Session 51: Documentation update: docs/ folder restructured as juno-documentation MyST Jupyter Book.
juno-documentation¶
The flat
docs/folder has been restructured intojuno-documentation/, a standalone MyST-MD Jupyter Book configured viamyst.yml.Content is organised into 11 parts and 54 chapters, each in its own
.mdfile underpart1/throughpart11/. Navigation links (<-/->) and a full Table of Contents inindex.mdcross-link every chapter.All Mermaid diagrams are declared with the MyST
{mermaid}directive where applicable and render natively in the built book and in any Mermaid-aware viewer.A
references.mdback-matter table maps every chapter back to the originating file indocs/.build.shprovides a one-command build (./build.sh);README.mddocuments prerequisites and the local preview workflow.
Status¶
Session 50: /train-file-qa: multi-fact Q&A from a JSON file in one training loop; HTTP API.
/train-file-qa¶
REPL command loads a
.jsonarray of{"Q","A"}objects viaLoraQaFile.Each pair expands to the same four chat-templated variants as
/train-qa; all units train in onetrainOnUnitspass with QA loss targets.LoraTrainer.trainQaPairsUntilResultfor the programmatic multi-pair path.LoraApiServer: with./juno lora --api-port N:POST /v1/lora/train-file-qa(JSON body) andPOST /v1/lora/savefor curl workflows.Dropped verbose
[TRACE]dump of formatted training text / token IDs on/train-qa.Docs:
docs/LoRA.md,docs/howto.md.
Status¶
Session 49: LoRA Tier 11 (complete): --lora-microbatch CLI/env + VRAM OOM auto-fallback.
LoRA microbatch CLI and VRAM ladder (Tier 11)¶
LoraMicrobatch:--lora-microbatch N/LORA_MICROBATCH(default 8, range 1..128); appliesjuno.lora.microbatchbefore resident upload (noJAVA_TOOL_OPTIONSrequired).LoraResidentUpload: on FP32 microbatch VRAM OOM with half support: close, set microbatch=1, retry FP16 once; further OOM uses existing auto→CPU / gpu fail-closed policy.Wired through
LoraCliOptions,LoraTrainingConfig,ConsoleMain,LoraTrainer,scripts/run.sh/run.bat, and all three LoRA training handlers.Docs:
docs/LoRA.md,docs/howto.md,docs/performance.md,docs/agent-arch.txt.
Status¶
Session 48: LoRA Tier 9 (complete): microbatch GEMM + published GPU speed gates.
LoRA GPU microbatch and product gates (Tier 9)¶
GpuBlasOps/DeviceActivationBatch: FP32cublasSgemm_v2/rocblas_sgemmmicrobatch for frozen forward and transpose; CPU oracleCpuFrozenBatchOps.Default
juno.lora.microbatch=8uploads FP32 resident weights and batches linears across positions inLoraTrainableHandler.computeGradients(host adapters / Adam unchanged).LoraTrainableHandlerGpuBackwardTest: CPU↔GPU loss/grad parity + TinyLlama speed gates (GTX 1080: ~14× e2e, ~11× backward vs CPU).Docs may describe production GPU LoRA training as frozen batched GPU + host adapters; device-resident adapters / GPU Adam remain deferred (not required after intensity proof).
--lora-train-deviceand LLaMA/Qwen2 timing subsets remain as in Session 46 (transferMsstill 0).
Status¶
Session 47: LoRA Tier 10 (complete): multi-arch GPU residency + production gates.
LoRA multi-arch GPU residency (Tier 10)¶
LoraResidentWeights: shared upload / close / VRAM-OOM fallback / matVec+transpose routing.LoraTrainableHandlerrefactored onto the helper (LLaMA-family / Qwen2 unchanged behavior).Phi3LoraTrainableHandler/Qwen3LoraTrainableHandlerupload physical fused (Phi) or dense (Qwen3) projections when--lora-train-deviceresolves to aGpuMatVec; CPU fallback preserved.Gated live LoRA smokes (
LoraLiveSmokeTest) for TinyLlama / Qwen2.5 / Phi-3.5 / dense Qwen3 fixtures.EosOutputFilter: hold back / strip turn-end markers (</s>,<|end|>,<|im_end|>, …) so/train-qacompletions never stream into REPL orGenerationResulttext (all LoRA chat templates).DoRA: correctness-complete, not production-perf-gated (prefer LoRA/rsLoRA for large all-linear jobs).
Tier 7 JFR metrics marked complete (programmatic
--jfr, mode identity, extractor, docs).Tier 5 held-out research / quality matrix remains deferred; exact K-quant QA-LoRA merge unsupported.
Status¶
Session 46: LoRA Tier 9 (start → completed in Session 48): --lora-train-device productization.
LoRA GPU train-device (Tier 9)¶
--lora-train-device auto|gpu|cpu/LORA_TRAIN_DEVICE(default auto).LoraTrainDevice: MatVec selection;gpufails closed without CUDA/ROCm;cpuforcesCpuMatVec.LoraTrainer/ LoRA REPL honor the mode; JFRtrainDeviceis the resolved label (cpu/cuda/rocm).LoraStepTiming: fillsfrozenForwardMs/frozenTransposeBackwardMs/adapterBackwardMs/attentionNonlinearMsonjuno.LoraTrainStepfrom LLaMA/Qwen2 handler instrumentation (transferMsstill 0 until H2D counters).Microbatch / parity IT / speed gates: completed in Session 48.
Status¶
Session 45: LoRA Tier 8: train-file scheduling and corpus caps.
LoRA train-file scheduling (Tier 8)¶
--lora-chunk-tokens/LORA_CHUNK_TOKENS(default 32; recommend 128 for large/train-file).--lora-max-train-tokens/LORA_MAX_TRAIN_TOKENS(0= unlimited): seeded whole-chunk subsample of supervised prediction tokens./trainand/train-fileuse document-levelTrainUnits; chunking happens insideLoraTrainingLoop.LoraCorpusLimithelper; docs/help no longer claim a silent 128 default.
Status¶
Session 44: LoRA training progress bar (loss → target).
LoraTrainProgressBar: percent from pass-2 baseline loss toward--lora-loss-target-*; max-iters not used.ETA from loss-improvement rate since baseline; final frame ETA
0swhen the run ends.
Status¶
Session 43: LoRA Tier 6: multi-architecture training (CPU oracle).
LoRA multi-architecture (Tier 6)¶
LoraTrainingHandler/LoraTrainingHandlerFactory: explicit allowlist bygeneral.architecture.LoraModelLayout/LoraProjectionBinding: logical keys → physical GGUF tensors (Phi fused slices).Handlers: LLaMA-family (
LoraTrainableHandler),Qwen2LoraTrainableHandler(frozen QKV biases),Phi3LoraTrainableHandler(fused QKV/gate-up + NeoX RoPE),Qwen3LoraTrainableHandler(per-head Q/K RMSNorm,qDim).LoraMergelayout-aware multi-adapter fused-slice F32 patching for Phi-3.Rejected for LoRA:
qwen3moe,qwen35,gemma, unknown.Qwen3
/train-qatemplate parity with empty<think>block.
Status¶
Session 42: LoRA REPL UX + WebUI model dropdown.
/resetdeletes the.loracheckpoint (no overwrite save); memory reset + chat history clear unchanged.LoRA banner and chat footer show sampling
temperature(and top-k / top-p on the banner).Default LoRA training log is a compact progress bar;
--verbose/-vrestores full[TRACE]/ per-pass lines.WebUI model dropdown parses OpenAI
GET /v1/models(data/id/x_juno_*) so names appear again.
Status¶
Session 41: LoRA Tier 7 (complete): JFR metrics for all adapter modes and operations.
LoRA JFR metrics (Tier 7)¶
Programmatic LoRA
--jfrlifecycle matches local mode (jdk.jfr.Recording+ auto-extracttarget/metrics/metrics.jsonon exit). Launchers pass--jfras an app arg (no-XX:StartFlightRecordingfor LoRA).LoraMetricsIdentity: CLI vocabulary tags (lora/rslora/dora/qa-lora) on train, validation, merge, norm-refresh, playback, and checkpoint events.New events:
juno.LoraNormRefresh,juno.LoraMerge,juno.LoraPlayback,juno.LoraCheckpoint.JfrMetricsExtractoraggregates train/validation/merge/DoRA-refresh/playback series with guarded field reads (older recordings still extract).
Status¶
Session 40: LoRA Tier 5 (complete implementation): QA-LoRA + merge policies.
LoRA QA-LoRA and quantized merge (Tier 5)¶
Gate A codecs retained:
QuantizationLayout,GgufQuantCodec/GgufKQuantCodec(juno-kquant-v1),QuantizedMergeMetrics.QaLoraAdapter: sum-pool grouped A (rank×groupCount) + B; dense-expansion oracle and finite-difference tests.AdapterAlgorithm,MergeCapability(SIDECAR_ONLY/F32_PRESERVE/SOURCE_TYPE_PROJECTED;EXACT_AFFINErejected for K-quants).Checkpoint v2: QA entries store
groupWidthbefore A, Tier-5 extension blob (algorithm, pooling, ggml type, encoder id, merge policy); v1 export rejected for QA-LoRA.QaLoraInitializer: group width from actual tensor GGML type (Q4_K/Q5_K→32, Q6_K→16); fingerprints verified on load.Training/playback:
LoraTrainableHandler, Adam, gradients, CLI--lora-mode qa-lora,--lora-group-width,--lora-merge.LoraMerge: F32 preserve (default) and explicitSOURCE_TYPE_PROJECTEDrequantization with per-tensor metrics; zero-delta copies raw bytes; never silent exact→projected fallback.Exact K-quant QA-LoRA zero-point merge remains no-go. Full held-out experiment matrix / deployment quality gates are research follow-ups; sidecar + F32 stay production-safe.
Status¶
Session 39: LoRA Tier 5 (Gate A start): shared GGUF K-quant codec layer.
LoRA QA-LoRA / quantized merge foundations (Tier 5 Gate A)¶
QuantizationLayout: Q4_K / Q5_K / Q6_K geometry (block/sub-block width, affine vs symmetric).GgufKQuantCodec/GgufQuantCodec: versioned encoder idjuno-kquant-v1; decode matches llama.cpp goldens; encode moved out ofLoraMerge.QuantizedMergeMetrics: RMSE, max error, delta-retention helpers for projected merge.GgufReaderandLlamaTransformerHandler.dequantizedelegate K-quant decode to the shared codec; fused matVec paths unchanged for performance.No-op path:
copyRawUnchanged: decode/re-encode must not be used for byte-identical preservation.Non-closure tests: Q6_K additive shift and Q4_K nested-scale offset are not exact (exact K-merge remains no-go).
Next: grouped QA-LoRA math (Gate B), merge capability policy, then projected merge experiments.
Status¶
Session 38: LoRA Tier 4 (start): resident transpose primitives and baseline instrumentation.
LoRA GPU training foundations (Tier 4)¶
Vendor-neutral
GpuBindings.opNoTranspose()(CUDACUBLAS_OP_N=0, ROCmrocblas_operation_none=111).GpuMatVec.sgemvTransposefor resident FP32/FP16W^T * g(same row-major buffer as forwardOP_T).ResidentWeightMatrix+LoraTrainableHandlerroutes frozen forward and transpose backward through resident GPU weights when uploaded (supportsHalfResidentFP16 or FP32 fallback).JFR backend labels:
*-resident-transpose/*-resident-fp16-transpose.LoraTrainEventfields for frozen forward/transpose, attention/nonlinear, adapter backward, and transfer (filled when finer instrumentation lands).GPU adjoint tests:
CudaMatVecTransposeTest,RocmMatVecTransposeTest(GpuMatVecTransposeContractTest).Baseline section in
docs/performance.md: hybrid path is not yet marketed as production GPU training.--lora-train-deviceshipped in Session 46; CPU/GPU gradient parity IT and speed gates remain open.Fix:
LoraAdapterSet.resetFrom(REPL/reset) bumps DoRA cache generation so inference drops trained magnitude coefficients.Fix:
/resetalso clears REPL chat history and rotates the session id: otherwise multi-turn context still contains the memorized answers.
Status¶
Session 37: LoRA Tier 3 (phase 1–2): rsLoRA, Kaiming, checkpoint v2, DoRA.
LoRA advanced adapters (Tier 3)¶
Explicit adapter metadata:
LoraAdapterConfigwithLoraScaling,LoraInitialization,LoraMode.rsLoRA scale
alpha/√rank; PEFT-compatible Kaiming-uniform A init (legacy-normal retained for compatibility overloads).Checkpoint version 2 (length-delimited) with declared alpha, scaling, init, mode, optional DoRA magnitude and base-tensor fingerprints; v1 still loads.
Canonical detached-norm DoRA (
DoraMagnitude,DoraProjection); magnitude is an AdamW parameter group with decay off.DoraInitializerbuilds magnitudes/fingerprints from GGUF dequant; merge applies LoRA/rsLoRA/DoRA formulas to F32.CLI/env:
--lora-mode,--lora-scaling,--lora-init(LORA_MODE,LORA_SCALING,LORA_INIT).DoRA norm-refresh is correctness-complete but not production-perf-gated; prefer standard LoRA/rsLoRA for large all-linear jobs until a measured refresh budget exists (Tier 10).
Status¶
Session 36: LoRA Tier 2: schedules, AdamW, dropout, validation, and LoRA+.
LoRA training quality (Tier 2)¶
Warmup/cosine and constant learning-rate schedules (
--lora-lr-schedule,--lora-warmup-steps,--lora-min-lr).True A-only decoupled AdamW (
--lora-weight-decay); moments see raw gradients only. Numerical trajectories change vs coupled L2; checkpoints remain compatible.LoRA+ parameter groups: A uses scheduled LR, B uses
LR * --lora-plus-ratio(default1.0= ordinary behavior).Deterministic train-only inverted dropout (
--lora-dropout,--lora-seed); inference and validation stay dropout-free.Forward-only
evaluateLoss; held-out validation split with patience/min-delta and best-weight restore (--lora-validation-*).Shared
LoraTrainingLooporchestration for REPL andLoraTrainer; Q&A variants are hold-out units.JFR
LoraTrainStepcarries A/B LR, LoRA+ ratio, and dropout; optionalLoraValidationevent.
Status¶
Session 35: LoRA Tier 1: projection coverage, token-weighted accumulation, and clipping.
LoRA correctness foundation (Tier 1)¶
Configurable projection targets:
qv(default),all/all-linear, or comma-separated keys (wq,wk,wv,wo,wgate,wup,wdown).Complete forward/backward for all seven dense linear projections, including current-position K and inverse-RoPE on Q and K.
computeGradientsseparated from optimizer updates; token-weighted gradient accumulation across chunks.Global L2 gradient clipping after prediction-count normalization (
--lora-max-grad-norm;0disables clip).Builder-based
LoraTrainingConfigandLoraTrainer.open(..., config); legacy overload keeps qv, accum=1, clipping off.Architecture gate: Phi-3 / Qwen3 / Qwen3-MoE rejected for LoRA (dense LLaMA-family required).
/resetreinitialises A and B from the selected target config (not B-only zeroing).Merge maps all seven projections via
LoraProjection; adapted tensors remain F32.Terminology: LoRA on a quantized GGUF base (not QLoRA).
Status¶
Session 34: Windows launcher fixed: run.bat and juno.bat fully functional on Windows.
Windows launcher (scripts/run.bat, juno.bat)¶
All subcommands (cluster, local, lora, merge, test) and flags are now working on Windows.
Root cause fixes:
JAR name mismatch.
run.batreferencedjuno-player.jarandjuno-master.jar: names that Maven never produces. The actual artifacts arejuno-player-<version>-shaded.jarandjuno-master-<version>.jar. Fixed by reading the project version frompom.xmlat startup usingfindstrand constructing the correct paths dynamically.Java version detection hang. CMD cannot redirect
stderrin a pipeline (2>&1) reliably inside afor /floop when delayed expansion is active.java -versionwrites to stderr and the output was silently lost, leavingJAVAVER_RAWundefined. Fixed by capturingjava -version 2> tmpfileto a temp file and reading the file withfor /f.find_javanested-if failure. Nestedif ... (if ... (...))blocks are not reliable in CMD withsetlocal enabledelayedexpansion. Replaced with a flat goto-based structure (find_java_wherelabel).Infinite loop on empty argument. In argument-parsing loops,
if exist "%~1"on an empty%~1expands toif exist ""which matches the current directory (always true), causing an infinite loop. Fixed by guarding withif not "%~1"==""before theif existcheck in thecluster,local,lora, andtestparsers.JFR block inside
if not ... (for ...)silently skipped. CMD does not support aforcommand inside anifparenthesized block when delayed expansion is on. Replaced with a goto-based pattern (lora_jfr_skip/test_jfr_skiplabels).
Documentation updated:
README.md: Windows launcher note in section 2.2, Windows requirements paragraph,juno.batreferences formerge.docs/howto.md: Windows note at top; Windows command-prompt examples added to every subcommand section (local,cluster,lora,merge) and Build and Test.
Status¶
Session 33: Model support documentation: Phi-3 supported; Gemma, Qwen 2 / Qwen3 / Qwen3.5 under development.
Supported model status (docs)¶
User-facing docs now state a single, consistent model-support policy:
| Family | general.architecture | Status |
|---|---|---|
| LLaMA, Mistral, TinyLlama, … | llama, mistral, … | Supported via LlamaTransformerHandler |
| Phi-3 / Phi-3.5 | phi3 | Supported via Phi3TransformerHandler |
| Gemma | gemma | Under development (LlamaTransformerHandler + gemma template) |
| Qwen 2 / 2.5 | qwen2 | Under development (Llama handler + QKV bias groundwork) |
| Qwen3 dense | qwen3 | Under development (Qwen3TransformerHandler in progress) |
| Qwen3-MoE | qwen3moe | Under development (Qwen3MoeTransformerHandler in progress) |
| Qwen3.5 | qwen35 | Under development (hybrid DeltaNet; separate handler) |
Updated files:
README.md,RELEASE_NOTES.md: Supported models sectiondocs/arch.md: handler routing and tokenizer notesdocs/features.md,docs/howto.md,docs/LoRA.md: Phi-3 OK for inference; Gemma and Qwen paths not production-ready; LoRA still LLaMA-family (+ Phi-3 template detection)docs/phi3-inference-handoff.md: status set to supported (retains debug handoff notes)docs/model_support_summary_972ab30f.plan.md: roadmap, dispatch table, chat matrix, gaps, decisions log
Policy: Phi-3 is production-ready in docs and validation (local + cluster). Gemma and all Qwen families remain under development until dedicated validation lands.
Status¶
Session 32: ROCm/HIP backend for AMD GPU inference via Panama FFI.
AMD GPU support (ROCm/HIP + rocBLAS)¶
Full first-class AMD GPU support alongside the existing NVIDIA CUDA backend. The GPU abstraction layer auto-selects CUDA > ROCm > CPU at startup with no configuration required. Tested on AMD Radeon RX 7900 XT (gfx1100, ROCm 7.2.x).
New production classes (node module):
GpuBindings: vendor-neutral interface implemented byCudaBindingsandRocmBindings. Exposes all device runtime and BLAS handles asMethodHandleaccessors, shared constants (H2D,D2H,STREAM_NON_BLOCKING), and static helpers (check,callInt,loadLibrary,bind). Static helpers eliminate per-implementation boilerplate.GpuMatVec: sealed interface (permits CudaMatVec, RocmMatVec) extendingMatVec. Exposesupload(float[], int, int)anduploadHalf(float[], int, int)so transformer handlers depend on the GPU abstraction rather than a concrete vendor class.RocmBindings: Panama FFI downcall handles forlibamdhip64.soandlibrocblas.so. Pre-bindshipHostMalloc flags=0viaMethodHandles.insertArgumentsto match thecudaMallocHostarity visible to all callers. Key ROCm constants:opTranspose()=112(rocblas_operation_transpose),hipDeviceProp_tsizeof=1472, name@0, totalGlobalMem@288 (measured from ROCm 7.2.x headers, Linux x86_64).RocmAvailability: HIP device detection:isAvailable(),deviceCount(),deviceName(int),vramBytes(int). MirrorsCudaAvailabilityin structure.RocmMatVec:MatVec/GpuMatVecimplementation backed byrocblas_sgemv(FP32) androcblas_hssgemv_strided_batched(FP16). Three compute paths:Host FP32: temporary device buffers per call; synchronous H2D → kernel → D2H.
Device-resident FP32 (
DeviceFloatMatrix): per-thread scratch for x/y; async stream copies.Device-resident FP16 (
DeviceHalfMatrix): x converted FP16 in off-heap arena; FP32 accumulation. Off-heapArena.ofConfined()staging for all H2D/D2H copies: required by Java 25 Panama (heap segments rejected by native downcalls).
MatVecBackend: enum replacing ad-hoc string literals for thejuno.MatVec.backendJFR dimension. Values:CPU,CUDA,CUDA_RESIDENT,CUDA_RESIDENT_FP16,ROCM,ROCM_RESIDENT,ROCM_RESIDENT_FP16. Label strings are part of the JFR contract and unchanged.
Modified production classes:
GpuContext: refactored from CUDA-only to backend-agnostic. AddsGpuBindings bindingsfield,bindings()accessor,selectBindings()(CUDA → ROCm priority order with-Djuno.gpu.backend=cuda|rocm|autooverride),createMatVec()factory,backendLabel()delegate.close()usesbindings.cublasDestroy()instead of hardcoded CUDA call. PrivatedeviceName()anddeviceVram()helpers useGpuBindingsstruct-offset accessors.CudaBindings: addsimplements GpuBindings; 20 accessor methods expose the existingMethodHandlefields to vendor-neutral callers. Zero existing fields or constants removed.CudaAvailability: field-access calls updated to useCudaBindings.instance()accessor methods (PROP_NAME_OFFSET→instance().PROP_NAME_OFFSET, etc.).CudaMatVec: implementsGpuMatVec(wasMatVec);upload/uploadHalfmade public with@Override; backend labels replaced byMatVecBackendenum calls.DeviceFloatMatrix/DeviceHalfMatrix: directCudaBindings.instance()field access replaced byGpuContext#bindings()method calls (GpuBindings). Both classes now work identically on CUDA and ROCm.DeviceHalfMatrixcachesgpu = ctx.bindings()at construction.LlamaTransformerHandler:instanceof CudaMatVec→instanceof GpuMatVecfor weight upload gate;cudaMallocOOM message check extended to also catchhipMalloc;matVecQuantBackendLabel(int)→matVecQuantBackend(int)returnsMatVecBackend.CPU.Phi3TransformerHandler: sameinstanceoffix; OOM check extended tohipMalloc.LoraTrainableHandler: sameinstanceoffix.ForwardPassHandlerLoader:pickMatVecchecks bothCudaAvailabilityandRocmAvailability; device count query reads from the available backend;GpuContext.shared(dev).createMatVec()replacesnew CudaMatVec(...).EmbeddedNodeServer: usesgpuContext.createMatVec()andgpuContext.backendLabel()for log messages.ConsoleMain/JunoPlayer:new CudaMatVec(gpuCtx)→gpuCtx.createMatVec().MatVecEvent: addsbackend(MatVecBackend)setter to avoid hand-written label strings at call sites; publicString backendfield kept for JFR contract.
New tests (55 total, 0 failures on RX 7900 XT):
RocmMatVecTest(30): extendsMatVecBackendContractTestfor full API parity; correctness vs CPU reference at 2048×2048, 5632×2048, 32000×2048; trivial known-value cases; 4-thread concurrent safety; throughput sanity.RocmAvailabilityTest(8): device detection present/absent; name format; VRAM bounds; out-of-range index fallbacks.GpuContextTest+5@Tag(rocm): ROCm context lifecycle, backend priority,createMatVecfactory, shared singleton, system-property override.ForwardPassHandlerLoaderSelectBackendTest+2@Tag(rocm):RocmMatVecrouting, process-wideGpuContext.shared(0)reuse.ForwardPassHandlerLoaderSelectLoraBackendTest+1@Tag(rocm): LoRA routing on ROCm.MatVecQuantizedBackendLabelTest: updated to useMatVecBackendenum constants.
Run ROCm-tagged tests:
mvn test -pl node -Dgroups=rocmPerformance (RX 7900 XT, ROCm 7.2.x):
| Shape | Path | Time (5 runs) |
|---|---|---|
| 32000×2048 | rocblas_sgemv host FP32 | 408 ms |
All existing 194 unit tests pass unchanged.
Status¶
Session 31: Panama FFI for Juno math: JavaCPP / bytedeco removed, CUDA bindings rewritten with java.lang.foreign.
Panama FFI GPU bindings (node module)¶
The entire CUDA bridge has been rewritten using the Java 25 Panama Foreign Function & Memory API
(java.lang.foreign.Linker, SymbolLookup, MemorySegment, Arena). The org.bytedeco:cuda-platform
dependency has been removed from node/pom.xml.
New production class:
CudaBindings: Panama FFI downcall handles forlibcudart.so.12andlibcublas.so.12. Resolves all CUDA Runtime and cuBLAS symbols once at class-init time viaLinkerandSymbolLookup; resultingMethodHandleinstances are thread-safe with zero per-call Java overhead. Exposes:cudaGetDeviceCount,cudaGetDeviceProperties,cudaSetDevice,cudaMalloc,cudaFree,cudaMallocHost,cudaFreeHost,cudaMemcpy,cudaMemcpyAsync,cudaStreamCreateWithFlags,cudaStreamSynchronize,cudaStreamDestroy,cublasCreate,cublasDestroy,cublasSetStream,cublasSetPointerMode,cublasSgemv,cublasHSSgemvStridedBatched.cudaDevicePropstruct-offset constants (DEVICE_PROP_BYTES=1512,PROP_NAME_OFFSET=0,PROP_TOTAL_MEM_OFFSET=288) measured from CUDA 12.x headers on Linux x86_64. Singleton init:CudaBindings.instance()/CudaBindings.isAvailable().
Modified production classes:
CudaMatVec: all JNI / JavaCPP call sites replaced withCudaBindingsdowncall handles. Native memory managed exclusively viaMemorySegmentandArena. Device weight matrices (DeviceFloatMatrix,DeviceHalfMatrix) held resident;MemorySegmentpassed directly to cuBLAS asADDRESS: zero H2D copy per token. Per-threadFp32Scratch/Fp16Scratchscratch on device grown lazily and reused. FP16 x staging packed withFloat.floatToFloat16into a confined off-heap arena in the hot path.GpuContext: cuBLAS handle stored asMemorySegment(opaquecublasHandle_t); created and destroyed viaCudaBindings.cublasSerializationLock()serializes stream-binding and kernel submission on the shared handle.shared(int)returns a process-wide singleton per device index.DeviceFloatMatrix: device memory allocated viaCudaBindings.deviceMalloc; backingMemorySegmentsized torows * cols * 4bytes; H2D via synchronouscudaMemcpy.DeviceHalfMatrix: same pattern; FP16 x staging via confined arena;MemorySegment.ofArraypins heap array for duration of downcall.CudaAvailability: device detection updated to useCudaBindingsdowncall handles.
node/pom.xml: org.bytedeco:cuda-platform dependency removed.
maven-surefire-plugin argLine updated: --enable-native-access=ALL-UNNAMED,
--add-opens java.base/java.lang=ALL-UNNAMED, --add-opens java.base/java.nio=ALL-UNNAMED.
New test: CudaBindingsTest: two scenarios:
CUDA present (
@Tag("gpu")): everyMethodHandlenon-null, singleton loads cleanly.CUDA absent (CPU-only CI):
isAvailable()returns false,instance()throwsIllegalStateException.
Run GPU-tagged tests: mvn test -Dgroups=gpu -pl node
All existing tests pass unchanged.
Status¶
Session 30: Maven Central publish configuration.
Maven Central publish (pom.xml, all module POMs)¶
All modules configured for publishing to central.sonatype.org via the Central Portal publisher.
Version set to 0.1.0-RC across root POM and juno-bom.
Changes:
maven-source-plugin 3.3.1:attach-sourcesexecution atverifyphase; produces-sources.jarrequired by Maven Central.maven-javadoc-plugin 3.11.2:attach-javadocsexecution atverifyphase;doclint=none,failOnError=false; produces-javadoc.jarrequired by Maven Central.maven-gpg-plugin:sign-releaseexecution moved fromverifytoinstallphase so sources and Javadoc jars are already attached before signing.--pinentry-mode loopbackadded togpgArgumentsto allow-Dgpg.passphrase=...without a GUI pinentry agent.distributionManagement:<repository>and<snapshotRepository>wired tocentral.sonatype.orgCentral Portal publisher endpoint.Developer / SCM metadata:
<organization>Machine Learning Cabinet</organization>,<organizationUrl>https://ml.cab/</organizationUrl>, SCM tag updated tov0.1.0-RC.All module POMs: publish config consolidated into root POM; per-module boilerplate removed.
Status¶
Session 29: OpenAI-compatible REST API (POST /v1/chat/completions, GET /v1/models).
OpenAI-compatible API¶
Any client that speaks the OpenAI Chat Completions wire format: LangChain, LlamaIndex,
LiteLLM, the OpenAI Python/Node SDKs, or any internal tool built against openai.*: works
against Juno with a single base-URL change. No prompt reformatting, no adapter library, no
glue code.
New classes (coordinator module):
OpenAiAdapter: pure static mapping helpers between Juno internals and the OpenAI wire format:repetitionPenaltyFromFrequencyPenalty(float)(OpenAI −2..2 range → Juno ≥1),validateCompletionsN(Integer)(rejects n ≠ 1),toOpenAiFinishReason(StopReason)(stop/length/error), andchatCompletionId(String)(chatcmpl-+ compact UUID).OpenAiChatHandler: Javalin handler class owning three endpoints:POST /v1/chat/completions: deserialisesOaiChatCompletionRequest(Jackson,@JsonIgnoreProperties(ignoreUnknown = true)), validatesnandmessages, builds anInferenceRequest+SamplingParams, then dispatches to eitherscheduler.submitAndWait()(blocking, returnsChatCompletionJSON) orscheduler.submit()(streaming, writestext/event-streamchunks terminated bydata: [DONE]).GET /v1/models: filtersModelRegistrytoLOADEDstatus, wraps eachModelDescriptorin an OpenAIModelobject withx_juno_*extension fields.GET /v1/models/{modelId}: single-model lookup; 404 when absent.
Modified: InferenceApiServer: constructs OpenAiChatHandler in the constructor
(passing the latency callback so HealthReporter still records P99). Routes
POST /v1/chat/completions and GET /v1/models[/{modelId}] to the handler.
The existing POST /v1/inference and POST /v1/inference/stream endpoints are untouched.
Modified: ConsoleMain (juno-player module): --api-port N flag starts a
RequestScheduler + InferenceApiServer alongside the existing REPL in both local and
cluster modes. A virtual-thread shutdown hook calls apiServer.stop() on JVM exit.
buildLocalModelRegistry() populates a ModelRegistry from the in-process LlamaConfig so
GET /v1/models returns the loaded model immediately.
Modified: scripts/run.sh: --api-port N flag wired into both cmd_local() and
cmd_cluster(). Environment override: API_PORT.
New file: api/src/main/resources/juno-api.yaml: OpenAPI 3.0.3 spec for the public
client-facing API. Documents all request fields with their Juno internal mappings, the SSE
chunk event sequence, Juno extension fields (x_juno_priority, x_juno_session_id,
x_juno_top_k, x_juno_latency_ms, x_juno_retry_after_ms, x_juno_queue_depth), and
all error codes.
New test: OpenAiAdapterTest: unit tests for all four mapping helpers.
Field mapping summary (request):
| OpenAI field | Juno internal | Notes |
|---|---|---|
model | modelId | First loaded model if omitted |
messages[].role / .content | ChatMessage | Text only; images not supported |
temperature | SamplingParams.temperature | 0.0–2.0; default 0.7 |
top_p | SamplingParams.topP | 0.0–1.0; default 0.9 |
max_completion_tokens | SamplingParams.maxTokens | 1–32768; default 200 |
max_tokens | SamplingParams.maxTokens | Deprecated alias |
frequency_penalty | SamplingParams.repetitionPenalty | 1 + max(0, fp/2) |
stream | route selection | false → blocking JSON; true → SSE |
n | : | Only 1 is accepted; other values → 400 |
stop, presence_penalty, logit_bias, user, seed | : | Silently ignored |
x_juno_priority | RequestPriority | HIGH / NORMAL / LOW |
x_juno_session_id | InferenceRequest.sessionId | Enables KV-cache reuse across turns |
x_juno_top_k | SamplingParams.topK | 0 = disabled; default 50 |
All modules compile. All existing tests pass. OpenAiAdapterTest (4 assertions) passes.
Status¶
Session 28: Health dashboard: CPU load metric, role-conditional secondary metric, node throughput.
Health dashboard fixes¶
Fix 1: temperatureCelsius → cpuLoad.
/sys/class/thermal is unavailable on EC2 VMs; the Temperature row always showed a dash
placeholder. Replaced with process CPU utilisation read from OperatingSystemMXBean.getCpuLoad() (0.0-1.0, available on all JVM platforms, no sysfs). Changes:
NodeHealthrecord: fieldtemperatureCelsiusremoved,cpuLoadadded (same sentinel -1.0 convention, clamped to 0.0 on first-sample unavailability).HealthReporter.buildProbeJson():readTemperatureCelsius()+ all sysfs helpers (findThermalZone,findHwmonTemp, thermalPath/thermalProbed state) removed; replaced by 5-linereadCpuLoad().HealthMain.NodeHealthDto:temperatureCelsiusfield →cpuLoad.Dashboard HTML (both
HealthMainandInferenceApiServerembedded console): “Temperature” row → “CPU load” formatted asXX.X %.
Fix 2: Role-conditional secondary metric: coordinator shows Latency P99, nodes show Throughput.
Latency P99 was populated by HealthReporter.recordLatency(), which is only called from InferenceApiServer on the coordinator JVM. Worker nodes always showed a dash placeholder. Added a nodeRole field ("coordinator" | "node") to NodeHealth and NodeHealthDto so the dashboard can branch:
Coordinator card: Latency P99 (ms): end-to-end generation time, already wired via
InferenceApiServer.setLatencyReporter().Worker node cards: Throughput (MB/s): activation bytes forwarded per second via new
HealthReporter.recordBytes(long n)+drainThroughput()(atomic byte counter drained each probe interval).
Wiring:
EmbeddedNodeServer: retainedNodeServiceImplreference asserviceImplfield; addedsetHealthReporter(HealthReporter)on outer class delegating to a new package-private setter on the inner class.forwardPass()callshr.recordBytes(encodedOutput.length)after eachresponseObserver.onNext().NodeMain: constructs reporter withnodeRole="node", callsserver.setHealthReporter(reporter)afterserver.start().CoordinatorMain: constructs reporter withnodeRole="coordinator".HealthReporterconstructors: 2-arg and 3-arg remain backward-compatible (default role"node"); new canonical 4-arg constructor(nodeId, nodeRole, healthBaseUrl, intervalMs). AddedstartForCoordinator(healthBase)factory alongside existingstartForNode(nodeId, healthBase).buildNodeDetail()switched fromMap.of()(10-entry limit) toMap.ofEntries()to accommodate 12 fields.
Investigation 3: Why 1 of 10 concurrent sessions produced no tokens (no code change).
Root cause: gRPC ServerBuilder.forPort(port) with no custom executor defaults to a thread pool bounded by ~2 × CPU count (4 threads on m7i-flex.large). With 9 sessions concurrently running prefill (26 steps × 9 = up to 234 in-flight blocking stubs), all 4 gRPC threads on each node were saturated. The 10th session’s first pipeline.forward() call queued behind them for ~8.5 minutes until prefill of the other 9 finished. The fix is ServerBuilder.forPort(port).executor(Executors.newVirtualThreadPerTaskExecutor()): virtual threads don’t block OS threads on gRPC I/O. JFR evidence: juno.ForwardPass.decode.p95_ms = 3095 ms on node-1 (coordinator node running layers 0–8 plus the REST server) vs 914 ms on node-2; coordinator log confirms 10 tokenizer encodes but only 9 near-simultaneous prefills.
All modules compile. All existing tests pass (NodeHealth, HealthEvaluator, HealthReactor constructors updated to 9-arg signature).
Session 27: GPU lifecycle, multi-device shared contexts, CUDA streams, Llama VRAM fallback, docs.
ForwardPassHandler.releaseGpuResources(): default no-op;LlamaTransformerHandlerandPhi3TransformerHandlerclose allDeviceHalfMatrixbuffers.EmbeddedNodeServerinvokes it on shard reload, load failure, andunloadShard(then swaps inStubForwardPassHandler).GpuContext.shared(int): one process-wideGpuContextper CUDA device index (map + lock);close()remains a no-op for shared instances.ForwardPassHandlerLoader.selectBackend()andEmbeddedNodeServerhonour-Djuno.cuda.device=N, validated againstCudaAvailability.deviceCount().CudaMatVec: per-thread non-blocking CUDA stream;cublasSetStream_v2+cudaMemcpyAsyncfor resident FP32/FP16x/ytransfers;synchronized(gpuContext.cublasSerializationLock())around stream binding and kernels. Hostsgemv(float[],…)also runs under the same lock.Llama GPU OOM: upload wrapped like Phi-3: on
cudaMallocfailure, partialDeviceHalfMatrixbuffers are **close()**d and inference falls back to CPU quantised matmul for those projections.Docs/tests:
README.md,docs/arch.md,GpuContextTest(multi-GPU assumption),NodeMainJavadoc forjuno.cuda.device.
All modules build and all tests pass. Verified end-to-end with:
TinyLlama-1.1B-Chat-v1.0.Q4_K_M.gguf
TinyLlama-1.1B-Chat-v1.0.Q5_K_M.llamafile
TinyLlama-1.1B-Chat-v1.0.Q2_K.gguf
Meta-Llama-3.2-1B-Instruct-Q8_0.llamafile
phi-3.5-mini-instruct.Q4_K_M.gguf on a 3-node CPU cluster
Phi-3.5 GPU matmul path:
CudaMatVecBackendTestFP16 resident matvec +mvn test -Dgroups=gpu -pl nodeon CUDA 12.x
Session 26: Phi-3 GPU matmul, FP16 resident weights, CLI and local GPU wiring.
Phi3TransformerHandler GPU path uploads dequantized fused QKV / FFN slices and output projection as DeviceHalfMatrix (IEEE FP16 on device, roughly half the VRAM of DeviceFloatMatrix). Forward uses CudaMatVec.sgemv(DeviceHalfMatrix, x), implemented with cublasHSSgemvStridedBatched: same (CUBLAS_OP_T, m=cols, n=rows, lda=cols) layout contract as the proven cublasSgemv_v2 path for row-major A. Host float[] activations are converted to FP16 for the per-call device x buffer; accumulation stays FP32. Earlier cublasSgemmEx / cublasGemmEx mixed-dtype attempts returned NOT_SUPPORTED / INVALID_VALUE on common stacks; the HSS strided-batched GEMV avoids that.
Session 26: Native LoRA merge (juno merge).
LoraMerge (new, node module) writes a new GGUF file from a base model and a .lora checkpoint without re-quantising the patched tensors. The 44 LoRA-adapted projection weights (wq/wv on every layer) are stored as F32; all other tensors are copied verbatim in their original quantised encoding. F32 is required because the LoRA delta (~6×10⁻⁴) is smaller than Q4_K quantisation noise (~3×10⁻³): re-quantising would silently erase the training. Verified: merged TinyLlama recalls /train-qa facts (name “Dima”) correctly under ./juno local with no .lora sidecar.
GgufReader gains five new public methods needed by the GGUF writer: ggufFileOffset(), metadataSectionEnd(), tensorOrder(), tensorNelems(name), and keeps the existing tensorAbsoluteOffset / tensorType / tensorDims. Internal storage changed from HashMap to LinkedHashMap so tensorOrder() is stable. A List<String> tensorOrder field is added to preserve insertion order.
LoraMergeMain (juno-player module): CLI entry point for juno merge. Reads --model-path, --lora-path, --output, --heap. Derives <model>.lora and <model>-merged.gguf as defaults.
run.sh gains cmd_merge() and the merge) dispatch case.
ConsoleMain /merge-hint REPL command updated: now prints the actual ./juno merge invocation instead of the old “contributions welcome” message.
Three bugs fixed during development of LoraMerge:
Q4_K:
d = maxRange/63→d = maxRange/(63×15). Previous formula collapsed all 4-bit quant values to{0,1}.Q5_K: same bug, factor 31.
d = maxRange/63→d = maxRange/(63×31).Q3_K scRaw packing: aux0/aux1 high-nibble extraction used a broken two-pass utmp reconstruction; replaced with a clean direct inverse of
GgufReader.loadQ3_K.
Session 25: Code quality: dead code removed, test helpers moved to test scope, docs fully updated.
CyclicForwardPassHandler moved from node/src/main to node/src/test. It is a deterministic stub with no business value without a model; it belongs exclusively in the test compilation unit. EmbeddedNodeServer no longer imports it: the three call sites (pre-load placeholder, model-load-failure fallback, no-model stub mode) are now served by a new private StubForwardPassHandler inner class that returns zero-filled arrays of the correct shape with no test machinery. node/pom.xml gains a maven-jar-plugin test-jar execution so other modules can still import CyclicForwardPassHandler; coordinator/pom.xml and juno-master/pom.xml declare the node:tests classifier dependency.
VRAM / OOM: GPU buffer allocation is wrapped; on failure (including cudaMalloc OOM), partial device buffers are closed and the handler falls back to CPU quantised LlamaTransformerHandler.matVec-style matmul for those projections.
ConsoleMain: missing break after --cpu fixed: parsing no longer fell through into --lora, which incorrectly set loraMode when forcing CPU inference.
ConsoleMain.runLocalRepl: one shared GpuContext + CudaMatVec instance for every in-process shard load (avoids redundant cuBLAS contexts and matches production “one GPU per JVM” usage).
Tests: CudaMatVecBackendTest.device_half_matrix_sgemv_matches_host_path (512×512) anchors FP16 resident correctness vs LlamaTransformerHandler.matVec.
JFR: MatVecEvent.backend cuda-resident-fp16 labels the Phi FP16 device path. (As of session 27, Llama GPU resident weights also use cuda-resident-fp16; cuda-resident remains for DeviceFloatMatrix / tests.)
Session 26: LoRA inference overlay (--lora-play), Q&A training mode (/train-qa), diagnostic tracing, and AWS deploy hardening.
--lora-play PATH: apply trained adapters at inference in any mode¶
Pre-trained .lora checkpoint files can now be applied read-only at inference time without entering the lora REPL. Three modes are supported:
local mode:
./juno local --model-path model.gguf --lora-play /path/to/model.loraConsoleMain.runLocalRepl() calls LoraAdapterSet.load(Path.of(loraPlayPath)) before building the shard handlers and passes the result into ForwardPassHandlerLoader.load(..., playAdapters).
cluster mode (forked JVMs):
./juno --model-path model.gguf --lora-play /path/to/model.loraClusterHarness.withLoraPlay(path) injects -Djuno.lora.play.path=PATH into every forked node JVM command. EmbeddedNodeServer.NodeServiceImpl reads this property at construction and loads adapters inside loadShard() before the ForwardPassHandlerLoader call.
AWS deployed cluster:
./launcher.sh juno-deploy.sh setup --lora-play /absolute/path/to/model.loraSee AWS section below.
ForwardPassHandlerLoader: new LoRA overload¶
// New canonical overload: all others delegate to this
public static ForwardPassHandler load(
Path modelPath, ShardContext context, MatVec backend,
LoraAdapterSet adapters) throws IOExceptionWhen adapters != null, the loader routes to LoraTrainableHandler (inference-only, no optimizer attached) instead of the architecture-specific handler. When adapters == null the existing phi3 / llama dispatch is unchanged. selectBackend() promoted from package-private to public so juno-player-module callers can reuse it.
ClusterHarness: withLoraPlay() fluent method¶
harness.withLoraPlay("/path/to/model.lora");Stores the path and injects -Djuno.lora.play.path=PATH into the launchNode() JVM command, after the JFR flags. Without this, forked node JVMs start with loraPlayPath=null and run the base model regardless of what the coordinator is told.
/train-qa: conversational Q&A training¶
New REPL command in lora mode for training single-fact associations:
you > /train-qa What is my name? A: Dima
Question: What is my name?
Answer : Dima
Formatted as 4 Q&A pairs · model type: tinyllama
Training rank=8 · lr=1.0E-4 · 40 steps ...
✔ done loss=▼ 1.53 (−0.83)The command auto-generates 4 phrasings of the question (exact, Can you tell me: ..., Please answer: ..., plus one repeat) to improve generalization. The chat template appropriate for the model type (detected from the model path) is applied to each pair. Flags --lora-steps-qa N and --lora-early-stop F control training depth.
Separator syntax: Q: <question> A: <answer> or <question> A: <answer>.
Diagnostic tracing (--verbose)¶
All tracing is prefixed [TRACE] for easy grep. Added to:
| Location | What is shown |
|---|---|
| LoRA REPL startup | Model type (chat template key), model path, all LoRA hyperparameters |
/train-qa | Exact formatted training text with ↵ for newlines, token count, token IDs (verbose only) |
| Per training step (verbose) | step=N loss=F chunk=M/T ms=D |
| Cluster inference (verbose) | Chat template key used for each inference request |
juno-deploy.sh bootstrap | Per-node params baked into user-data script |
juno-deploy.sh SCP | Local source, remote target, per-node node.env patch |
juno-deploy.sh coordinator env | Full cluster-nodes.env contents echoed after write |
AWS deploy hardening (juno-deploy.sh)¶
Multiple bugs fixed during end-to-end AWS validation:
Double base64 encoding (cloud-init rejected user-data). --user-data was passed as a pre-base64-encoded string. AWS CLI base64-encodes it again; cloud-init received double-encoded garbage and logged Unhandled non-multipart (text/x-not-multipart) userdata. Fix: write user-data to a temp file and pass file:///tmp/juno-userdata-*.sh: the CLI reads it raw and does single encoding. The [TRACE] size line now also prints first-line: #!/bin/bash so shebang presence is visible in the setup log.
TRACE logs contaminating user-data. _build_node_userdata is called as USER_DATA=$(_build_node_userdata ...) which captures all stdout. The four log / [TRACE] calls inside the function were writing to stdout, prepending ANSI escape codes before #!/bin/bash. Cloud-init saw no shebang on line 1 and skipped execution. Fix: all log calls inside _build_node_userdata now redirect to stderr with >&2.
Relative --lora-play path not resolved. When called from scripts/aws/, a path like ../models/model.lora resolves to scripts/models/model.lora (which doesn’t exist). _scp_lora_to_nodes hit the [[ ! -f ]] guard and returned silently, leaving node.env with empty JUNO_LORA_PLAY_PATH. Fix: --lora-play is resolved to absolute path at parse time via realpath. setup() also validates the file exists before any AWS spend.
Race condition: coordinator started before node restart completed. _scp_lora_to_nodes previously used systemctl restart --no-block and polled systemctl is-active to detect readiness. The old instance remained active during shutdown so the poll returned immediately, _write_cluster_env_and_start_coordinator ran, and the coordinator sent loadShard to the old (no-LoRA) instance. The restarted instance came up 19 minutes later, too late. Fix: synchronous stop → patch → start per node: systemctl stop juno-node (synchronous, waits for JVM exit), sed patch of node.env, systemctl start juno-node (synchronous, returns once gRPC port is bound, ~2s). Coordinator only starts after all three nodes have confirmed active status with correct env.
Local relative path baked verbatim into cluster-nodes.env. Even when SCP succeeded, the coordinator received JUNO_LORA_PLAY_PATH=../models/... (the pre-realpath value), causing model load failed: ../models/... on the nodes. Fix: _scp_lora_to_nodes updates the global LORA_PLAY_PATH to the remote absolute path (/opt/juno/models/<basename>) before returning, so _write_cluster_env_and_start_coordinator writes the correct value.
_write_cluster_env_and_start_coordinator missing closing brace. The } was accidentally elided, causing scan_regions() to be parsed as part of the function body.
End-to-end verification:
you> what is my name?
bot> DimaConfirmed working on 3 × m7i-flex.large AWS cluster (eu-north-1) with TinyLlama-1.1B-Chat-v1.0.Q4_K_M and a .lora adapter trained locally, SCPed and deployed via juno-deploy.sh setup --lora-play.
Session 34: Windows launcher fixed: run.bat/juno.bat fully functional; docs updated with Windows examples. (this session)
Session 33: Model support documentation: Phi-3 supported; Gemma, Qwen 2 / Qwen3 / Qwen3.5 under development. (unchanged)
Session 24: Configurable activation byte order (--byteOrder BE|LE). (unchanged)
Session 22: Q2_K and Q3_K quantization support. (unchanged)
Session 21: Two new deployment fat-jar modules and a unified AWS script. (unchanged)
Session 20: GPU inference actually wired end-to-end. (unchanged)
Session 19: metrics module, Meta-Llama 3 tokenizer fix, AWS infrastructure scripts. (unchanged)
Session 18: GPT-2 BPE tokenizer, JFR instrumentation fixes. (unchanged)
Session 17: AWS infrastructure scripts. (unchanged)
Session 14: LoRA fine-tuning + JFR profiling. (unchanged)