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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ curl -s http://127.0.0.1:8216/v1/chat/completions \
| Environment variables | [Environment reference](server/docs/ENVIRONMENT.md) |
| Server internals | [Architecture](server/docs/ARCHITECTURE.md) |
| Client integration and qualification | [Harness guide](harness/README.md) |
| Server engine components | [Engine components](server/docs/ENGINE_COMPONENTS.md) |

Benchmarks stay with each implementation: [DFlash](server/RESULTS.md), [PFlash](optimizations/pflash/), [Spark](optimizations/spark/), [KVFlash](optimizations/kvflash/), and [Megakernel](optimizations/megakernel/).

Expand Down
22 changes: 16 additions & 6 deletions server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -541,12 +541,14 @@ add_library(dflash_common STATIC
src/common/backend_precision.cpp
src/common/daemon_loop.cpp
src/common/gguf_inspect.cpp
src/common/backend_plan.cpp
src/common/backend_factory.cpp
src/common/feature_gate.cpp
src/placement/placement_config.cpp
src/common/layer_split_utils.cpp
src/common/ddtree.cpp
src/common/peer_access.cpp
src/engine/luce_engine.cpp
# ── Server components (tokenizer, chat template) ──
src/server/tokenizer.cpp
src/server/chat_template.cpp
Expand Down Expand Up @@ -1657,6 +1659,8 @@ if(DFLASH27B_TESTS)
set(_server_unit_sources
test/test_unit_main.cpp
test/test_server_unit.cpp
test/test_generation.cpp
test/test_luce_engine.cpp
test/test_anchor_params.cpp
test/test_derived_scalars.cpp
test/test_adaptive_keep_ratio.cpp
Expand Down Expand Up @@ -1690,6 +1694,7 @@ if(DFLASH27B_TESTS)
endif()
add_executable(test_server_unit ${_server_unit_sources})
target_sources(test_server_unit PRIVATE
src/engine/generation.cpp
src/server/http_server.cpp
src/server/scheduler.cpp
src/server/model_card.cpp
Expand Down Expand Up @@ -1717,6 +1722,9 @@ if(DFLASH27B_TESTS)
target_link_libraries(test_server_unit PRIVATE CURL::libcurl)
endif()
target_link_libraries(test_server_unit PRIVATE dflash_common ggml ${DFLASH27B_GGML_BACKEND_TARGET})
if(UNIX)
target_link_libraries(test_server_unit PRIVATE pthread)
endif()
if(DFLASH27B_GPU_BACKEND STREQUAL "cuda")
find_package(CUDAToolkit REQUIRED)
target_link_libraries(test_server_unit PRIVATE CUDA::cudart)
Expand Down Expand Up @@ -1816,16 +1824,18 @@ if(DFLASH27B_TESTS)
list(APPEND _raw_unit_test_targets test_model_smoke)
endif()

# Feature/architecture gate tests. check_feature_compatibility(),
# collect_feature_warnings() and the capability table are pure functions,
# so this target deliberately compiles only feature_gate.cpp and
# placement_config.cppno dflash_common, no ggml, no GPU toolkit. That
# keeps a gate rule testable in seconds instead of behind a full backend
# build, which is the whole reason these tests do not live in
# Backend planning and feature/architecture gate tests. The plan builder,
# check_feature_compatibility(), collect_feature_warnings(), and the
# capability table are pure policy, so this target compiles only their
# sources and placement_config.cpp. It needs no dflash_common, ggml, or GPU
# toolkit. This keeps policy rules testable in seconds instead of behind a
# full backend build, which is why these tests do not live in
# test_server_unit.
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_feature_gate.cpp")
add_executable(test_feature_gate test/test_feature_gate.cpp)
target_sources(test_feature_gate PRIVATE
test/test_backend_plan.cpp
src/common/backend_plan.cpp
src/common/feature_gate.cpp
src/placement/placement_config.cpp)
target_include_directories(test_feature_gate PRIVATE
Expand Down
251 changes: 251 additions & 0 deletions server/docs/ENGINE_COMPONENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
# Server engine components

This document describes the server engine structure implemented in the current
code. It is a reference, not a roadmap.

## Ownership overview

```text
server_main
├─ builds BackendPlan
├─ creates ModelBackend
├─ transfers ModelBackend ownership to LuceEngine
└─ constructs HttpServer, which borrows LuceEngine

LuceEngine
├─ owns ModelBackend
├─ owns the serving thread
└─ selects one serving loop
├─ serial: HttpServer::worker_loop()
└─ concurrent: HttpServer::scheduler_loop(SeqEngine &)

HttpServer
├─ owns sockets and HTTP protocol state
├─ owns the current ServerJob queue
├─ prepares model-ready GenerateRequest values
└─ formats tokens and terminal results for clients
```

The backend outlives `HttpServer` because `server_main` constructs
`LuceEngine` before the server. `HttpServer` stops and joins the serving loop
before its transport and cache state is destroyed.

## Components

### `BackendPlan`

`BackendPlan` is the validated, normalized input to backend construction. Its
groups describe the effective model, placement, cache, speculation, runtime,
and architecture-specific values selected during startup.

The backend factory accepts a plan, projects its values into the selected
architecture config, and returns one owned `ModelBackend`. Architecture
configs own persistent strings; they do not retain pointers into the plan.

Files:

- `server/src/common/backend_plan.cpp`
- `server/src/common/backend_plan_internal.h`
- `server/src/common/backend_factory.{h,cpp}`

### `ModelBackend`

`ModelBackend` is the common model-resource interface implemented by each
architecture adapter. A concrete backend owns weights, caches, snapshots,
model-specific execution state, and any architecture-specific helpers.

Its complete-request generation operations remain the execution mechanism for
serial serving. A backend that supports continuous batching also exposes a
borrowed `SeqEngine`; that object is owned by the backend and remains valid for
the backend lifetime.

File: `server/src/common/model_backend.h`

### `LuceEngine`

`LuceEngine` is the runtime owner. It owns exactly one `ModelBackend` and at
most one serving thread.

Its current public lifecycle is:

```cpp
explicit LuceEngine(std::unique_ptr<ModelBackend> backend);

ModelBackend & backend() noexcept;
bool start_serving(ServingLoops loops, bool allow_concurrent);
void stop_serving();
```

`start_serving()` selects the concurrent loop only when both conditions hold:

- the caller permits concurrent local serving; and
- the backend exposes a `SeqEngine`.

Otherwise it starts the serial loop. The selected callback is owned by the
worker thread. `stop_serving()` requests shutdown and joins that thread before
returning. Destruction also calls `stop_serving()` and then destroys the owned
backend, whose concrete destructor performs backend shutdown.

Files: `server/src/engine/luce_engine.{h,cpp}`

### `HttpServer`

`HttpServer` owns the HTTP-facing state:

- listen and client sockets;
- request parsing and response formatting;
- SSE state and client-disconnect detection;
- tokenizer-dependent request preparation;
- prefix-cache policy and server status;
- the current intrusive `ServerJob` queue.

It borrows `LuceEngine` and, through it, a `ModelBackend`. The references are
valid for the complete `HttpServer` lifetime.

At startup the server supplies its established serial and concurrent loops to
`LuceEngine`. Upstream PFlash forwarding disables concurrent local serving,
so the serial worker remains selected for that configuration.

Files:

- `server/src/server/http_server.{h,cpp}`
- `server/src/server/scheduler.cpp`

### `GenerateRequest` and `GenerateResult`

`GenerateRequest` is the model-ready input shared by backend execution paths.
It owns every token sequence that may be retained during generation:

- prompt tokens;
- speculative hint tokens;
- stall-detection sequences;
- thinking-budget close tokens.

`GenerateResult` owns the completed token vector, timings, typed failure, and
generation metadata. Neither type contains HTTP or JSON state.

File: `server/src/common/generation_types.h`

### Generation channel

The generation channel provides transport-independent request and result
lifetime types:

```text
GenerationQueue::submit(GenerateRequest)
-> Generation consumer handle

GenerationQueue::next()
-> GenerateRequest + GenerationSource

GenerationSource
-> TokenBatch
-> coalesced GenerationProgress
-> one GenerateCompleted result
```

`Generation` and `GenerationSource` are move-only. Dropping an unfinished
consumer cancels that generation. Losing its producer completes the consumer
with a typed failure. The queue bounds both waiting requests and buffered
tokens; shutdown rejects new requests and completes every live channel.

These channel types are implemented and tested. The live HTTP path still uses
`ServerJob` and the two existing serving loops, so the channel is not yet the
HTTP submission path.

Files:

- `server/src/engine/generation.{h,cpp}`
- `server/test/test_generation.cpp`

### `SeqEngine`

`SeqEngine` is the model-side capability used by concurrent serving. It owns
slot allocation, per-sequence model state, KV capacity, batched prefill and
decode execution, sampling, and retirement.

`HttpServer::scheduler_loop()` currently owns admission order, fair prefill
selection, cancellation, response construction, and non-blocking delivery.
All `SeqEngine` calls come from the single serving thread owned by
`LuceEngine`.

File: `server/src/common/concurrency/seq_engine.h`

## Runtime flow

### Startup

```text
CLI arguments
-> BackendPlan::build()
-> create_backend(plan)
-> LuceEngine(std::move(backend))
-> HttpServer(engine, tokenizer, config)
-> LuceEngine::start_serving(...)
```

Backend validation and normalization happen before construction. Reporting,
tokenizer setup, and backend construction read the same effective plan values.

### Serial request

```text
client thread
-> enqueue ServerJob
serving thread
-> worker_loop()
-> prepare prompt, cache state, GenerateRequest, and callbacks
-> ModelBackend::generate() or restore_and_generate()
-> stream or format GenerateResult
-> complete ServerJob
client thread
-> close request connection
```

Only the serving thread mutates model execution state.

### Concurrent request

```text
client thread
-> enqueue ServerJob
serving thread
-> scheduler_loop(SeqEngine &)
-> admit requests into model slots
-> select bounded prefill work and all live decode rows
-> SeqEngine::step()
-> buffer and flush client output without blocking other slots
-> retire completed, failed, or cancelled slots
```

Admission order and slot identity remain stable until retirement. A slow or
disconnected client cannot block model progress for other live slots.

### Shutdown

```text
HttpServer::shutdown()
-> set stopping flag and wake the request queue
-> LuceEngine::stop_serving()
-> invoke request_stop callback
-> join serving thread
-> close SSE clients
-> drain remaining ServerJob values
-> release server-owned cache and transport state
```

`stop_serving()` is idempotent and serializes stop/restart through the full
worker join. `HttpServer` also calls it when `run()` exits. `LuceEngine`
destroys the backend only after the serving thread has joined; the concrete
backend destructor owns its single shutdown call.

## Current boundary

The ownership boundary is active: `LuceEngine` owns the backend and execution
thread, while `HttpServer` owns transport. Request coordination has not fully
crossed that boundary yet: `ServingLoops` and the `ServerJob` queue connect the
runtime owner to the existing HTTP worker and scheduler.

The generation channel is therefore an available engine component, not yet a
live server entry point. This distinction is reflected in the types and tests
rather than hidden behind a second generation path.
9 changes: 5 additions & 4 deletions server/src/bailingmoe3/bailingmoe3_backend.cpp
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
#include "bailingmoe3_backend.h"

#include <cstdio>
#include <utility>

namespace dflash::common {
namespace {

Qwen35Config make_qwen_runtime_config(const BailingMoe3Config & cfg) {
Qwen35Config make_qwen_runtime_config(BailingMoe3Config cfg) {
Qwen35Config runtime;
runtime.target_path = cfg.model_path;
runtime.target_path = std::move(cfg.model_path);
runtime.device = cfg.device;
runtime.stream_fd = cfg.stream_fd;
// The Ling baseline uses the ordinary contiguous F16/Q4 KV cache and the
Expand All @@ -22,8 +23,8 @@ Qwen35Config make_qwen_runtime_config(const BailingMoe3Config & cfg) {

} // namespace

BailingMoe3Backend::BailingMoe3Backend(const BailingMoe3Config & cfg)
: Qwen35Backend(make_qwen_runtime_config(cfg)) {}
BailingMoe3Backend::BailingMoe3Backend(BailingMoe3Config cfg)
: Qwen35Backend(make_qwen_runtime_config(std::move(cfg))) {}

bool BailingMoe3Backend::load_target_model(ggml_backend_t backend,
TargetWeights & out) {
Expand Down
6 changes: 4 additions & 2 deletions server/src/bailingmoe3/bailingmoe3_backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@

#include "qwen35_backend.h"

#include <string>

namespace dflash::common {

// Configuration intentionally exposes only the features the first native
// Ling backend implements. Speculative decode and expert offload can be added
// after the autoregressive path has a logits-equivalent baseline.
struct BailingMoe3Config {
const char * model_path = nullptr;
std::string model_path;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
DevicePlacement device;
int stream_fd = -1;
};

class BailingMoe3Backend final : public Qwen35Backend {
public:
explicit BailingMoe3Backend(const BailingMoe3Config & cfg);
explicit BailingMoe3Backend(BailingMoe3Config cfg);

void print_ready_banner() const override;
bool supports_dflash_spec_decode() const override { return false; }
Expand Down
Loading
Loading