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
7 changes: 6 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2316,6 +2316,7 @@ add_library(vllm_shared SHARED "${_vllm_shared_stub}")
add_library(vllm::shared ALIAS vllm_shared)
set_target_properties(vllm_shared PROPERTIES
OUTPUT_NAME vllm
ARCHIVE_OUTPUT_NAME vllm_shared
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR}
CXX_VISIBILITY_PRESET hidden
Expand All @@ -2328,7 +2329,11 @@ target_include_directories(vllm_shared PUBLIC
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
# Force-link the whole `vllm` archive (the C ABI + engine + the CPU-backend
# static registrar) and inherit its PUBLIC deps (CUDA::cudart, Threads, ...).
target_link_libraries(vllm_shared PRIVATE vllm)
# On Windows the packaged shared target also needs the vendored BLAKE3 archive
# explicitly on its own link line; relying on the static archive's usage
# requirements is not sufficient once the C ABI DLL is assembled via
# /WHOLEARCHIVE.
target_link_libraries(vllm_shared PRIVATE vllm blake3_vendored)
# Export only the C ABI: `vllm_*` stays global, everything else is localized.
# UNLIKE the force-link guard above, `UNIX AND NOT APPLE` is CORRECT here: a
# linker version script is a GNU-ld/ELF feature with no ld64 spelling (ld64 uses
Expand Down
7 changes: 7 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2691,6 +2691,13 @@ declarations in that header) suitable for `dlopen` / FFI / LocalAI integration.
This line read `19` and `36` until 2026-08-17; both numbers were last true
several ABI additions ago, and neither is derived by any gate.

On native Windows/MSVC, the shared-library packaging lane keeps the runtime DLL
name at `vllm` and gives the import/static archive the distinct name
`vllm_shared`, so one build tree can hold the shared C ABI package and the
static `vllm` archive without a filename collision. The same ABI smoke test
therefore resolves the exported symbols through `LoadLibraryA` /
`GetProcAddress` on Windows and `dlopen` / `dlsym` on POSIX.

