Skip to content

Commit a40136e

Browse files
committed
refactor(server): add backend-neutral IPC process layer
1 parent 6a9cbe5 commit a40136e

11 files changed

Lines changed: 363 additions & 204 deletions

dflash/CMakeLists.txt

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ add_library(dflash_common STATIC
235235
src/laguna/laguna_target_graph.cpp
236236
src/laguna/laguna_daemon.cpp
237237
src/laguna/laguna_backend.cpp
238+
src/common/backend_ipc.cpp
238239
src/common/dflash_feature_ring.cpp
239240
src/common/dflash_capture.cpp
240241
src/common/dflash_draft_ipc.cpp
@@ -671,24 +672,24 @@ if(DFLASH27B_TESTS)
671672
endif()
672673
endif()
673674

674-
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/ipc/dflash_draft_ipc_main.cpp")
675-
add_executable(dflash_draft_ipc_daemon
676-
src/ipc/dflash_draft_ipc_main.cpp
675+
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/ipc/backend_ipc_main.cpp")
676+
add_executable(backend_ipc_daemon
677+
src/ipc/backend_ipc_main.cpp
677678
)
678-
target_include_directories(dflash_draft_ipc_daemon PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS})
679+
target_include_directories(backend_ipc_daemon PRIVATE ${DFLASH27B_SRC_INCLUDE_DIRS})
679680
if(DFLASH27B_GPU_BACKEND STREQUAL "hip")
680-
target_compile_definitions(dflash_draft_ipc_daemon PRIVATE DFLASH27B_BACKEND_HIP=1 GGML_USE_HIP)
681+
target_compile_definitions(backend_ipc_daemon PRIVATE DFLASH27B_BACKEND_HIP=1 GGML_USE_HIP)
681682
else()
682-
target_compile_definitions(dflash_draft_ipc_daemon PRIVATE
683+
target_compile_definitions(backend_ipc_daemon PRIVATE
683684
DFLASH27B_BACKEND_CUDA=1
684685
DFLASH27B_CUDA_MIN_SM=${_dflash_cuda_min_sm})
685686
endif()
686-
target_link_libraries(dflash_draft_ipc_daemon PRIVATE dflash_common ggml ${DFLASH27B_GGML_BACKEND_TARGET} pthread)
687+
target_link_libraries(backend_ipc_daemon PRIVATE dflash_common ggml ${DFLASH27B_GGML_BACKEND_TARGET} pthread)
687688
if(DFLASH27B_GPU_BACKEND STREQUAL "cuda")
688689
find_package(CUDAToolkit REQUIRED)
689-
target_link_libraries(dflash_draft_ipc_daemon PRIVATE CUDA::cudart)
690+
target_link_libraries(backend_ipc_daemon PRIVATE CUDA::cudart)
690691
else()
691-
target_link_libraries(dflash_draft_ipc_daemon PRIVATE hip::host)
692+
target_link_libraries(backend_ipc_daemon PRIVATE hip::host)
692693
endif()
693694
endif()
694695

dflash/docs/ARCHITECTURE.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,15 @@ dflash/src/
4242
│ ├── snapshot_backend.h # Platform-aware snapshot backend selection
4343
│ ├── dflash_target.h # DFlashTarget interface (spec decode)
4444
│ ├── daemon_loop.{h,cpp} # Generic stdin/stdout daemon loop
45+
│ ├── backend_ipc.{h,cpp} # Generic backend IPC process lifecycle
4546
│ ├── device_placement.h # Multi-GPU placement config
4647
│ ├── gguf_inspect.{h,cpp}# Read arch + layer count from GGUF
4748
│ ├── layer_split_utils.{h,cpp} # compute_layer_ranges()
4849
│ ├── dflash_layer_split_runtime.h # LayerSplitRuntimeConfig + ActivationPair
4950
│ ├── dflash_feature_ring.{h,cpp} # DraftFeatureMirror + ring copy helpers
5051
│ ├── dflash_capture.{h,cpp} # target_capture_index() helper
5152
│ ├── dflash_draft_ipc.{h,cpp} # DFlash draft IPC client + remote copy
52-
│ ├── dflash_draft_ipc_daemon.cpp # Generic DFlash draft IPC daemon body
53+
│ ├── dflash_draft_ipc_daemon.cpp # DFlash draft mode for backend_ipc_daemon
5354
│ ├── dflash_draft_graph.{h,cpp} # Universal build_draft_step (DFlash draft graph)
5455
│ ├── dflash_spec_decode.{h,cpp} # Generic spec-decode loop over DFlashTarget
5556
│ ├── ddtree.{h,cpp} # Dynamic Draft Tree algorithm
@@ -240,15 +241,14 @@ Key components:
240241
Bridge qwen35 internals (`TargetWeights`, `TargetCache`,
241242
`TargetLayerSplitShard`) to the generic `DFlashTarget` interface so the
242243
shared spec-decode loop can drive verification.
243-
- **Feature transfer + draft daemon** (`common/dflash_feature_ring.{h,cpp}`,
244-
`common/dflash_capture.{h,cpp}`, `common/dflash_draft_ipc.{h,cpp}`,
245-
`common/dflash_draft_ipc_daemon.cpp`):
244+
- **Feature transfer + backend IPC daemon** (`common/backend_ipc.{h,cpp}`,
245+
`common/dflash_feature_ring.{h,cpp}`, `common/dflash_capture.{h,cpp}`,
246+
`common/dflash_draft_ipc.{h,cpp}`, `common/dflash_draft_ipc_daemon.cpp`):
246247
Move captured target activations into the draft-side ring buffer
247248
(`DraftFeatureMirror`) and ship them across processes/GPUs. The IPC
248-
client, parent-side feature-slice helper, and the daemon body itself
249-
all live in `common/` and are reusable by any DFlash target architecture
250-
(the DFlash draft model is a single universal Qwen3-style network shared
251-
across every target).
249+
process lifecycle is shared through `backend_ipc`; the DFlash draft client,
250+
parent-side feature-slice helper, and daemon mode stay on top of that common
251+
process layer and remain reusable by any DFlash target architecture.
252252
253253
### Qwen3Backend, Gemma4Backend, LagunaBackend
254254

dflash/docs/MIXED_BACKEND.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ kept at host-data or process boundaries:
1616
```bash
1717
cmake -S . -B build-cuda -DCMAKE_BUILD_TYPE=Release \
1818
-DDFLASH27B_GPU_BACKEND=cuda
19-
cmake --build build-cuda --target pflash_daemon test_dflash -j
19+
cmake --build build-cuda --target pflash_daemon test_dflash backend_ipc_daemon -j
2020

2121
cmake -S . -B build-hip -DCMAKE_BUILD_TYPE=Release \
2222
-DDFLASH27B_GPU_BACKEND=hip \
2323
-DDFLASH27B_HIP_ARCHITECTURES=<your-gfx-arch>
24-
cmake --build build-hip --target pflash_daemon test_dflash -j
24+
cmake --build build-hip --target pflash_daemon test_dflash backend_ipc_daemon -j
2525
```
2626

2727
## PFlash phase split
@@ -69,13 +69,14 @@ python scripts/phase_split_dual_gpu.py run-prompt \
6969

7070
## DFlash draft split
7171

72-
For DFlash, the target process can launch a separate draft IPC daemon from a
72+
For DFlash, the target process can launch a separate backend IPC daemon from a
7373
different backend build. The target process keeps target execution and any
7474
target layer split inside its own backend binary.
7575

7676
Use these `test_dflash` options for the target process:
7777

78-
- `--draft-ipc-bin <path>` points to the other backend's `test_dflash` binary.
78+
- `--draft-ipc-bin <path>` points to the other backend's `backend_ipc_daemon`
79+
binary.
7980
- `--draft-ipc-gpu <id>` selects the draft daemon device in that backend's
8081
visible-device namespace.
8182
- `--draft-ipc-work-dir <path>` selects where temporary IPC payload files are

dflash/src/common/backend_ipc.cpp

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// backend_ipc.cpp - generic backend IPC process launcher.
2+
3+
#include "backend_ipc.h"
4+
#include "io_utils.h"
5+
6+
#include <cstdint>
7+
#include <cstdio>
8+
#include <cstdlib>
9+
#include <string>
10+
#include <vector>
11+
12+
#if !defined(_WIN32)
13+
# include <cerrno>
14+
# include <cstring>
15+
# include <sys/stat.h>
16+
# include <sys/wait.h>
17+
# include <unistd.h>
18+
#endif
19+
20+
namespace dflash::common {
21+
22+
const char * backend_ipc_mode_name(BackendIpcMode mode) {
23+
switch (mode) {
24+
case BackendIpcMode::DFlashDraft: return "dflash-draft";
25+
}
26+
return "unknown";
27+
}
28+
29+
bool parse_backend_ipc_mode(const std::string & value, BackendIpcMode & out) {
30+
if (value == "dflash-draft") {
31+
out = BackendIpcMode::DFlashDraft;
32+
return true;
33+
}
34+
return false;
35+
}
36+
37+
bool BackendIpcProcess::start(const BackendIpcLaunchConfig & cfg) {
38+
#if defined(_WIN32)
39+
(void)cfg;
40+
std::fprintf(stderr, "Backend IPC is only implemented on POSIX hosts\n");
41+
return false;
42+
#else
43+
close();
44+
if (cfg.bin.empty() || cfg.payload_path.empty()) return false;
45+
if (!init_work_dir(cfg.work_dir)) return false;
46+
47+
int cmd_pipe[2] = {-1, -1};
48+
int stream_pipe[2] = {-1, -1};
49+
if (::pipe(cmd_pipe) != 0 || ::pipe(stream_pipe) != 0) {
50+
std::fprintf(stderr, "backend-ipc pipe failed: %s\n", std::strerror(errno));
51+
if (cmd_pipe[0] >= 0) ::close(cmd_pipe[0]);
52+
if (cmd_pipe[1] >= 0) ::close(cmd_pipe[1]);
53+
if (stream_pipe[0] >= 0) ::close(stream_pipe[0]);
54+
if (stream_pipe[1] >= 0) ::close(stream_pipe[1]);
55+
return false;
56+
}
57+
58+
pid_ = ::fork();
59+
if (pid_ < 0) {
60+
std::fprintf(stderr, "backend-ipc fork failed: %s\n", std::strerror(errno));
61+
::close(cmd_pipe[0]); ::close(cmd_pipe[1]);
62+
::close(stream_pipe[0]); ::close(stream_pipe[1]);
63+
pid_ = -1;
64+
return false;
65+
}
66+
if (pid_ == 0) {
67+
::dup2(cmd_pipe[0], STDIN_FILENO);
68+
::close(cmd_pipe[0]);
69+
::close(cmd_pipe[1]);
70+
::close(stream_pipe[0]);
71+
72+
std::vector<std::string> argv_storage;
73+
argv_storage.reserve(cfg.args.size() + 5);
74+
argv_storage.emplace_back(cfg.bin);
75+
argv_storage.emplace_back(
76+
std::string("--backend-ipc-mode=") + backend_ipc_mode_name(cfg.mode));
77+
argv_storage.emplace_back(cfg.payload_path);
78+
for (const std::string & arg : cfg.args) argv_storage.emplace_back(arg);
79+
argv_storage.emplace_back("--stream-fd=" + std::to_string(stream_pipe[1]));
80+
81+
std::vector<char *> argv;
82+
argv.reserve(argv_storage.size() + 1);
83+
for (std::string & arg : argv_storage) argv.push_back(arg.data());
84+
argv.push_back(nullptr);
85+
::execv(cfg.bin.c_str(), argv.data());
86+
std::fprintf(stderr, "backend-ipc exec failed: %s: %s\n",
87+
cfg.bin.c_str(), std::strerror(errno));
88+
_exit(127);
89+
}
90+
91+
::close(cmd_pipe[0]);
92+
::close(stream_pipe[1]);
93+
stream_fd_ = stream_pipe[0];
94+
cmd_ = ::fdopen(cmd_pipe[1], "w");
95+
if (!cmd_) {
96+
std::fprintf(stderr, "backend-ipc fdopen failed: %s\n", std::strerror(errno));
97+
::close(cmd_pipe[1]);
98+
close();
99+
return false;
100+
}
101+
int32_t status = -1;
102+
if (!read_exact_fd(stream_fd_, &status, sizeof(status)) || status != 0) {
103+
std::fprintf(stderr, "backend-ipc daemon did not become ready (status=%d)\n", status);
104+
close();
105+
return false;
106+
}
107+
active_ = true;
108+
std::printf("[backend-ipc] ready mode=%s bin=%s work_dir=%s\n",
109+
backend_ipc_mode_name(cfg.mode), cfg.bin.c_str(), work_dir_.c_str());
110+
return true;
111+
#endif
112+
}
113+
114+
void BackendIpcProcess::close() {
115+
#if !defined(_WIN32)
116+
if (cmd_) {
117+
std::fclose(cmd_);
118+
cmd_ = nullptr;
119+
}
120+
if (stream_fd_ >= 0) {
121+
::close(stream_fd_);
122+
stream_fd_ = -1;
123+
}
124+
if (pid_ > 0) {
125+
int status = 0;
126+
::waitpid(pid_, &status, 0);
127+
pid_ = -1;
128+
}
129+
if (owns_work_dir_ && !work_dir_.empty()) {
130+
::rmdir(work_dir_.c_str());
131+
}
132+
#endif
133+
active_ = false;
134+
owns_work_dir_ = false;
135+
work_dir_.clear();
136+
seq_ = 0;
137+
}
138+
139+
std::string BackendIpcProcess::next_path(const char * prefix) {
140+
return work_dir_ + "/" + prefix + "_" + std::to_string(seq_++) + ".bin";
141+
}
142+
143+
#if !defined(_WIN32)
144+
bool BackendIpcProcess::init_work_dir(const std::string & requested) {
145+
if (!requested.empty()) {
146+
work_dir_ = requested;
147+
owns_work_dir_ = false;
148+
if (::mkdir(work_dir_.c_str(), 0700) != 0 && errno != EEXIST) {
149+
std::fprintf(stderr, "backend-ipc mkdir failed: %s: %s\n",
150+
work_dir_.c_str(), std::strerror(errno));
151+
return false;
152+
}
153+
return true;
154+
}
155+
const char * tmp = std::getenv("TMPDIR");
156+
std::string templ = std::string(tmp && *tmp ? tmp : "/tmp") +
157+
"/backend-ipc-XXXXXX";
158+
std::vector<char> buf(templ.begin(), templ.end());
159+
buf.push_back('\0');
160+
char * dir = ::mkdtemp(buf.data());
161+
if (!dir) {
162+
std::fprintf(stderr, "backend-ipc mkdtemp failed: %s\n", std::strerror(errno));
163+
return false;
164+
}
165+
work_dir_ = dir;
166+
owns_work_dir_ = true;
167+
return true;
168+
}
169+
#endif
170+
171+
} // namespace dflash::common

dflash/src/common/backend_ipc.h

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// backend_ipc.h - generic backend IPC process launcher.
2+
//
3+
// Owns the out-of-process backend daemon lifecycle: fork/exec, command pipe,
4+
// binary status stream, and scratch work directory. Individual IPC modes keep
5+
// their own payload protocol on top of this process wrapper.
6+
7+
#pragma once
8+
9+
#include <cstdio>
10+
#include <string>
11+
#include <vector>
12+
13+
#if !defined(_WIN32)
14+
# include <sys/types.h>
15+
#endif
16+
17+
namespace dflash::common {
18+
19+
enum class BackendIpcMode {
20+
DFlashDraft,
21+
};
22+
23+
const char * backend_ipc_mode_name(BackendIpcMode mode);
24+
bool parse_backend_ipc_mode(const std::string & value, BackendIpcMode & out);
25+
26+
struct BackendIpcLaunchConfig {
27+
std::string bin;
28+
BackendIpcMode mode = BackendIpcMode::DFlashDraft;
29+
std::string payload_path;
30+
std::vector<std::string> args;
31+
std::string work_dir;
32+
};
33+
34+
class BackendIpcProcess {
35+
public:
36+
BackendIpcProcess() = default;
37+
BackendIpcProcess(const BackendIpcProcess &) = delete;
38+
BackendIpcProcess & operator=(const BackendIpcProcess &) = delete;
39+
~BackendIpcProcess() { close(); }
40+
41+
bool start(const BackendIpcLaunchConfig & cfg);
42+
void close();
43+
44+
bool active() const { return active_; }
45+
FILE * command_stream() const { return cmd_; }
46+
int stream_fd() const { return stream_fd_; }
47+
const std::string & work_dir() const { return work_dir_; }
48+
49+
std::string next_path(const char * prefix);
50+
51+
private:
52+
#if !defined(_WIN32)
53+
bool init_work_dir(const std::string & requested);
54+
55+
pid_t pid_ = -1;
56+
#endif
57+
FILE * cmd_ = nullptr;
58+
int stream_fd_ = -1;
59+
std::string work_dir_;
60+
int seq_ = 0;
61+
bool owns_work_dir_ = false;
62+
bool active_ = false;
63+
};
64+
65+
} // namespace dflash::common

0 commit comments

Comments
 (0)