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
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,43 @@ public void store(
checkVectorRange("value", value, valueOffset, valueDim);
ensureCapacity(position + 1);
int slot = slotIndex(layer, position);
System.arraycopy(key, keyOffset, keys, slot * keyDim, keyDim);
System.arraycopy(value, valueOffset, values, slot * valueDim, valueDim);
if (HALF_PRECISION) {
storeHalfPrecision(key, keyOffset, keys, slot * keyDim, keyDim);
storeHalfPrecision(value, valueOffset, values, slot * valueDim, valueDim);
} else {
System.arraycopy(key, keyOffset, keys, slot * keyDim, keyDim);
System.arraycopy(value, valueOffset, values, slot * valueDim, valueDim);
}
populated[slot] = true;
}

/**
* Whether to hold cached keys and values at half precision, off by default.
*
* <p>llama.cpp holds {@code cache_k} and {@code cache_v} as F16, so attention there reads back
* rounded keys and values. Enabling this reproduces its layer output exactly on Qwen3 and raises
* embedding agreement, but it flips a greedy token on the pinned SQLCoder Q5_K_M fixture, which
* is itself a llama.cpp equivalence reference. The two references disagree, so this cannot be the
* default until that is resolved; see the qwen3 K-quant divergence investigation.
*/
private static final boolean HALF_PRECISION =
Boolean.getBoolean("models.purejava.halfPrecisionKvCache");

/**
* Copies a vector into the cache at half precision.
*
* <p>llama.cpp holds {@code cache_k} and {@code cache_v} as F16, so attention there reads back
* rounded keys and values. Storing F32 made this runtime more precise than the reference rather
* than equivalent to it, which the embedding equivalence gate reports as a failure.
*/
private static void storeHalfPrecision(
float[] source, int sourceOffset, float[] destination, int destinationOffset, int length) {
for (int index = 0; index < length; index++) {
destination[destinationOffset + index] =
Float.float16ToFloat(Float.floatToFloat16(source[sourceOffset + index]));
}
}

/**
* Returns the concatenated key vectors for a layer from fromPos (inclusive) to toPos (exclusive).
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.within;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag;
Expand Down Expand Up @@ -275,4 +276,27 @@ void layerBeyondRangeThrows() {
.isInstanceOf(IllegalArgumentException.class);
}
}

@Test
void roundsThroughHalfPrecisionWhenTheReferenceModeIsEnabled() {
// llama.cpp holds cache_k/cache_v as F16. This runtime keeps F32 by default because enabling
// the rounding flips a greedy token on the pinned SQLCoder fixture, so only the arithmetic is
// asserted here; models.purejava.halfPrecisionKvCache selects it at runtime.
float value = 1.0f / 3.0f;
float rounded = Float.float16ToFloat(Float.floatToFloat16(value));

assertThat(rounded).isNotEqualTo(value);
assertThat(rounded).isCloseTo(value, within(1.0e-3f));
}

@Test
void keepsExactlyRepresentableValuesUnchanged() {
KvCache cache = new KvCache(1, 2, 2, 2);
float[] exact = {0.5f, -2.25f};

cache.store(0, 0, exact, exact);

assertThat(cache.keySlice(0, 0, 1)).containsExactly(exact);
assertThat(cache.valueSlice(0, 0, 1)).containsExactly(exact);
}
}
Loading