```c
#include "vllm.h"

Expand Down
7 changes: 3 additions & 4 deletions src/vllm/model_executor/model_loader/safetensors_reader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

#include <nlohmann/json.hpp>

#include "vllm/support/platform_compat.h"

namespace vllm {

namespace {
Expand Down Expand Up @@ -280,10 +282,7 @@ namespace {

#if !defined(_WIN32)
long HostPageSize() {
static const long page = [] {
const long p = ::sysconf(_SC_PAGESIZE);
return p > 0 ? p : 4096;
}();
static const long page = support::HostPageSize();
return page;
}
#endif
Expand Down
68 changes: 68 additions & 0 deletions src/vllm/support/platform_compat.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#pragma once

#include <cstdint>
#include <cstdio>
#include <cstdlib>

#if defined(_WIN32)
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <io.h>
#include <windows.h>
#else
#include <unistd.h>
#endif

namespace vllm::support {

#if defined(_WIN32)

inline long HostPageSize() {
SYSTEM_INFO system_info{};
GetSystemInfo(&system_info);
return system_info.dwPageSize > 0
? static_cast<long>(system_info.dwPageSize)
: 4096L;
}

inline int CurrentProcessId() { return static_cast<int>(::GetCurrentProcessId()); }

inline int FileDescriptorFromFile(std::FILE* file) { return _fileno(file); }

inline bool TruncateFile(int fd, std::uint64_t size) { return _chsize_s(fd, size) == 0; }

// DIVERGENT ON AN EMPTY VALUE, and deliberately not normalised: `_putenv_s(name,
// "")` REMOVES the variable, where POSIX `setenv(name, "", 1)` defines it empty.
// No caller passes an empty value today. A test that needs defined-but-empty must
// say so at its call site rather than relying on this — the same contract
// `tests/support/test_env.h` records for the test-side seam.
inline bool SetEnvVar(const char* name, const char* value) {
return _putenv_s(name, value) == 0;
}

#else

inline long HostPageSize() {
const long page_size = ::sysconf(_SC_PAGESIZE);
return page_size > 0 ? page_size : 4096L;
}

inline int CurrentProcessId() { return ::getpid(); }

inline int FileDescriptorFromFile(std::FILE* file) { return ::fileno(file); }

inline bool TruncateFile(int fd, std::uint64_t size) {
return ::ftruncate(fd, static_cast<off_t>(size)) == 0;
}

inline bool SetEnvVar(const char* name, const char* value) {
return ::setenv(name, value, 1) == 0;
}

#endif

} // namespace vllm::support
15 changes: 15 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2197,3 +2197,18 @@ target_include_directories(test_qwen3_32b_nvfp4a16_paged_engine PRIVATE
# CPU-only and needs no checkpoint.
vllm_cpp_add_test(test_registry_downcast_refusal
vllm/models/test_registry_downcast_refusal.cpp)

# The five suites that reach src/vllm/support/platform_compat.h, granted per
# target rather than globally: the file already carries 123 explicit
# ${CMAKE_SOURCE_DIR}/src grants, and a blanket one in vllm_cpp_add_test would
# convert that deliberate opt-in into a repo-wide default (#503).
foreach(_pc_target
test_safetensors
test_minimax_h3
test_minimax_h3_video_fold
test_kv_offload_connector
test_kv_offload_tiering)
if(TARGET ${_pc_target})
target_include_directories(${_pc_target} PRIVATE ${CMAKE_SOURCE_DIR}/src)
endif()
endforeach()
18 changes: 11 additions & 7 deletions tests/capi/test_capi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@
#include <string>
#include <vector>

#include <unistd.h>

#include <nlohmann/json.hpp>

#include "capi/engine_handle.h"
#include "support/test_env.h"
#include "vllm/config/device.h"
#include "vllm/config/multimodal.h"
#include "vllm/entrypoints/model_loader.h"
#include "vllm/platforms/interface.h"
#include "vllm/support/platform_compat.h"
#include "vllm/entrypoints/openai/serving_utils.h"
#include "vllm/model_executor/models/qwen3_5_weights.h"
#include "vllm/tokenizer/bpe.h"
Expand Down Expand Up @@ -414,7 +414,7 @@ TEST_CASE("capi: vllm_complete_tokens matches the string-prompt completion (ABI
// reports six zero-initialized buffer entries must not satisfy ABI v12.
const int32_t expected_ids[6] = {22, 12, 14, 9, 13, 2};
for (int i = 0; i < 6; ++i) {
INFO("generated token index ", i);
CAPTURE(i);
CHECK(out_tokens[i] == expected_ids[i]);
}
REQUIRE(via_tok.text != nullptr);
Expand Down Expand Up @@ -1222,7 +1222,7 @@ TEST_CASE("capi: enable_jump_forward defaults to 0 and validates (ABI v10)") {
TEST_CASE("capi: enable_jump_forward=on reaches the engine; default is inert (ABI v10)") {
// Resolution reads VT_ENABLE_JUMP_FORWARD as an override; clear it so this
// test asserts the FIELD's effect, not an ambient env override.
::unsetenv("VT_ENABLE_JUMP_FORWARD");
vllm_test::UnsetEnv("VT_ENABLE_JUMP_FORWARD");
const HfConfig c = MakeConfig();

// Default (nullopt): jump-forward resolves OFF — byte-identical to before v10.
Expand Down Expand Up @@ -1697,8 +1697,12 @@ struct VideoFoldWorkspace {
std::string root, fixture;
VideoFoldWorkspace() {
static int counter = 0;
root = "/tmp/vllm_capi_video_" + std::to_string(::getpid()) + "_" +
std::to_string(counter++);
root =
(std::filesystem::temp_directory_path() /
("vllm_capi_video_" +
std::to_string(vllm::support::CurrentProcessId()) + "_" +
std::to_string(counter++)))
.string();
std::filesystem::create_directories(root);
fixture = root + "/fixture";
minimax_h3_fold::WriteFoldFixture(fixture);
Expand Down Expand Up @@ -1812,7 +1816,7 @@ TEST_CASE("capi v12: vllm_video_generate reproduces the pre-fold goldens") {
for (int f = 0; f < 8; ++f) {
char name[64];
std::snprintf(name, sizeof(name), "/frame_%06d.ppm", f);
INFO("frame ", f);
CAPTURE(f);
CHECK(ReadAllBytes(out_dir + name) == ReadAllBytes(golden_dir + name));
}
CHECK(ReadAllBytes(out_dir + "/audio.wav") ==
Expand Down
71 changes: 56 additions & 15 deletions tests/capi/test_dlopen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,61 @@

#include <doctest/doctest.h>

#if defined(_WIN32)
#include <windows.h>
#else
#include <dlfcn.h>
#endif

#include <string>

#ifndef VLLM_SHARED_LIB_PATH
#error "VLLM_SHARED_LIB_PATH must be defined (path to the built libvllm.so)"
#error "VLLM_SHARED_LIB_PATH must be defined (path to the built shared library)"
#endif

namespace {

#if defined(_WIN32)
using SharedLibraryHandle = HMODULE;

std::string LastSharedLibraryError() {
const DWORD error = GetLastError();
return error == 0 ? std::string() : ("GetLastError=" + std::to_string(error));
}

SharedLibraryHandle OpenSharedLibrary(const char* path) {
return LoadLibraryA(path);
}

void* LoadSymbol(SharedLibraryHandle handle, const char* name) {
return reinterpret_cast<void*>(GetProcAddress(handle, name));
}

bool CloseSharedLibrary(SharedLibraryHandle handle) {
return FreeLibrary(handle) != 0;
}
#else
using SharedLibraryHandle = void*;

std::string LastSharedLibraryError() {
const char* error = dlerror();
return error != nullptr ? std::string(error) : std::string();
}

SharedLibraryHandle OpenSharedLibrary(const char* path) {
return dlopen(path, RTLD_NOW | RTLD_LOCAL);
}

void* LoadSymbol(SharedLibraryHandle handle, const char* name) {
return dlsym(handle, name);
}

bool CloseSharedLibrary(SharedLibraryHandle handle) {
return dlclose(handle) == 0;
}
#endif


// Function-pointer types for the ABI symbols we dlsym. These mirror the
// declarations in vllm.h; a header-less consumer would type them by hand.
using fn_version = const char* (*)(void);
Expand Down Expand Up @@ -58,27 +103,23 @@ using fn_string_free = void (*)(char*);
using fn_completion_free = void (*)(vllm_completion*);
using fn_last_error = const char* (*)(void);

// Resolve `name` from `handle`; the returned pointer must be non-null (fails the
// test otherwise). Uses a union-free reinterpret through void* (POSIX-sanctioned
// for dlsym function pointers).
template <typename Fn>
Fn Sym(void* handle, const char* name) {
void* p = dlsym(handle, name);
INFO("dlsym(", name, ")");
REQUIRE(p != nullptr);
return reinterpret_cast<Fn>(p);
Fn Sym(SharedLibraryHandle handle, const char* name) {
void* symbol = LoadSymbol(handle, name);
INFO("resolve(", name, ")");
REQUIRE(symbol != nullptr);
return reinterpret_cast<Fn>(symbol);
}

} // namespace

// ─── the packaging DoD: dlopen + dlsym every ABI symbol, drive header-free ────
TEST_CASE("dlopen: libvllm.so resolves the whole C ABI by name and drives it") {
// (1) dlopen the built shared library (RTLD_NOW forces eager symbol binding —
// an unresolved symbol would fail here, proving the .so is self-contained).
void* lib = dlopen(VLLM_SHARED_LIB_PATH, RTLD_NOW | RTLD_LOCAL);
INFO("dlopen error: ", (dlerror() != nullptr ? dlerror() : ""));
TEST_CASE("shared library resolves the whole C ABI by name and drives it") {
SharedLibraryHandle lib = OpenSharedLibrary(VLLM_SHARED_LIB_PATH);
INFO("shared library load error: ", LastSharedLibraryError());
REQUIRE(lib != nullptr);


// (2) dlsym EVERY stable C ABI symbol by name — all must be non-null.
auto p_version = Sym<fn_version>(lib, "vllm_version");
auto p_abi = Sym<fn_abi_version>(lib, "vllm_abi_version");
Expand Down Expand Up @@ -143,5 +184,5 @@ TEST_CASE("dlopen: libvllm.so resolves the whole C ABI by name and drives it") {
// p_engine_free on null is a no-op (exercises the free pointer safely).
p_engine_free(nullptr);

CHECK(dlclose(lib) == 0);
CHECK(CloseSharedLibrary(lib));
}
18 changes: 12 additions & 6 deletions tests/vllm/models/test_minimax_h3.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,14 @@
#include <nlohmann/json.hpp>

#include <algorithm>
#include <filesystem>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <map>
#include <memory>
#include <set>
#include <sys/stat.h>
#include <unistd.h>
#include <array>
#include <cstdio>
#include <cstring>
Expand All @@ -50,6 +49,7 @@
#include "support/max_abs_diff.h"
#include "vllm/model_executor/model_loader/gguf_dequant.h"
#include "vllm/model_executor/model_loader/gguf_reader.h"
#include "vllm/support/platform_compat.h"
#include "vllm/model_executor/model_loader/safetensors_reader.h"
#include "vllm/multimodal/qwen3vl_processor.h"
#include "../gguf_builder.h"
Expand Down Expand Up @@ -83,6 +83,8 @@ using vllm::ParseMiniMaxH3DitParams;

namespace {

namespace fs = std::filesystem;

// ---------------------------------------------------------------------------
// H3Rand — the exact mirror of the generator's deterministic stream
// (scripts/gen-minimax-h3-goldens.py :: h3_rand). A per-tensor FNV-1a seed plus a
Expand Down Expand Up @@ -560,7 +562,7 @@ std::map<std::string, std::string> WriteMiniMaxH3ShardedDit(
const std::set<std::string>& omit_payload = {}) {
REQUIRE(num_shards > 0);
REQUIRE(entries.size() >= num_shards);
::mkdir(dir.c_str(), 0755);
fs::create_directories(dir);

std::map<std::string, std::string> weight_map;
std::vector<std::vector<H3StEntry>> per_shard(num_shards);
Expand Down Expand Up @@ -593,7 +595,7 @@ std::map<std::string, std::string> WriteMiniMaxH3ShardedDit(
uint64_t WriteMiniMaxH3SparseShardedRelease(const std::vector<vllm::MiniMaxH3TensorSpec>& specs,
const std::string& dir, size_t num_shards) {
REQUIRE(num_shards > 0);
::mkdir(dir.c_str(), 0755);
fs::create_directories(dir);
std::vector<std::vector<const vllm::MiniMaxH3TensorSpec*>> per_shard(num_shards);
std::map<std::string, std::string> weight_map;
for (size_t i = 0; i < specs.size(); ++i) {
Expand Down Expand Up @@ -635,7 +637,10 @@ uint64_t WriteMiniMaxH3SparseShardedRelease(const std::vector<vllm::MiniMaxH3Ten
std::fwrite(header.data(), 1, header.size(), fh);
std::fflush(fh);
// The payload is a HOLE: declared in full, allocated not at all.
REQUIRE(::ftruncate(fileno(fh), static_cast<off_t>(sizeof(n) + header.size() + offset)) == 0);
const auto declared_size =
static_cast<std::uint64_t>(sizeof(n) + header.size() + offset);
REQUIRE(vllm::support::TruncateFile(
vllm::support::FileDescriptorFromFile(fh), declared_size));
std::fclose(fh);
declared += offset;
}
Expand All @@ -656,7 +661,8 @@ void RemoveShardedDit(const std::string& dir, size_t num_shards) {
std::remove((dir + "/" + ShardFileName(s, num_shards)).c_str());
}
std::remove((dir + "/model.safetensors.index.json").c_str());
::rmdir(dir.c_str());
std::error_code ec;
fs::remove(dir, ec);
}

} // namespace
Expand Down
Loading