Skip to content
Open
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
62 changes: 62 additions & 0 deletions src/DotLLM.Core/Attention/IKvCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,66 @@ public interface IKvCache : IDisposable
/// </summary>
/// <param name="length">The new current length (must be &lt;= <see cref="CurrentLength"/>).</param>
void Rollback(int length);

/// <summary>
/// Attempts to reserve in-place write slots for the K and V projections at the given
/// <paramref name="positions"/>. When successful, callers can target <paramref name="kDst"/>
/// and <paramref name="vDst"/> as the K/V projection output buffers, and run the
/// post-projection in-place pipeline (AddBias, LoRA delta, QK-norm, RoPE) directly on
/// those spans — avoiding the scratch buffer and the subsequent <c>Update</c>
/// memcpy. Length advancement is deferred to <see cref="CommitSlot"/>; the caller must
/// invoke <see cref="CommitSlot"/> after writing to keep <see cref="CurrentLength"/>
/// consistent.
/// </summary>
/// <remarks>
/// <para>
/// Returns <c>false</c> when the cache cannot expose an in-place slot for the given
/// positions — most commonly because positions are non-contiguous, exceed
/// <see cref="MaxLength"/>, would span a paged-block boundary, or the underlying storage
/// is quantized / device-resident. The caller must then fall back to the existing
/// scratch + <c>Update</c> path.
/// </para>
/// <para>
/// The default implementation returns <c>false</c>, preserving backward compatibility
/// for every <see cref="IKvCache"/> implementation that has not opted in.
/// </para>
/// </remarks>
/// <param name="layerIndex">Transformer layer index.</param>
/// <param name="positions">Position indices for the new entries. Must be contiguous for
/// the slot to be reservable.</param>
/// <param name="kDst">On success, span covering the K cache slot for these positions
/// (<c>positions.Length * kvStride</c> FP32 elements). Undefined on failure.</param>
/// <param name="vDst">On success, span covering the V cache slot for these positions.
/// Undefined on failure.</param>
/// <returns><c>true</c> when a slot was reserved and <paramref name="kDst"/>/<paramref name="vDst"/>
/// are valid in-place targets; <c>false</c> otherwise.</returns>
/// <exception cref="ArgumentOutOfRangeException">Implementations that expose raw layer
/// storage throw when <paramref name="layerIndex"/> is outside the cache's layer range.
/// An out-of-range layer is a caller bug, not a "cannot reserve" condition.</exception>
bool TryReserveSlot(
int layerIndex,
ReadOnlySpan<int> positions,
out Span<float> kDst,
out Span<float> vDst)
{
kDst = default;
vDst = default;
return false;
}

/// <summary>
/// Commits a prior successful <see cref="TryReserveSlot"/> call by advancing
/// <see cref="CurrentLength"/> based on <paramref name="positions"/>. Idempotent across
/// layers within the same forward pass — the maximum-position computation matches
/// <c>Update</c>'s semantics.
/// </summary>
/// <remarks>
/// The default implementation is a no-op. Callers must only invoke this after a
/// successful <see cref="TryReserveSlot"/> on the same cache for the same positions.
/// </remarks>
/// <param name="layerIndex">Transformer layer index.</param>
/// <param name="positions">Position indices for the entries written during the slot.</param>
void CommitSlot(int layerIndex, ReadOnlySpan<int> positions)
{
}
}
65 changes: 65 additions & 0 deletions src/DotLLM.Engine/KvCache/PagedKvCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,71 @@ public void Rollback(int length)
_blockTable.SetCurrentLength(length);
}

