refactor(ext): make the component cache the only continuation contract (#1026)

This commit is contained in:
Federico Izzo authored and GitHub committed 2026-11-05 10:11:46 +01:00
1 parent 409cb6ff0e
commit 65fe68a9f3
46 files changed
+194 -456

No files matched your search

+2
View File
@@ -72,6 +72,8 @@ Changed lines, excluding generated files: **S** ≤ 300 · **M** 300–1,000 ·
Cards 02–19 change no runtime behavior until card 19 wires them in and deletes
the legacy cache (D1).
That was the plan; [D1](d1-transition.md) records what happened instead.
## Dependency graph
```mermaid
@@ -2,6 +2,10 @@
**Type:** decision · **Blocks:** 19 · **Status:** agreed
**Outcome:** the single switch-over did not happen. Serving routed by runner
capability while DeepSeek V4 Flash and LFM3 migrated, and both caches ran side by
side until the legacy cache was deleted. The routing went with it.
## Decision
Serving never runs the old and the new cache side by side, and the new cache
+34 -11
View File
@@ -43,9 +43,33 @@ are rejected for external extensions; keep your own implementation headers
inside your repository. The include checks enforce the source API boundary.
Extension code still has arbitrary native-code privileges during build and run.
A text runner that provides continuation implements `cache::Adapter` and links
`gufo_ext_cache`, the public target for the component cache and its HIP backing.
It exists only in HIP builds, so list it under a target that requires `hip`.
Every text runner provides continuation through the component cache. It
implements `cache::Adapter` and links `gufo_ext_cache`, the public target for the
component cache and its HIP backing. That target exists only in HIP builds, so
list it under a target that requires `hip`.
## Provide continuation
The cache owns every execution slot and decides what to retain, evict and write
to disk. A runner describes its state and moves bytes on request:
- `CreateComponentCacheResources` returns the adapter and its backing. Build the
backing with `hip::CreateHipComponentCacheResources`, and charge memory to the
ledger it receives.
- `BindComponentState` wraps one slot in the runner's `TextRunnerState`.
- `ReconcileComponentState` runs after the cache restores rows into a slot, and
brings the runner's own bookkeeping to that position.
- `cache::Adapter` declares each component as append rows or whole private state
and copies it in and out. [adapter.hpp](../src/cache/adapter.hpp) states the
ordering and failure rules. Report positions per component, never from prompt
length.
`gufo/ext/transfer_pool.hpp` and `gufo/ext/snapshot_copy.hpp` are the supported
helpers for those copies. Set `prefix_reuse` to false only for a runner which must
start each request cold. The cache identity must change whenever retained bytes
stop being valid: include the model artifact, context size and every setting that
alters state. An adapter test should restore each component and compare the
result byte for byte with an uninterrupted run.
## Start with the template
@@ -149,14 +173,13 @@ matches must remain errors; do not steal another provider's artifacts with an
overly broad probe.
For a text model, own tokenization, chat/control framing and tool syntax in the
extension. Declare supported inputs, state/persistence/resource capabilities and
sampling defaults accurately. Implement incremental execution, cancellation,
state isolation, snapshot/restore and batching only under the contracts you
advertise. To capture prefill checkpoints without ending a pass at them, declare
`in_pass_checkpoint` and `max_in_pass_checkpoints`, implement
`PrefillCheckpointBytes` and override the boundaries form of `PrefillThrough`;
each capture must equal the state a pass ending at its boundary leaves. The
single-boundary form is deprecated and captures one checkpoint per call.
extension. Declare supported inputs, state/resource capabilities and sampling
defaults accurately. Implement incremental execution, cancellation, state
isolation and batching only under the contracts you advertise. To capture prefill
checkpoints without ending a pass at them, declare `in_pass_checkpoint` and
`max_in_pass_checkpoints`, and implement `PrefillCheckpointBytes` and
`PrefillThrough`; each capture must equal the state a pass ending at its boundary
leaves.
Preserve sampler/constraint state and cache compatibility identities
across rollback and continuation. Draft models/heads and sidecars stay with the
target model's artifact set. Wire the CLI prompt/chat/benchmark drivers as well
+1 -1
View File
@@ -2,7 +2,7 @@
"schema": 1,
"name": "template_model",
"version": "0.1.0",
"api_version": ">= 0.1.10",
"api_version": ">= 0.2.0",
"entry": "template_model_register",
"registration_target": "gufo_template_model",
"depends": [],
+2 -2
View File
@@ -21,9 +21,7 @@ include/gufo/ext/experimental/hip_utils.hpp
include/gufo/ext/experimental/hipblaslt_plan_database.hpp
include/gufo/ext/experimental/host_allocations.hpp
include/gufo/ext/experimental/managed_alloc.hpp
include/gufo/ext/experimental/snapshot_copy.hpp
include/gufo/ext/experimental/snapshot_transfer.hpp
include/gufo/ext/experimental/transfer_pool.hpp
include/gufo/ext/experimental/weight_upload.hpp
include/gufo/ext/gguf_identity.hpp
include/gufo/ext/gguf_reader.hpp
@@ -49,9 +47,11 @@ include/gufo/ext/registry.hpp
include/gufo/ext/sampling.hpp
include/gufo/ext/session_mode.hpp
include/gufo/ext/sha256.hpp
include/gufo/ext/snapshot_copy.hpp
include/gufo/ext/speculative_verifier.hpp
include/gufo/ext/text.hpp
include/gufo/ext/text_contracts.hpp
include/gufo/ext/transfer_pool.hpp
include/gufo/ext/tts.hpp
include/gufo/ext/types.hpp
include/gufo/ext/utf8.hpp
+1 -1
View File
@@ -1,5 +1,5 @@
{
"major": 0,
"minor": 1,
"minor": 2,
"revision_base": 0
}
+25
View File
@@ -0,0 +1,25 @@
# Component cache is the only continuation contract
Breaking: series 0.2 starts here, and an extension must request `>= 0.2.0`. The
legacy continuation cache is gone, so every text runner provides continuation
through the component cache.
Removed from `TextModelRunner`: `CreateState`, `UsesComponentCache`,
`SnapshotPayloadBytes`, `Snapshot`, `SnapshotForPersistence`, `RestoreOrFork`,
`PersistentSnapshotPayloadBytes`, `SerializePersistentSnapshot`,
`StreamPersistentSnapshot`, `RestorePersistentSnapshot`, `SnapshotSink` and the
single-boundary `PrefillThrough`.
`CreateComponentCacheResources`, `BindComponentState` and
`ReconcileComponentState` are now pure virtual. `InPassConsumer` takes only the
boundary index, because a capture always goes through the lease.
Also removed: `TextRunnerSnapshot`, `ContinuationSnapshot`,
`TextRunnerPersistenceDescriptor`, `TextRunnerDescriptor::persistence`,
`TextRunnerCapabilities::snapshot` and `fork`,
`TextRunnerRamCacheOptions::kMaxEntries`, and the
`DiskStartupStats::legacy_probe_bytes_read` and `removed_legacy_bytes` counters.
`prefix_reuse` alone now says whether a runner retains continuations.
`transfer_pool.hpp` and `snapshot_copy.hpp` move from `gufo/ext/experimental/`
to `gufo/ext/`: they are the supported helpers for a continuation adapter.
+2 -2
View File
@@ -87,9 +87,9 @@ private:
std::uint64_t epoch_{};
};
struct DiskStartupStats {
std::uint64_t manifest_bytes_read{}, legacy_probe_bytes_read{};
std::uint64_t manifest_bytes_read{};
std::uint64_t dependency_stats{}, rejected_manifests{};
std::uint64_t managed_bytes{}, removed_legacy_bytes{};
std::uint64_t managed_bytes{};
};
// Host-buffer publication only; caller retains and admits immutable buffers
// until Publish returns. Full chunks already referenced by a checkpoint can be
+4 -4
View File
@@ -60,7 +60,7 @@ struct DurableCapture {
cache::DiskManifest manifest;
std::vector<cache::PersistenceSource> sources;
};
class ComponentSnapshot final : public TextRunnerSnapshot {
class ComponentSnapshot final : public ContinuationSnapshot {
public:
std::shared_ptr<const cache::Checkpoint> checkpoint;
std::shared_ptr<DurableCapture> durable;
@@ -730,10 +730,10 @@ struct ComponentTextCache::Lease::Impl {
.reused = cached != 0,
.reused_tokens = cached};
}
std::unique_ptr<TextRunnerSnapshot> CaptureDurable();
std::unique_ptr<ContinuationSnapshot> CaptureDurable();
};
std::unique_ptr<TextRunnerSnapshot>
std::unique_ptr<ContinuationSnapshot>
ComponentTextCache::Lease::Impl::CaptureDurable() {
if (!persist || !cache->queue || !cache->resources->copy_observed_rows ||
purpose == SnapshotPurpose::kHistory)
@@ -1512,7 +1512,7 @@ std::size_t ComponentTextCache::Lease::CaptureBytes(
impl_->cache->resources->adapter->PrefillCheckpointPositions(
impl_->slot.Execution(), boundary));
}
std::unique_ptr<TextRunnerSnapshot> ComponentTextCache::Lease::Capture() {
std::unique_ptr<ContinuationSnapshot> ComponentTextCache::Lease::Capture() {
auto& request = *impl_;
auto& cache = *request.cache;
auto positions =
+1 -2
View File
@@ -17,7 +17,6 @@ namespace gufo::server {
using ext::TextModelRunner;
using ext::TextPromptContext;
using ext::TextRunnerDiskCacheOptions;
using ext::TextRunnerSnapshot;
using ext::TextRunnerState;
// The capture/backing contracts are cache-owned (RFC step 6); these aliases
@@ -58,7 +57,7 @@ public:
void Invalidate() noexcept;
[[nodiscard]] std::size_t CaptureBytes() const;
[[nodiscard]] std::size_t CaptureBytes(std::size_t boundary) const;
[[nodiscard]] std::unique_ptr<TextRunnerSnapshot> Capture();
[[nodiscard]] std::unique_ptr<ContinuationSnapshot> Capture();
void PreparePrefill(std::size_t tokens);
[[nodiscard]] ComponentCacheMetrics Metrics() const;
[[nodiscard]] std::vector<std::size_t> PlannedCaptures(
+14 -1
View File
@@ -12,11 +12,24 @@ namespace gufo::server {
// The continuation bases and token id are runtime contracts (RFC step 6).
// These aliases keep the serving spelling.
using ext::ContinuationInputPrefix;
using ext::ContinuationSnapshot;
using ext::ContinuationState;
using ext::ContinuationToken;
using ext::PrefixInputIdentity;
/// Immutable continuation payload retained by the cache.
class ContinuationSnapshot {
public:
ContinuationSnapshot() = default;
virtual ~ContinuationSnapshot() = default;
ContinuationSnapshot(const ContinuationSnapshot&) = delete;
ContinuationSnapshot& operator=(const ContinuationSnapshot&) = delete;
ContinuationSnapshot(ContinuationSnapshot&&) = delete;
ContinuationSnapshot& operator=(ContinuationSnapshot&&) = delete;
[[nodiscard]] virtual std::size_t PayloadBytes() const noexcept = 0;
};
enum class SnapshotEventReason : std::uint8_t {
kByteCapacity,
kCaptureFailure,
+39 -82
View File
@@ -131,24 +131,6 @@ ValidatedRunner ValidateRunner(std::shared_ptr<TextModelRunner> runner,
throw std::invalid_argument(
"text runner maximum context must be at least one token");
}
if (descriptor.capabilities.fork && !descriptor.capabilities.snapshot) {
throw std::invalid_argument(
"text runner fork capability requires snapshot support");
}
if (descriptor.persistence.has_value()) {
if (!descriptor.capabilities.snapshot || !descriptor.capabilities.fork) {
throw std::invalid_argument(
"text runner persistence requires snapshot and fork support");
}
if (descriptor.persistence->compatibility_identity.empty()) {
throw std::invalid_argument(
"text runner persistence identity must not be empty");
}
if (descriptor.persistence->payload_version == 0) {
throw std::invalid_argument(
"text runner persistence payload version must be nonzero");
}
}
auto resources = runner->ResourceClaim();
const auto per_state_reservation = PerStateReservationBytes(resources);
@@ -231,9 +213,6 @@ struct TextRunnerPool::Impl {
std::optional<TextRunnerDiskCacheOptions> disk_cache_options,
TextRunnerRamCacheOptions ram_cache_options)
: validated(ValidateRunner(std::move(model_runner), state_count)) {
if (!validated.runner->UsesComponentCache())
throw std::invalid_argument(
"text runner does not provide a component cache adapter");
{
const auto resources = validated.runner->ResourceClaim();
const auto automatic =
@@ -450,8 +429,7 @@ struct TextRunnerPool::Request::Impl {
} catch (...) {
return;
}
if (!capabilities.prefix_reuse || !capabilities.snapshot ||
!capabilities.fork) {
if (!capabilities.prefix_reuse) {
return;
}
if (lease->cache_hit() && !lease->restored_from_disk() &&
@@ -566,7 +544,7 @@ struct TextRunnerPool::Request::Impl {
snapshot_future =
std::async(std::launch::async, [this, position, purpose] {
CaptureBoundarySnapshotNow(position, purpose);
return std::unique_ptr<TextRunnerSnapshot>{};
return std::unique_ptr<ContinuationSnapshot>{};
});
boundary_capture = true;
} catch (...) {
@@ -582,8 +560,7 @@ struct TextRunnerPool::Request::Impl {
bool reserved = false;
try {
const auto capabilities = runner->Descriptor().capabilities;
if (!capabilities.prefix_reuse || !capabilities.snapshot ||
!capabilities.fork)
if (!capabilities.prefix_reuse)
return;
const auto& state = dynamic_cast<const TextRunnerState&>(lease->state());
if (runner->CheckpointPosition(state) != position)
@@ -600,7 +577,7 @@ struct TextRunnerPool::Request::Impl {
reserved = lease->TryReserveSnapshot(bytes, position, true, purpose);
if (!reserved)
return;
std::shared_ptr<const TextRunnerSnapshot> snapshot = lease->Capture();
std::shared_ptr<const ContinuationSnapshot> snapshot = lease->Capture();
if (snapshot) {
snapshot_metrics.snapshot_bytes += lease->PublishSnapshot(
{prefix.begin(), prefix.end()}, snapshot, true);
@@ -637,7 +614,7 @@ struct TextRunnerPool::Request::Impl {
: SnapshotPurpose::kHistory);
if (!reserved)
return;
std::shared_ptr<const TextRunnerSnapshot> snapshot = lease->Capture();
std::shared_ptr<const ContinuationSnapshot> snapshot = lease->Capture();
if (snapshot) {
snapshot_metrics.snapshot_bytes += lease->PublishSnapshot(
{prefix.begin(), prefix.end()}, snapshot, true);
@@ -690,12 +667,12 @@ struct TextRunnerPool::Request::Impl {
: std::span<const std::uint8_t>{};
}
bool prompt_snapshot_attempted{false};
std::shared_ptr<const TextRunnerSnapshot> prompt_snapshot;
std::shared_ptr<const ContinuationSnapshot> prompt_snapshot;
TextRunnerPool::Request::CommitMetrics snapshot_metrics;
std::size_t snapshot_bytes{0};
bool retain_snapshot{false};
std::chrono::steady_clock::time_point snapshot_start;
std::future<std::unique_ptr<TextRunnerSnapshot>> snapshot_future;
std::future<std::unique_ptr<ContinuationSnapshot>> snapshot_future;
};
TextRunnerPool::Request::Request() = default;
@@ -814,8 +791,7 @@ std::size_t TextRunnerPool::Request::ShareCheckpoint(
if (!*this || impl_->decode_ready || impl_->stopped)
return 0;
const auto capabilities = impl_->runner->Descriptor().capabilities;
if (!capabilities.incremental_prefill || !capabilities.prefix_reuse ||
!capabilities.snapshot || !capabilities.fork)
if (!capabilities.incremental_prefill || !capabilities.prefix_reuse)
return 0;
// The complete prompt is retained only after decoding starts; share the
// last prefill position instead so peers can start sooner.
@@ -1018,7 +994,7 @@ TextPrefillStep TextRunnerPool::Request::Prefill(std::size_t max_input_tokens) {
struct PromptCapture {
std::size_t position;
std::size_t bytes;
std::unique_ptr<TextRunnerSnapshot> snapshot;
std::unique_ptr<ContinuationSnapshot> snapshot;
/// Its reservation keeps the request's fallback.
bool preserve{false};
/// The cache refused the reservation; nothing was captured.
@@ -1027,35 +1003,30 @@ TextPrefillStep TextRunnerPool::Request::Prefill(std::size_t max_input_tokens) {
std::vector<PromptCapture> prompt_captures;
// In-pass checkpoint `index`, read while the runner exposes it. Optional
// retention may refuse it; the live prefill stays valid either way.
const auto consume = [&](std::size_t index,
std::unique_ptr<TextRunnerSnapshot> native) {
const auto consume = [&](std::size_t index) {
const auto& capture = in_pass.at(index);
if (!capture.prompt) {
if (!native)
impl_->CaptureInPassCheckpoint(capture.position);
impl_->CaptureInPassCheckpoint(capture.position);
return;
}
PromptCapture taken{capture.position, impl_->snapshot_bytes,
std::move(native)};
PromptCapture taken{capture.position, impl_->snapshot_bytes, {}};
// Only the first target keeps the request's fallback.
taken.preserve = prompt_captures.empty() && impl_->retain_fallback &&
impl_->fallback_position == 0;
if (!taken.snapshot) {
try {
// A checkpoint captured before it in this pass took the lease's
// reservation and shares rows with it; reserve what this capture
// adds now.
taken.bytes = impl_->lease->CaptureBytes();
const auto tokens = std::span<const TextRunnerToken>(impl_->prompt)
.first(capture.position);
taken.refused = !impl_->lease->TryReserveSnapshot(
taken.bytes, capture.position, taken.preserve,
SnapshotPurpose::kContinuation, tokens);
if (!taken.refused)
taken.snapshot = impl_->lease->Capture();
} catch (...) {
taken.snapshot.reset();
}
try {
// A checkpoint captured before it in this pass took the lease's
// reservation and shares rows with it; reserve what this capture
// adds now.
taken.bytes = impl_->lease->CaptureBytes();
const auto tokens = std::span<const TextRunnerToken>(impl_->prompt)
.first(capture.position);
taken.refused = !impl_->lease->TryReserveSnapshot(
taken.bytes, capture.position, taken.preserve,
SnapshotPurpose::kContinuation, tokens);
if (!taken.refused)
taken.snapshot = impl_->lease->Capture();
} catch (...) {
taken.snapshot.reset();
}
prompt_captures.push_back(std::move(taken));
};
@@ -1297,7 +1268,6 @@ TextRunnerPool::Request::CommitMetrics TextRunnerPool::Request::Commit() {
impl_.reset();
return {};
}
const auto capabilities = impl_->runner->Descriptor().capabilities;
std::vector<ContinuationToken> checkpoint = impl_->prompt;
checkpoint.insert(checkpoint.end(), impl_->generated.begin(),
impl_->generated.end());
@@ -1313,31 +1283,22 @@ TextRunnerPool::Request::CommitMetrics TextRunnerPool::Request::Commit() {
}
checkpoint.resize(position);
// As in Cancel: an unaligned prefill frontier is not kept live. The lease
// drops a component state after publishing the prompt snapshot, which
// reads its positions; other states are reset here.
// drops the state after publishing the prompt snapshot, which reads its
// positions.
if (position % impl_->alignment != 0 &&
position > impl_->lease->cached_tokens() &&
position <= impl_->prefill_offset) {
checkpoint.clear();
}
if (capabilities.snapshot && capabilities.fork) {
CommitMetrics metrics = impl_->snapshot_metrics;
metrics.snapshot_bytes += impl_->lease->Commit(
std::move(impl_->snapshot_tokens), std::move(impl_->prompt_snapshot),
std::move(checkpoint));
metrics.component_cache = component_cache_metrics();
if (metrics.component_cache) {
metrics.disk_queued_bytes = metrics.component_cache->disk_queued_bytes;
metrics.disk_enqueue_ms = metrics.component_cache->disk_enqueue_ms;
}
impl_.reset();
return metrics;
CommitMetrics metrics = impl_->snapshot_metrics;
metrics.snapshot_bytes += impl_->lease->Commit(
std::move(impl_->snapshot_tokens), std::move(impl_->prompt_snapshot),
std::move(checkpoint));
metrics.component_cache = component_cache_metrics();
if (metrics.component_cache) {
metrics.disk_queued_bytes = metrics.component_cache->disk_queued_bytes;
metrics.disk_enqueue_ms = metrics.component_cache->disk_enqueue_ms;
}
std::unique_ptr<TextRunnerSnapshot> snapshot;
CommitMetrics metrics;
metrics.snapshot_bytes =
impl_->lease->Commit(std::move(checkpoint), std::move(snapshot));
impl_.reset();
return metrics;
}
@@ -1390,13 +1351,9 @@ TextRunnerPool::Request::Cancel() noexcept {
state.Invalidate();
}
CommitMetrics metrics = impl_->snapshot_metrics;
if (capabilities.snapshot && capabilities.fork) {
metrics.snapshot_bytes += impl_->lease->Commit(
std::move(impl_->snapshot_tokens), std::move(impl_->prompt_snapshot),
std::move(checkpoint));
} else {
metrics.snapshot_bytes = impl_->lease->Commit(std::move(checkpoint));
}
metrics.snapshot_bytes += impl_->lease->Commit(
std::move(impl_->snapshot_tokens), std::move(impl_->prompt_snapshot),
std::move(checkpoint));
metrics.component_cache = component_cache_metrics();
if (metrics.component_cache) {
metrics.disk_queued_bytes = metrics.component_cache->disk_queued_bytes;
-2
View File
@@ -47,10 +47,8 @@ using ext::TextRunnerDescriptor;
using ext::TextRunnerDiskCacheOptions;
using ext::TextRunnerMeasuredResources;
using ext::TextRunnerNgramRows;
using ext::TextRunnerPersistenceDescriptor;
using ext::TextRunnerRamCacheOptions;
using ext::TextRunnerResourceClaim;
using ext::TextRunnerSnapshot;
using ext::TextRunnerState;
using ext::TextRunnerToken;
+2 -1
View File
@@ -58,7 +58,8 @@ public:
[[nodiscard]] virtual std::size_t PayloadBytes() const noexcept = 0;
/// Stable model-owned byte payload used by persistent continuation caches.
/// Stable model-owned byte payload. Serving does not use it; model tests
/// compare it as a byte-exact oracle.
[[nodiscard]] virtual std::size_t PersistentPayloadBytes() const {
throw std::logic_error(
"draft backend snapshot does not support persistent sizing");
-20
View File
@@ -50,24 +50,4 @@ public:
virtual void Invalidate() noexcept = 0;
};
/// Immutable model-private continuation payload.
class ContinuationSnapshot {
public:
ContinuationSnapshot() = default;
virtual ~ContinuationSnapshot() = default;
ContinuationSnapshot(const ContinuationSnapshot&) = delete;
ContinuationSnapshot& operator=(const ContinuationSnapshot&) = delete;
ContinuationSnapshot(ContinuationSnapshot&&) = delete;
ContinuationSnapshot& operator=(ContinuationSnapshot&&) = delete;
[[nodiscard]] virtual std::size_t PayloadBytes() const noexcept = 0;
/// Prefer an available state that still holds this snapshot's borrowed rows.
/// This is an allocation hint; every state must remain able to restore it.
[[nodiscard]] virtual bool PrefersState(
const ContinuationState&) const noexcept {
return false;
}
};
} // namespace gufo::ext
-59
View File
@@ -76,55 +76,6 @@ std::vector<TextDecodeStep> TextModelRunner::DecodeBatch(
return steps;
}
std::unique_ptr<TextRunnerSnapshot> TextModelRunner::Snapshot(
const TextRunnerState&) const {
throw std::logic_error("runner does not support snapshots");
}
std::unique_ptr<TextRunnerSnapshot> TextModelRunner::SnapshotForPersistence(
const TextRunnerState& state) const {
return Snapshot(state);
}
std::size_t TextModelRunner::SnapshotPayloadBytes(
const TextRunnerState&) const {
return 0;
}
void TextModelRunner::RestoreOrFork(TextRunnerState&,
const TextRunnerSnapshot&) const {
throw std::logic_error("runner does not support snapshots");
}
std::size_t TextModelRunner::PersistentSnapshotPayloadBytes(
const TextRunnerSnapshot&) const {
return 0;
}
std::size_t TextModelRunner::SerializePersistentSnapshot(
const TextRunnerSnapshot&, std::span<std::uint8_t>) const {
return 0;
}
void TextModelRunner::RestorePersistentSnapshot(
TextRunnerState&, std::span<const std::uint8_t>) const {
throw std::logic_error("runner does not support persistent snapshots");
}
std::unique_ptr<ComponentCacheResources>
TextModelRunner::CreateComponentCacheResources(cache::ResourceLedger&,
std::size_t) const {
return nullptr;
}
std::unique_ptr<TextRunnerState> TextModelRunner::BindComponentState(
cache::Adapter&, cache::Slot&) const {
throw std::logic_error("runner does not support component cache");
}
void TextModelRunner::ReconcileComponentState(TextRunnerState&,
std::size_t) const {}
std::shared_ptr<const sampling::TokenConstraint>
TextModelRunner::BindConstraint(
std::shared_ptr<const sampling::JsonConstraint> grammar) const {
@@ -197,16 +148,6 @@ TextModelRunner::BuildReasoningBudget(
return budget;
}
void TextModelRunner::StreamPersistentSnapshot(
const TextRunnerSnapshot& snapshot, const SnapshotSink& sink) const {
std::vector<std::uint8_t> payload(PersistentSnapshotPayloadBytes(snapshot));
if (SerializePersistentSnapshot(snapshot, payload) != payload.size()) {
throw std::runtime_error(
"persistent snapshot serialized byte count mismatch");
}
sink(payload);
}
// ---------------------------------------------------------------------------
// Provider & Tool Registries
// ---------------------------------------------------------------------------
+11 -85
View File
@@ -56,7 +56,6 @@ struct TextPreparedPrompt {
struct TextRunnerRamCacheOptions {
static constexpr std::size_t kAutomaticMaxBytes = std::size_t{32} << 30;
static constexpr std::size_t kMaxEntries = 128;
std::size_t capacity_bytes{0};
bool enabled{true};
};
@@ -90,8 +89,6 @@ struct TextExecutionPlan {
struct TextRunnerCapabilities {
bool supports_vision{false};
bool incremental_prefill{false};
bool snapshot{false};
bool fork{false};
bool final_token_advance_required{true};
bool incremental_text_is_exact{false};
bool multi_token_decode{false};
@@ -106,24 +103,15 @@ struct TextRunnerCapabilities {
/// rounded down to it, except the complete prompt, which only an
/// unchanged retry restores; one keeps every position.
std::size_t prefill_alignment{1};
/// Boundaries one PrefillThrough call may capture in place. A runner that
/// overrides only the deprecated single-boundary form keeps one.
/// Boundaries one PrefillThrough call may capture in place.
std::size_t max_in_pass_checkpoints{1};
};
struct TextRunnerPersistenceDescriptor {
std::vector<std::uint8_t> compatibility_identity;
std::uint32_t payload_version{0};
bool operator==(const TextRunnerPersistenceDescriptor&) const = default;
};
struct TextRunnerDescriptor {
std::string model_id;
std::string state_abi;
std::uint32_t max_context{0};
TextRunnerCapabilities capabilities;
std::optional<TextRunnerPersistenceDescriptor> persistence;
sampling::TextModelPreset sampling_preset{
sampling::TextModelPreset::kUnspecified};
};
@@ -204,17 +192,6 @@ private:
std::span<const TextRunnerToken> prefill_lookahead_;
};
class TextRunnerSnapshot : public ContinuationSnapshot {
public:
TextRunnerSnapshot() = default;
~TextRunnerSnapshot() override = default;
TextRunnerSnapshot(const TextRunnerSnapshot&) = delete;
TextRunnerSnapshot& operator=(const TextRunnerSnapshot&) = delete;
TextRunnerSnapshot(TextRunnerSnapshot&&) = delete;
TextRunnerSnapshot& operator=(TextRunnerSnapshot&&) = delete;
};
struct TextRunnerAdvance {
std::reference_wrapper<TextRunnerState> state;
TextRunnerToken token{0};
@@ -410,15 +387,14 @@ public:
[[nodiscard]] virtual std::string Decode(
std::span<const TextRunnerToken> tokens) const = 0;
[[nodiscard]] virtual std::unique_ptr<TextRunnerState> CreateState()
const = 0;
[[nodiscard]] virtual bool UsesComponentCache() const { return false; }
/// Continuation contract. The cache owns every execution slot: it creates
/// the resources once, binds a state to each slot, and reconciles a state
/// after restoring rows into its slot at the given position.
[[nodiscard]] virtual std::unique_ptr<ComponentCacheResources>
CreateComponentCacheResources(cache::ResourceLedger&, std::size_t) const;
CreateComponentCacheResources(cache::ResourceLedger&, std::size_t) const = 0;
[[nodiscard]] virtual std::unique_ptr<TextRunnerState> BindComponentState(
cache::Adapter&, cache::Slot&) const;
virtual void ReconcileComponentState(TextRunnerState&, std::size_t) const;
cache::Adapter&, cache::Slot&) const = 0;
virtual void ReconcileComponentState(TextRunnerState&, std::size_t) const = 0;
[[nodiscard]] virtual bool HasDecodeFrontier(const TextRunnerState&) const {
return true;
}
@@ -456,38 +432,12 @@ public:
/// with a PrefillCheckpointBytes claim) without ending a pass there. Each
/// capture must equal the state a pass ending at its boundary leaves. The
/// runner invokes `consume` with each boundary's index, synchronously while
/// that boundary is readable as the lease's component state (it must drain
/// its captures before returning), or with the boundary's own snapshot
/// (native snapshot runners). Override this form; the default forwards one
/// boundary to the deprecated single-boundary form.
using InPassConsumer =
std::function<void(std::size_t, std::unique_ptr<TextRunnerSnapshot>)>;
[[nodiscard]] virtual TextPrefillStep PrefillThrough(
TextRunnerState& state, std::span<const TextRunnerToken> prompt,
std::size_t offset, std::size_t budget,
std::span<const std::size_t> boundaries,
const InPassConsumer& consume) const {
if (boundaries.size() != 1)
throw std::logic_error("runner captures one in-pass checkpoint per call");
bool captured = false;
std::unique_ptr<TextRunnerSnapshot> native;
auto step = PrefillThrough(state, prompt, offset, budget, boundaries[0],
&native, [&] {
captured = true;
consume(0, nullptr);
return std::unique_ptr<TextRunnerSnapshot>{};
});
if (!captured)
consume(0, std::move(native));
return step;
}
/// Deprecated: captures one boundary, into `checkpoint` or through
/// `capture` while the boundary is readable as the lease's component
/// state. Override the boundaries form instead.
/// that boundary is readable as the lease's component state; it must drain
/// its captures before returning.
using InPassConsumer = std::function<void(std::size_t)>;
[[nodiscard]] virtual TextPrefillStep PrefillThrough(
TextRunnerState&, std::span<const TextRunnerToken>, std::size_t,
std::size_t, std::size_t, std::unique_ptr<TextRunnerSnapshot>*,
const std::function<std::unique_ptr<TextRunnerSnapshot>()>& = {}) const {
std::size_t, std::span<const std::size_t>, const InPassConsumer&) const {
throw std::logic_error("runner does not support in-pass checkpoints");
}
[[nodiscard]] virtual TextDecodeSelection SelectNext(
@@ -508,30 +458,6 @@ public:
}
[[nodiscard]] virtual bool DeviceUsable() const { return true; }
[[nodiscard]] virtual std::size_t SnapshotPayloadBytes(
const TextRunnerState& state) const;
[[nodiscard]] virtual std::unique_ptr<TextRunnerSnapshot> Snapshot(
const TextRunnerState& state) const;
[[nodiscard]] virtual std::unique_ptr<TextRunnerSnapshot>
SnapshotForPersistence(const TextRunnerState& state) const;
virtual void RestoreOrFork(TextRunnerState& state,
const TextRunnerSnapshot& snapshot) const;
[[nodiscard]] virtual std::size_t PersistentSnapshotPayloadBytes(
const TextRunnerSnapshot& snapshot) const;
[[nodiscard]] virtual std::size_t SerializePersistentSnapshot(
const TextRunnerSnapshot& snapshot,
std::span<std::uint8_t> destination) const;
using SnapshotSink = std::function<void(std::span<const std::uint8_t>)>;
virtual void StreamPersistentSnapshot(const TextRunnerSnapshot& snapshot,
const SnapshotSink& sink) const;
virtual void RestorePersistentSnapshot(
TextRunnerState& state, std::span<const std::uint8_t> payload) const;
private:
mutable std::mutex constraint_mutex_;
mutable std::shared_ptr<const sampling::ConstraintVocabulary>
@@ -4,7 +4,7 @@
#include <array>
#include <atomic>
#include <cstring>
#include <gufo/ext/experimental/snapshot_copy.hpp>
#include <gufo/ext/snapshot_copy.hpp>
#include <mutex>
#include <optional>
#include <stdexcept>
@@ -2,7 +2,7 @@
#define GUFO_MODELS_DEEPSEEK_V4_FLASH_CONTINUATION_ADAPTER_HPP_
#include <gufo/ext/adapter.hpp>
#include <gufo/ext/experimental/transfer_pool.hpp>
#include <gufo/ext/transfer_pool.hpp>
#include "src/models/deepseek_v4_flash/continuation_layout.hpp"
#include "src/models/deepseek_v4_flash/engine.hpp"
+1 -1
View File
@@ -2,7 +2,7 @@
"schema": 1,
"name": "deepseek_v4_flash",
"version": "0.1.0",
"api_version": ">= 0.1.0",
"api_version": ">= 0.2.0",
"entry": "deepseek_v4_flash_register",
"registration_target": "gufo_deepseek_v4_flash",
"depends": [],
+4 -18
View File
@@ -135,14 +135,10 @@ public:
throw std::invalid_argument(
"DSpark disk cache requires a support SHA-256 fingerprint");
}
persistence_ = TextRunnerPersistenceDescriptor{
.compatibility_identity = DeepSeekCompatibilityIdentity(
artifact_fingerprint,
use_dspark_ ? support_fingerprint : std::string{}, max_context_,
max_draft_tokens_),
.payload_version = DS4_SESSION_PAYLOAD_VERSION,
};
component_identity_ = persistence_->compatibility_identity;
component_identity_ = DeepSeekCompatibilityIdentity(
artifact_fingerprint,
use_dspark_ ? support_fingerprint : std::string{}, max_context_,
max_draft_tokens_);
} else {
// Without file fingerprints the loaded model is the identity; durable
// caches require the validated artifact identity above.
@@ -151,7 +147,6 @@ public:
}
}
[[nodiscard]] bool UsesComponentCache() const override { return true; }
[[nodiscard]] std::unique_ptr<ComponentCacheResources>
CreateComponentCacheResources(cache::ResourceLedger& ledger,
std::size_t ram) const override {
@@ -187,8 +182,6 @@ public:
.capabilities =
TextRunnerCapabilities{
.incremental_prefill = true,
.snapshot = true,
.fork = true,
.final_token_advance_required = false,
.incremental_text_is_exact = true,
.multi_token_decode = dspark,
@@ -196,7 +189,6 @@ public:
.batched_multi_token_decode_max_width = 8u,
.prefix_reuse = true,
},
.persistence = persistence_,
.sampling_preset = sampling::TextModelPreset::kDeepSeekV4Flash,
};
}
@@ -318,11 +310,6 @@ public:
return text;
}
[[nodiscard]] std::unique_ptr<TextRunnerState> CreateState() const override {
throw std::logic_error(
"DeepSeek execution slots are owned by the component cache");
}
void PreparePrefixReuse(
TextRunnerState& state,
std::span<const TextRunnerToken> prefix) const override {
@@ -723,7 +710,6 @@ private:
std::uint32_t max_context_;
bool use_dspark_;
std::uint32_t max_draft_tokens_;
std::optional<TextRunnerPersistenceDescriptor> persistence_;
cache::Identity component_identity_;
};
+1 -1
View File
@@ -2,7 +2,7 @@
"schema": 1,
"name": "embedding_gemma2",
"version": "0.1.0",
"api_version": ">= 0.1.0",
"api_version": ">= 0.2.0",
"entry": "embedding_gemma2_register",
"registration_target": "gufo_embedding_gemma2",
"depends": [],
+1 -1
View File
@@ -2,7 +2,7 @@
"schema": 1,
"name": "minimax_h3",
"version": "0.1.0",
"api_version": ">= 0.1.0",
"api_version": ">= 0.2.0",
"entry": "minimax_h3_register",
"registration_target": "gufo_minimax_h3",
"depends": [],
+1 -1
View File
@@ -4,7 +4,7 @@
#include <array>
#include <atomic>
#include <cstring>
#include <gufo/ext/experimental/snapshot_copy.hpp>
#include <gufo/ext/snapshot_copy.hpp>
#include <mutex>
#include <optional>
#include <stdexcept>
+1 -1
View File
@@ -3,7 +3,7 @@
#include <atomic>
#include <gufo/ext/adapter.hpp>
#include <gufo/ext/experimental/transfer_pool.hpp>
#include <gufo/ext/transfer_pool.hpp>
#include "src/models/qwen/continuation_layout.hpp"
#include "src/models/qwen/hip/dflash.hpp"
+1 -1
View File
@@ -2,7 +2,7 @@
"schema": 1,
"name": "qwen",
"version": "0.1.0",
"api_version": ">= 0.1.0",
"api_version": ">= 0.2.0",
"entry": "qwen_register",
"registration_target": "gufo_qwen",
"depends": [],
+3 -18
View File
@@ -609,21 +609,15 @@ public:
execution_policy_(hip::QwenExecutionPolicy::Production()) {
if (!artifact_fingerprint.empty()) {
const bool speculative = dflash_model_ != nullptr;
persistence_ = TextRunnerPersistenceDescriptor{
.compatibility_identity = QwenCompatibilityIdentity(
artifact_fingerprint, draft_artifact_fingerprint, max_context_,
execution_policy_, speculative, speculative_options_,
dflash_policy_),
.payload_version = kQwenComponentPayloadVersion,
};
component_identity_ = persistence_->compatibility_identity;
component_identity_ = QwenCompatibilityIdentity(
artifact_fingerprint, draft_artifact_fingerprint, max_context_,
execution_policy_, speculative, speculative_options_, dflash_policy_);
} else {
component_identity_ =
QwenProcessIdentity(model_.get(), dflash_model_.get(), max_context_);
}
}
[[nodiscard]] bool UsesComponentCache() const override { return true; }
[[nodiscard]] std::unique_ptr<ComponentCacheResources>
CreateComponentCacheResources(cache::ResourceLedger& ledger,
std::size_t ram) const override {
@@ -692,8 +686,6 @@ public:
TextRunnerCapabilities{
.supports_vision = model_->VisionEncoder() != nullptr,
.incremental_prefill = true,
.snapshot = true,
.fork = true,
.final_token_advance_required = !speculative_enabled,
.incremental_text_is_exact = true,
.multi_token_decode = speculative_enabled,
@@ -705,7 +697,6 @@ public:
.partial_prefill_retention = true,
.prefill_rollback = true,
},
.persistence = persistence_,
.sampling_preset = sampling::TextPreset(model_->GetConfig()),
};
}
@@ -847,11 +838,6 @@ public:
return model_->GetTokenizer().Decode(tokens);
}
[[nodiscard]] std::unique_ptr<TextRunnerState> CreateState() const override {
throw std::logic_error(
"Qwen execution slots are owned by the component cache");
}
void PreparePrefixReuse(
TextRunnerState& state,
std::span<const TextRunnerToken> prefix) const override {
@@ -1085,7 +1071,6 @@ private:
/// Rows a lookup copy of a request decoding alone may verify (DFlash).
std::uint32_t copy_rows_{static_cast<std::uint32_t>(
hip::QwenGpuExecutor::kMaxWideVerificationRows)};
std::optional<TextRunnerPersistenceDescriptor> persistence_;
cache::Identity component_identity_;
};
@@ -4,7 +4,7 @@
#include <array>
#include <atomic>
#include <cstring>
#include <gufo/ext/experimental/snapshot_copy.hpp>
#include <gufo/ext/snapshot_copy.hpp>
#include <mutex>
#include <optional>
#include <stdexcept>
@@ -2,7 +2,7 @@
#define GUFO_MODELS_QWEN38_FLASH_NEXT_CONTINUATION_ADAPTER_HPP_
#include <gufo/ext/adapter.hpp>
#include <gufo/ext/experimental/transfer_pool.hpp>
#include <gufo/ext/transfer_pool.hpp>
#include "src/models/qwen38_flash_next/continuation_layout.hpp"
#include "src/models/qwen38_flash_next/engine.hpp"
+1 -1
View File
@@ -2,7 +2,7 @@
"schema": 1,
"name": "qwen38_flash_next",
"version": "0.1.0",
"api_version": ">= 0.1.0",
"api_version": ">= 0.2.0",
"entry": "qwen38_flash_next_register",
"registration_target": "gufo_qwen38_flash_next",
"depends": [],
+7 -23
View File
@@ -234,17 +234,12 @@ public:
// lifetime is an exact process-local identity; durable caches still
// require the validated full artifact fingerprints below.
if (!artifact_fingerprint.empty()) {
persistence_ = TextRunnerPersistenceDescriptor{
.compatibility_identity = QwenFlashNextCompatibilityIdentity(
artifact_fingerprint, use_mtp_ ? mtp_fingerprint : std::string{},
use_mtp_, max_context_, max_draft_tokens_,
model_->DecodeConcurrency(),
models::qwen38_flash_next::RopePositionPolicy(
model_->config().rope_scaling)),
.payload_version =
models::qwen38_flash_next::Session::kSnapshotPayloadVersion,
};
component_identity_ = persistence_->compatibility_identity;
component_identity_ = QwenFlashNextCompatibilityIdentity(
artifact_fingerprint, use_mtp_ ? mtp_fingerprint : std::string{},
use_mtp_, max_context_, max_draft_tokens_,
model_->DecodeConcurrency(),
models::qwen38_flash_next::RopePositionPolicy(
model_->config().rope_scaling));
} else {
component_identity_ = QwenFlashNextProcessIdentity(
model_.get(), max_context_, use_mtp_, max_draft_tokens_,
@@ -252,7 +247,6 @@ public:
}
}
[[nodiscard]] bool UsesComponentCache() const override { return true; }
[[nodiscard]] std::unique_ptr<ComponentCacheResources>
CreateComponentCacheResources(cache::ResourceLedger& ledger,
std::size_t ram) const override {
@@ -286,8 +280,6 @@ public:
TextRunnerCapabilities{
.supports_vision = model_->VisionEncoder() != nullptr,
.incremental_prefill = true,
.snapshot = true,
.fork = true,
.final_token_advance_required = false,
.incremental_text_is_exact = true,
.multi_token_decode = use_mtp_,
@@ -302,7 +294,6 @@ public:
std::max<std::size_t>(1, model_->config().compress_ratio),
.max_in_pass_checkpoints = rocm::kMaxPassCheckpoints,
},
.persistence = persistence_,
.sampling_preset = sampling::TextModelPreset::kQwen38,
};
}
@@ -415,11 +406,6 @@ public:
return model_->tokenizer().Decode(tokens);
}
[[nodiscard]] std::unique_ptr<TextRunnerState> CreateState() const override {
throw std::logic_error(
"Flash-Next execution slots are owned by the component cache");
}
void PreparePrefixReuse(
TextRunnerState& state,
std::span<const TextRunnerToken> prefix) const override {
@@ -489,7 +475,6 @@ public:
return bytes ? std::optional<std::size_t>(bytes) : std::nullopt;
}
using TextModelRunner::PrefillThrough;
[[nodiscard]] TextPrefillStep PrefillThrough(
TextRunnerState& state, std::span<const TextRunnerToken> prompt,
std::size_t offset, std::size_t budget,
@@ -519,7 +504,7 @@ public:
qfn.slot(), prefix, points,
[&](std::size_t index) {
const auto started = std::chrono::steady_clock::now();
consume(index, nullptr);
consume(index);
capture_ms += std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - started)
.count();
@@ -766,7 +751,6 @@ private:
std::uint32_t max_context_;
bool use_mtp_;
std::uint32_t max_draft_tokens_;
std::optional<TextRunnerPersistenceDescriptor> persistence_;
cache::Identity component_identity_;
};
+1 -1
View File
@@ -2,7 +2,7 @@
"schema": 1,
"name": "qwen3_asr",
"version": "0.1.0",
"api_version": ">= 0.1.0",
"api_version": ">= 0.2.0",
"entry": "qwen3_asr_register",
"registration_target": "gufo_qwen3_asr",
"depends": [],
+1 -1
View File
@@ -2,7 +2,7 @@
"schema": 1,
"name": "qwen3_tts",
"version": "0.1.0",
"api_version": ">= 0.1.0",
"api_version": ">= 0.2.0",
"entry": "qwen3_tts_register",
"registration_target": "gufo_qwen3_tts",
"depends": [],
+1 -1
View File
@@ -2,7 +2,7 @@
"schema": 1,
"name": "qwen_image_21",
"version": "0.1.0",
"api_version": ">= 0.1.0",
"api_version": ">= 0.2.0",
"entry": "qwen_image_21_register",
"registration_target": "gufo_qwen_image_21",
"depends": [],
+9 -46
View File
@@ -43,8 +43,6 @@ public:
bool defer_idle{};
bool in_pass{};
bool partial{};
/// Overrides only the deprecated single-boundary PrefillThrough.
bool legacy{};
std::size_t alignment{1};
/// Every prefill step's end and in-pass checkpoint, in order.
mutable std::vector<std::size_t> pass_ends;
@@ -53,20 +51,17 @@ public:
cache::Rows rows_per_chunk{4};
std::size_t row_backing_bytes{16};
mutable std::size_t through_calls{};
bool UsesComponentCache() const override { return true; }
TextRunnerDescriptor Descriptor() const override {
return {.model_id = "component-fake",
.state_abi = "component-fake-v1",
.max_context = 4096,
.capabilities = {.incremental_prefill = true,
.snapshot = true,
.fork = true,
.final_token_advance_required = false,
.prefix_reuse = true,
.in_pass_checkpoint = in_pass,
.partial_prefill_retention = partial,
.prefill_alignment = alignment,
.max_in_pass_checkpoints = legacy ? 1U : 64U}};
.max_in_pass_checkpoints = 64U}};
}
TextRunnerResourceClaim ResourceClaim() const override {
return {.state_capacity_bytes = 1ULL << 30,
@@ -87,9 +82,6 @@ public:
std::string Decode(std::span<const TextRunnerToken>) const override {
return "";
}
std::unique_ptr<TextRunnerState> CreateState() const override {
throw std::logic_error("legacy state must never be constructed");
}
// Image prompts carry their cache identity in the context.
void SetPromptContext(
TextRunnerState&,
@@ -137,15 +129,11 @@ public:
? std::optional<std::size_t>(1)
: std::nullopt;
}
using TextModelRunner::PrefillThrough;
TextPrefillStep PrefillThrough(TextRunnerState& state,
std::span<const TextRunnerToken> prompt,
std::size_t offset, std::size_t budget,
std::span<const std::size_t> boundaries,
const InPassConsumer& consume) const override {
if (legacy)
return TextModelRunner::PrefillThrough(state, prompt, offset, budget,
boundaries, consume);
++through_calls;
auto& s = dynamic_cast<State&>(state);
const auto count = std::min(budget, prompt.size() - offset);
@@ -154,7 +142,7 @@ public:
const auto head = prompt.subspan(at, boundaries[i] - at);
s.adapter.Append(s.slot, head, head);
at = boundaries[i];
consume(i, nullptr);
consume(i);
in_pass_points.push_back(boundaries[i]);
}
const auto tail = prompt.subspan(at, offset + count - at);
@@ -162,28 +150,6 @@ public:
pass_ends.push_back(offset + count);
return {count, offset + count == prompt.size()};
}
TextPrefillStep PrefillThrough(
TextRunnerState& state, std::span<const TextRunnerToken> prompt,
std::size_t offset, std::size_t budget, std::size_t boundary,
std::unique_ptr<TextRunnerSnapshot>* checkpoint,
const std::function<std::unique_ptr<TextRunnerSnapshot>()>& capture)
const override {
++through_calls;
auto& s = dynamic_cast<State&>(state);
const auto count = std::min(budget, prompt.size() - offset);
const auto head = prompt.subspan(offset, boundary - offset);
s.adapter.Append(s.slot, head, head);
try {
*checkpoint = capture();
} catch (...) {
checkpoint->reset();
}
in_pass_points.push_back(boundary);
const auto tail = prompt.subspan(boundary, offset + count - boundary);
s.adapter.Append(s.slot, tail, tail);
pass_ends.push_back(offset + count);
return {count, offset + count == prompt.size()};
}
TextDecodeSelection SelectNext(TextRunnerState& state,
sampling::SamplerState&) const override {
auto& s = dynamic_cast<State&>(state);
@@ -255,12 +221,9 @@ void InPassCapturePreservesBoundaryAndLiveTail() {
Prefill(cold);
const auto expected = Generate(cold);
cold.Invalidate();
for (const auto [fail, legacy] :
{std::pair{false, false}, std::pair{true, false},
std::pair{false, true}}) {
for (const bool fail : {false, true}) {
auto runner = std::make_shared<Runner>();
runner->in_pass = true;
runner->legacy = legacy;
TextRunnerPool pool(runner, 2);
auto seed =
pool.Acquire(prompt, sampling::SamplingConfig{}, {}, {}, true, 35);
@@ -651,7 +614,7 @@ void RestoreFromBusyBorrowedSource() {
const auto hash = runner->adapter->RecurrentHash(state.slot);
assert(source->TryReserveSnapshot(source->CaptureBytes(), prompt.size(),
false, SnapshotPurpose::kContinuation, {}));
std::shared_ptr<const TextRunnerSnapshot> snapshot = source->Capture();
std::shared_ptr<const ContinuationSnapshot> snapshot = source->Capture();
assert(source->PublishSnapshot(prompt, snapshot, false));
auto gauges = source->Metrics().ledger;
assert(gauges.reserved_bytes[static_cast<unsigned>(
@@ -683,7 +646,7 @@ void ColdAssignmentPrefersPreservedSlot() {
runner->adapter->Append(state.slot, prompt, prompt);
assert(source->TryReserveSnapshot(source->CaptureBytes(), prompt.size(),
false, SnapshotPurpose::kContinuation, {}));
std::shared_ptr<const TextRunnerSnapshot> snapshot = source->Capture();
std::shared_ptr<const ContinuationSnapshot> snapshot = source->Capture();
assert(source->PublishSnapshot(prompt, snapshot, false));
(void)source->Commit(prompt, {}, prompt);
// Idle streams are unavailable. Replacing the first slot would require
@@ -711,7 +674,7 @@ void ImageAfterCachedTextPrefixCaptures() {
runner->adapter->Append(state.slot, text, text);
assert(root->TryReserveSnapshot(root->CaptureBytes(), text.size(), false,
SnapshotPurpose::kContinuation, {}));
std::shared_ptr<const TextRunnerSnapshot> snapshot = root->Capture();
std::shared_ptr<const ContinuationSnapshot> snapshot = root->Capture();
assert(root->PublishSnapshot(text, snapshot, false));
}
auto image = text;
@@ -742,7 +705,7 @@ void ImageAfterCachedTextPrefixCaptures() {
const auto hash = runner->adapter->RecurrentHash(state.slot);
assert(lease->TryReserveSnapshot(lease->CaptureBytes(), image.size(), false,
SnapshotPurpose::kContinuation, {}));
std::shared_ptr<const TextRunnerSnapshot> snapshot = lease->Capture();
std::shared_ptr<const ContinuationSnapshot> snapshot = lease->Capture();
assert(lease->PublishSnapshot(image, snapshot, false));
// Model the retry after discarding a cancelled assistant: the image prompt
@@ -1492,7 +1455,7 @@ void FailedCaptureKeepsPublishedBoundary() {
const auto hash = runner->adapter->RecurrentHash(state.slot);
assert(lease->TryReserveSnapshot(lease->CaptureBytes(), 40, false,
SnapshotPurpose::kContinuation, {}));
std::shared_ptr<const TextRunnerSnapshot> snapshot = lease->Capture();
std::shared_ptr<const ContinuationSnapshot> snapshot = lease->Capture();
assert(lease->PublishSnapshot({prompt.begin(), prompt.begin() + 40}, snapshot,
false));
snapshot.reset();
@@ -1644,7 +1607,7 @@ void FragmentedBackingKeepsCompletePrompt() {
const auto hash = runner->adapter->RecurrentHash(state.slot);
assert(lease->TryReserveSnapshot(lease->CaptureBytes(), prompt.size(), false,
SnapshotPurpose::kContinuation, {}));
std::unique_ptr<TextRunnerSnapshot> snapshot;
std::unique_ptr<ContinuationSnapshot> snapshot;
try {
snapshot = lease->Capture();
} catch (const cache::ResourceExhausted&) {
+5 -13
View File
@@ -220,7 +220,7 @@ struct FakeControl {
std::uint32_t max_context{128};
std::optional<std::size_t> stop_after;
std::optional<std::size_t> eos_after;
// Retained state needs snapshot capabilities; a capture observer implies it.
// Requests share nothing unless asked; a capture observer implies it.
bool snapshots{false};
std::function<void()> snapshot_callback;
[[nodiscard]] bool Retains() const {
@@ -312,8 +312,6 @@ public:
.capabilities =
TextRunnerCapabilities{
.incremental_prefill = control_->incremental_prefill,
.snapshot = control_->Retains(),
.fork = control_->Retains(),
.final_token_advance_required =
control_->final_token_advance_required,
.incremental_text_is_exact =
@@ -322,9 +320,8 @@ public:
.batched_multi_token_decode =
control_->batched_multi_token_decode,
.batched_multi_token_decode_max_width = 4,
.prefix_reuse = control_->prefix_reuse,
.prefix_reuse = control_->prefix_reuse && control_->Retains(),
},
.persistence = std::nullopt,
.sampling_preset = control_->sampling_preset,
};
}
@@ -395,11 +392,6 @@ public:
return text;
}
[[nodiscard]] std::unique_ptr<TextRunnerState> CreateState() const override {
throw std::logic_error("slots are owned by the component cache");
}
[[nodiscard]] bool UsesComponentCache() const override { return true; }
[[nodiscard]] std::unique_ptr<gufo::server::ComponentCacheResources>
CreateComponentCacheResources(gufo::cache::ResourceLedger& ledger,
std::size_t) const override {
@@ -820,7 +812,7 @@ void TestParkedFollowerSurvivesLeaderCancellation() {
}
}
void TestSharedPrefixWaitRequiresSnapshots() {
void TestSharedPrefixWaitRequiresPrefixReuse() {
auto control = SharedPrefixControl(false);
control->block_prefill_label = 1;
auto scheduler = MakeScheduler(control, 2);
@@ -833,7 +825,7 @@ void TestSharedPrefixWaitRequiresSnapshots() {
const auto b = follower.Wait();
Expect(b.tokens == ExpectedTokens(1, 2) && b.shared_prefix_wait_ms == 0.0 &&
b.prefill_tokens == prompt.size(),
"runners without snapshots never park concurrent requests");
"runners without prefix reuse never park concurrent requests");
}
void TestParkedFollowersReserveVisibleSessions() {
@@ -3238,7 +3230,7 @@ int main() {
TestConcurrentSharedPrefixesPrefillOnce();
TestLeaderCancelledBeforeFollowerIsAdmitted();
TestParkedFollowerSurvivesLeaderCancellation();
TestSharedPrefixWaitRequiresSnapshots();
TestSharedPrefixWaitRequiresPrefixReuse();
TestParkedFollowersReserveVisibleSessions();
TestRetainedHistoryBeatsSharedPrefixWait();
TestAwaitedCheckpointSurvivesCachePressure();
+1 -20
View File
@@ -47,7 +47,6 @@ using gufo::server::TextRunnerDiskCacheOptions;
using gufo::server::TextRunnerMeasuredResources;
using gufo::server::TextRunnerPool;
using gufo::server::TextRunnerResourceClaim;
using gufo::server::TextRunnerSnapshot;
using gufo::server::TextRunnerState;
using gufo::server::TextRunnerToken;
@@ -188,7 +187,6 @@ public:
TextRunnerCapabilities{
.incremental_prefill = true,
},
.persistence = std::nullopt,
};
}
@@ -248,11 +246,6 @@ public:
return text;
}
[[nodiscard]] std::unique_ptr<TextRunnerState> CreateState() const override {
throw std::logic_error("slots are owned by the component cache");
}
[[nodiscard]] bool UsesComponentCache() const override { return true; }
[[nodiscard]] std::unique_ptr<gufo::server::ComponentCacheResources>
CreateComponentCacheResources(gufo::cache::ResourceLedger& ledger,
std::size_t) const override {
@@ -380,17 +373,7 @@ public:
private:
};
class SnapshotRunner : public FakeRunner {
public:
using FakeRunner::FakeRunner;
[[nodiscard]] TextRunnerDescriptor Descriptor() const override {
auto descriptor = FakeRunner::Descriptor();
descriptor.capabilities.snapshot = true;
descriptor.capabilities.fork = true;
return descriptor;
}
};
using SnapshotRunner = FakeRunner;
void TestPromptCountingValidatesCapabilitiesBeforeInference() {
const auto stats = std::make_shared<FakeStats>();
@@ -563,8 +546,6 @@ void TestResourceClaimsAreValidatedBeforeAllocation() {
"invalid resource claim is rejected before state allocation");
}
// A runner written against the single-boundary PrefillThrough keeps working
// through the default boundaries form, one checkpoint per call.
std::size_t RegularFiles(const std::filesystem::path& directory) {
std::size_t files = 0;
for (const auto& entry :
+3 -3
View File
@@ -81,7 +81,7 @@ gufo_target_extensions(fixture EXTENSIONS ${{selected}})
def add(self, name, depends=None, family=False, hip=False):
directory = self.source / (f"src/families/test/{name}" if family else f"src/models/{name}")
directory.mkdir(parents=True, exist_ok=True)
manifest = {"schema": 1, "name": name, "version": "1.2.3", "api_version": ">= 0.1.0",
manifest = {"schema": 1, "name": name, "version": "1.2.3", "api_version": ">= 0.2.0",
"depends": depends or [], "platforms": ["linux", "windows"],
"targets": {f"{name}_registration": {"requires": []}}}
if family:
@@ -287,7 +287,7 @@ void {name}_provide(gufo::ext::Registry& r) {{ r.AddProvider(gufo::ext::generate
"schema": 1,
"name": "external_model",
"version": "1.0.0",
"api_version": ">= 0.1.0",
"api_version": ">= 0.2.0",
"entry": "external_model_register",
"registration_target": "gufo_external_model",
"depends": [],
@@ -328,7 +328,7 @@ void external_model_register(gufo::ext::Registry&) {}
"schema": 1,
"name": "bad_ext",
"version": "1.0.0",
"api_version": ">= 0.1.0",
"api_version": ">= 0.2.0",
"entry": "bad_ext_register",
"registration_target": "gufo_bad_ext",
"depends": [],
@@ -78,31 +78,9 @@ private:
bool invalidated_{false};
};
class FakeSnapshot final : public gufo::ext::ContinuationSnapshot {
public:
explicit FakeSnapshot(const gufo::ext::ContinuationState* preferred)
: preferred_(preferred) {}
[[nodiscard]] std::size_t PayloadBytes() const noexcept override {
return 4096;
}
[[nodiscard]] bool PrefersState(
const gufo::ext::ContinuationState& state) const noexcept override {
return &state == preferred_;
}
private:
const gufo::ext::ContinuationState* preferred_;
};
void CheckContinuationBases() {
FakeState state;
FakeSnapshot snapshot(&state);
gufo::ext::ContinuationState& base = state;
gufo::ext::ContinuationSnapshot& snapshot_base = snapshot;
Expect(snapshot_base.PayloadBytes() == 4096, "snapshot payload bytes");
Expect(snapshot_base.PrefersState(base), "snapshot prefers its own state");
FakeSnapshot unrelated(nullptr);
Expect(!unrelated.PrefersState(base), "unrelated snapshot prefers nothing");
base.Invalidate();
Expect(state.invalidated(), "Invalidate reaches the model implementation");
+1 -1
View File
@@ -2,7 +2,7 @@
"schema": 1,
"name": "alpha",
"version": "1.2.3",
"api_version": ">= 0.1.0",
"api_version": ">= 0.2.0",
"entry": "alpha_register",
"registration_target": "alpha_registration",
"depends": [
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "base",
"kind": "library",
"version": "0.1.0",
"api_version": ">= 0.1.0",
"api_version": ">= 0.2.0",
"depends": [],
"platforms": [
"linux",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"schema": 1, "name": "beta", "version": "0.2.0", "api_version": ">= 0.1.0",
"schema": 1, "name": "beta", "version": "0.2.0", "api_version": ">= 0.2.0",
"entry": "beta_register", "registration_target": "beta_registration", "depends": [],
"platforms": ["linux", "windows"],
"providers": [{"id": "beta", "capability": "text_generation", "keys": ["gguf:beta"], "speculative": ["other"], "target": "beta_registration", "entry": "beta_provider_register"}],
+2 -2
View File
@@ -11,10 +11,10 @@
#include <gufo/ext/experimental/hipblaslt_plan_database.hpp>
#include <gufo/ext/experimental/host_allocations.hpp>
#include <gufo/ext/experimental/managed_alloc.hpp>
#include <gufo/ext/experimental/snapshot_copy.hpp>
#include <gufo/ext/experimental/snapshot_transfer.hpp>
#include <gufo/ext/experimental/transfer_pool.hpp>
#include <gufo/ext/experimental/weight_upload.hpp>
#include <gufo/ext/snapshot_copy.hpp>
#include <gufo/ext/transfer_pool.hpp>
#endif
// This symbol and registration function must be absent from a CPU executable.
+1 -1
View File
@@ -155,7 +155,7 @@ def check_diff(read_old, read_new, labels, old_records=None, new_records=None):
name, title = legacy
normalized = f"{RECORDS}/0.1/{name}.md"
description = re.sub(r"^# [0-9]+\.[0-9]+\.[0-9]+\n\n", "", body, count=1)
description = description.replace('`api_version: ">= 0.1.0"`',
description = description.replace('`api_version: ">= 0.2.0"`',
'an `api_version` floor in the active compatibility series')
if (not body.startswith(f"# {title}\n\n")
and new_records.get(normalized) == f"# {title}\n\n{description}"):