/// <inheritdoc/>
public bool TryReserveSlot(
int layerIndex,
ReadOnlySpan<int> positions,
out Span<float> kDst,
out Span<float> vDst)
{
kDst = default;
vDst = default;

if ((uint)layerIndex >= (uint)_numLayers)
throw new ArgumentOutOfRangeException(nameof(layerIndex));

int seqLen = positions.Length;
if (seqLen == 0) return false;

int start = positions[0];

// Contiguous run required (GEMM output is contiguous).
for (int i = 1; i < seqLen; i++)
{
if (positions[i] != start + i) return false;
}

// Bounds: entire run must fit within MaxLength. Written as a subtraction so it
// cannot overflow for large _maxSeqLen (start < _maxSeqLen is checked first, so
// _maxSeqLen - start is positive). This also makes start + seqLen safe below.
if ((uint)start >= (uint)_maxSeqLen) return false;
if (seqLen > _maxSeqLen - start) return false;

// Single-block run only: the run must not cross a block boundary, otherwise the
// in-place slot wouldn't be physically contiguous. Decode (seqLen=1) always
// satisfies this; multi-token runs only when they fit inside one block.
int blockSize = _pool.BlockSize;
int offset = start % blockSize;
if (offset + seqLen > blockSize) return false;

// Ensure a block exists (with refcount-1 fast-path) for the start position.
_blockTable.EnsureCapacity(start + seqLen);
Comment on lines +214 to +215
_blockTable.EnsureWritable(start);
var (blockId, offsetInBlock) = _blockTable.Resolve(start);

int totalFloats = seqLen * _kvStride;
kDst = new Span<float>(_pool.GetKeyPtr(blockId, layerIndex) + offsetInBlock * _kvStride, totalFloats);
vDst = new Span<float>(_pool.GetValuePtr(blockId, layerIndex) + offsetInBlock * _kvStride, totalFloats);
return true;
}
Comment on lines +178 to +223

/// <inheritdoc/>
public void CommitSlot(int layerIndex, ReadOnlySpan<int> positions)
{
int seqLen = positions.Length;
if (seqLen == 0) return;

int maxPos = positions[0];
for (int i = 1; i < seqLen; i++)
{
if (positions[i] > maxPos) maxPos = positions[i];
}

int newLength = maxPos + 1;
if (newLength > _blockTable.CurrentLength)
_blockTable.Advance(newLength);
}

/// <summary>
/// Gathers block data into a contiguous staging buffer for attention kernel consumption.
/// Copies block-by-block in logical order.
Expand Down
54 changes: 54 additions & 0 deletions src/DotLLM.Engine/KvCache/SimpleKvCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,60 @@ public void Rollback(int length)
_currentLength = length;
}

/// <inheritdoc/>
public bool TryReserveSlot(
int layerIndex,
ReadOnlySpan<int> positions,
out Span<float> kDst,
out Span<float> vDst)
{
kDst = default;
vDst = default;

if ((uint)layerIndex >= (uint)_numLayers)
throw new ArgumentOutOfRangeException(nameof(layerIndex));

int seqLen = positions.Length;
if (seqLen == 0) return false;

int start = positions[0];

// Contiguous run required: the GEMM writes [seqLen, kvStride] as a single
// contiguous block, which only maps to a contiguous cache region.
for (int i = 1; i < seqLen; i++)
{
if (positions[i] != start + i) return false;
}

// Bounds: the entire run must fit within the cache. Written as a subtraction so
// it cannot overflow for large _maxSeqLen (start < _maxSeqLen is checked first,
// so _maxSeqLen - start is positive).
if ((uint)start >= (uint)_maxSeqLen) return false;
if (seqLen > _maxSeqLen - start) return false;

int totalFloats = seqLen * _kvStride;
kDst = new Span<float>((float*)_keys[layerIndex] + (long)start * _kvStride, totalFloats);
vDst = new Span<float>((float*)_values[layerIndex] + (long)start * _kvStride, totalFloats);
return true;
}

/// <inheritdoc/>
public void CommitSlot(int layerIndex, ReadOnlySpan<int> positions)
{
int seqLen = positions.Length;
if (seqLen == 0) return;

int maxPos = positions[0];
for (int i = 1; i < seqLen; i++)
{
if (positions[i] > maxPos) maxPos = positions[i];
}

int newLength = maxPos + 1;
if (newLength > _currentLength)
_currentLength = newLength;
}

/// <inheritdoc/>
public void Dispose()
{
Expand Down
Loading