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
10 changes: 5 additions & 5 deletions hrx-integration/hrx-release.env
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

# Pinned HRX (amdxdna) release consumed by the HRX runtime path.
HRX_RELEASE_REPO="jtuyls/hrx"
HRX_RELEASE_TAG="flm-hrx-amdxdna-v2026.07.30"
HRX_RELEASE_TAG="flm-hrx-amdxdna-v2026.09.09"
# For Linux.
HRX_RELEASE_ASSET="hrx-amdxdna-2026.07.30-amdxdna-hal-native-rel-eb0b39f-linux-x86_64.tar.zst"
HRX_RELEASE_SHA256="661ed94051cc6ad04f53739b2df7a791aecb658bc435bd5a6ff3c46716696345"
HRX_RELEASE_ASSET="hrx-amdxdna-2026.09.09-amdxdna-hal-native-rel-effecac-linux-x86_64.tar.zst"
HRX_RELEASE_SHA256="16103aa1996e71f27ea355f707707ce09fbfd094b2e2989fd017f819d438a7e6"
# For Windows.
HRX_RELEASE_ASSET_WINDOWS="hrx-amdxdna-2026.07.30-amdxdna-hal-native-rel-eb0b39f-windows-x86_64.zip"
HRX_RELEASE_SHA256_WINDOWS="b3af74c9b393ce49ef71a3cbec2cbe92be6d40601858b41009d2bda57eadeb43"
HRX_RELEASE_ASSET_WINDOWS="hrx-amdxdna-2026.09.09-amdxdna-hal-native-rel-effecac-windows-x86_64.zip"
HRX_RELEASE_SHA256_WINDOWS="850a13affb927cdba5c0ba279681939c156154a28a917e5c2fb72cad38bf7cf2"
9 changes: 6 additions & 3 deletions src/common/AutoModel/modeling_hunyuan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@

/************ hunyuan-dense family **************/
Hunyuan::Hunyuan(flm_rt::device* npu_device_inst) : AutoModel(npu_device_inst, "Hunyuan") {
// the translator emits one short line per turn and the caller already has it
// from the stream / return value, so the raw dump would only double the log
this->log_raw_output = false;
// The translator streams one short line per turn, so echoing the
// "Model RAW Output:" dump only doubles the interactive log; keep it off by
// default. Diagnostics (qualification, numerical-match) anchor per-turn
// parsing on that marker, so FLM_LOG_RAW_OUTPUT=1 forces it back on for them
// without changing the interactive default.
this->log_raw_output = env_forces_raw_output();
// every turn either rewinds to the pinned prefix or clears, so the kv state
// the trailing eos forward exists to preserve is discarded either way
this->forward_on_eos = false;
Expand Down
14 changes: 13 additions & 1 deletion src/include/AutoModel/automodel.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
#include <string>
#include <type_traits>
#include <any>
#include <cstdlib>
#include <cstring>
#include "typedef.hpp"
#include "causal_lm.hpp"
#include "lm_config.hpp"
Expand Down Expand Up @@ -150,8 +152,18 @@ class AutoModel {
std::vector<int> checkpoint_his;
/// \brief dump the undecorated model output to stdout once a turn ends
/// \note on by default; models whose turns are short and driven in bulk (the
/// hunyuan translator) turn it off so the log is not doubled.
/// hunyuan translator) turn it off so the log is not doubled. Those
/// models should seed this from env_forces_raw_output() so diagnostics
/// can opt back in without changing the interactive default.
bool log_raw_output = true;
/// \brief whether FLM_LOG_RAW_OUTPUT forces the "Model RAW Output:" dump on
/// \note the qualification harness and numerical-match anchor per-turn parsing
/// on that marker, so they set FLM_LOG_RAW_OUTPUT=1 to re-enable it for
/// models that quiet it by default; unset/empty/"0" leave the default.
static bool env_forces_raw_output() {
const char* value = std::getenv("FLM_LOG_RAW_OUTPUT");
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
}
/// \brief run one more forward on the eos token once a turn ends
/// \note this keeps the kv cache aligned with token_history so a following
/// turn can append to it. Models that rewind or clear between turns
Expand Down
47 changes: 46 additions & 1 deletion src/include/buffer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <cstring>
#include <fstream>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
Expand All @@ -30,15 +31,25 @@
/// \note A copy (or mapping) does not duplicate the underlying memory; it only maps the pointer.
class bytes {
protected:
// HRX engines (IRON) use shared_ptr so copies keep the BO mapped. Unique_ptr
// copies drop ownership and leave data_ dangling across the .so boundary.
#if defined(FLM_USE_HRX)
std::shared_ptr<uint8_t[]> owned_data_;
#else
std::unique_ptr<uint8_t[]> owned_data_;
#endif
uint8_t* data_;
size_t size_;
bool is_owner_;
#ifdef FLM_DEVICE_BUFFER
bool is_bo_owner_;
flm_rt::bo* bo_;
#if defined(FLM_USE_HRX)
std::shared_ptr<flm_rt::bo> owned_bo_;
#else
std::unique_ptr<flm_rt::bo> owned_bo_;
#endif
#endif

public:
/// \brief constructor
Expand All @@ -52,11 +63,19 @@ class bytes {

/// \brief copy constructor
/// \param other the other bytes
#if defined(FLM_USE_HRX)
bytes(const bytes& other) : owned_data_(other.owned_data_), data_(other.data_), size_(other.size_), is_owner_(other.is_owner_)
#ifdef FLM_DEVICE_BUFFER
, is_bo_owner_(other.is_bo_owner_), bo_(other.bo_), owned_bo_(other.owned_bo_)
#endif
{}
#else
bytes(const bytes& other) : owned_data_(nullptr), data_(other.data_), size_(other.size_), is_owner_(false)
#ifdef FLM_DEVICE_BUFFER
, is_bo_owner_(false), bo_(other.bo_), owned_bo_(nullptr)
#endif
{}
#endif

/// \brief move constructor
/// \param other the other bytes
Expand Down Expand Up @@ -86,7 +105,11 @@ class bytes {
{
if (size > 0 && size < 8ull * 1024 * 1024 * 1024){
try {
#if defined(FLM_USE_HRX)
owned_data_ = std::shared_ptr<uint8_t[]>(new uint8_t[size]());
#else
owned_data_ = std::make_unique<uint8_t[]>(size);
#endif
}
catch (const std::bad_alloc& e) {
throw std::runtime_error(std::string("Failed to allocate bytes of size ") + std::to_string(size) + ": " + e.what());
Expand Down Expand Up @@ -128,10 +151,14 @@ class bytes {
throw std::runtime_error("Invalid size for bytes allocation");
}
size_t alignment = 1024 * 1024;
int padded_size = (size + alignment - 1) / alignment * alignment; // 1MB alignment
size_t padded_size = (size + alignment - 1) / alignment * alignment; // 1MB alignment

try {
#if defined(FLM_USE_HRX)
owned_bo_ = std::make_shared<flm_rt::ext::bo>(device, padded_size);
#else
owned_bo_ = std::make_unique<flm_rt::ext::bo>(device, padded_size);
#endif
}
catch (const std::exception& e) {
throw std::runtime_error(std::string("Failed to allocate flm_rt::ext::bo: ") + e.what());
Expand Down Expand Up @@ -169,6 +196,17 @@ class bytes {
/// \param other the other bytes
bytes& operator=(const bytes& other) {
if (this != &other) {
#if defined(FLM_USE_HRX)
owned_data_ = other.owned_data_;
data_ = other.data_;
size_ = other.size_;
is_owner_ = other.is_owner_;
#ifdef FLM_DEVICE_BUFFER
owned_bo_ = other.owned_bo_;
is_bo_owner_ = other.is_bo_owner_;
bo_ = other.bo_;
#endif
#else
if (is_owner_){
owned_data_.reset();
}
Expand All @@ -181,6 +219,7 @@ class bytes {
}
is_bo_owner_ = false;
bo_ = other.bo_;
#endif
#endif
}
return *this;
Expand Down Expand Up @@ -257,7 +296,11 @@ class bytes {
throw std::runtime_error("Cannot resize to zero size");
}
try {
#if defined(FLM_USE_HRX)
owned_data_.reset(new uint8_t[new_size]());
#else
owned_data_.reset(new uint8_t[new_size]);
#endif
}
catch (const std::bad_alloc& e) {
throw std::runtime_error(std::string("Failed to allocate bytes of size ") + std::to_string(new_size) + ": " + e.what());
Expand Down Expand Up @@ -302,6 +345,8 @@ class bytes {
/// \return the is bo owner
bool is_bo_owner() const { return is_bo_owner_; }

bool has_bo() const { return bo_ != nullptr; }

/// \brief sync to device (host writes -> device)
#if defined(FLM_USE_HRX)
void sync_to_device() { assert(bo_); bo_->flush(); }
Expand Down
97 changes: 88 additions & 9 deletions src/include/hrx_cpp/hrx_cpp.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,32 @@ inline bool hrx_report(hrx_status_t s, const char* where) {
// Executable cache: build one HRX XADX executable per distinct executable
// identity (xclbin + control program + host patch table) and resolve its export
// ordinal once.
//
// Ownership / lifetime (context-exhaustion fix)
// ---------------------------------------------------------------------------
// Every executable is created with HRX_AMDXDNA_CONTEXT_MODE_CREATE, i.e. it is
// backed by its own NPU hardware context. That pool is small: on the Windows
// MCDM path the 314 driver runs out after enough distinct executables and
// hrx_stream_wait then fails with
// D3DKMTCreateContextVirtual failed with 0xc01e0009
// (the Linux amdxdna path fails the analogous BO allocation with errno 11 /
// EAGAIN). A single long generation stays bounded because decode revisits the
// same control sequences, but a `flm serve` sweep that loads many models used
// to leak every model's executables forever: the cache was a process-wide map
// that was never pruned on model unload, so contexts accumulated across the
// sweep until the driver was exhausted -- exactly the "running out of contexts"
// failure seen at gemma4-e4b after many prior models.
//
// Executables are therefore reference counted. Each npu_app that dispatches a
// given (xclbin, ctrl_seq) holds exactly one reference for it; when the owning
// model's engine objects are destroyed (model unload / model switch in serve)
// the npu_app releases its references and, once the last owner drops, the
// executable is destroyed and its hardware context returned to the driver.
// ---------------------------------------------------------------------------
struct CachedExe {
hrx_executable_t exe = nullptr;
uint32_t ord = 0;
size_t refs = 0; // number of npu_app owners; released when it reaches 0
};

inline void append_key_bytes(std::string& key, const void* data, size_t byte_count) {
Expand All @@ -127,20 +149,37 @@ inline void append_key_bytes(std::string& key, const void* data, size_t byte_cou
}
}

inline hrx_executable_t build_or_get_executable(
const std::vector<uint8_t>& xclbin_bytes, const uint32_t* cc, size_t n,
uint32_t* ord_out) {
static std::mutex mu;
static std::unordered_map<std::string, CachedExe> cache;
inline std::mutex& exe_cache_mu() {
static std::mutex m;
return m;
}
inline std::unordered_map<std::string, CachedExe>& exe_cache() {
static std::unordered_map<std::string, CachedExe> c;
return c;
}
inline std::string make_exe_key(const std::vector<uint8_t>& xclbin_bytes,
const uint32_t* cc, size_t n) {
std::string key;
key.reserve(xclbin_bytes.size() + n * sizeof(uint32_t) +
2 * sizeof(uint64_t));
key.reserve(xclbin_bytes.size() + n * sizeof(uint32_t) + 2 * sizeof(uint64_t));
append_key_bytes(key, xclbin_bytes.data(), xclbin_bytes.size());
append_key_bytes(key, cc, n * sizeof(uint32_t));
std::lock_guard<std::mutex> lk(mu);
return key;
}

// Build (or fetch the cached) executable for (xclbin, cc). Does NOT change the
// reference count; callers manage ownership with retain_executable /
// release_executable below. Returns nullptr on create failure (never cached, so
// a later call can retry instead of dispatching a silent no-op).
inline hrx_executable_t lookup_or_build_executable(
const std::vector<uint8_t>& xclbin_bytes, const uint32_t* cc, size_t n,
uint32_t* ord_out, std::string* key_out) {
std::string key = make_exe_key(xclbin_bytes, cc, n);
std::lock_guard<std::mutex> lk(exe_cache_mu());
auto& cache = exe_cache();
auto it = cache.find(key);
if (it != cache.end()) {
if (ord_out) *ord_out = it->second.ord;
if (key_out) *key_out = std::move(key);
return it->second.exe;
}
hrx_const_byte_span_t xclbin = {xclbin_bytes.data(), xclbin_bytes.size()};
Expand Down Expand Up @@ -176,11 +215,51 @@ inline hrx_executable_t build_or_get_executable(
hrx_executable_release(exe);
exe = nullptr;
}
cache.emplace(std::move(key), CachedExe{exe, ord});
if (exe) {
cache.emplace(key, CachedExe{exe, ord, 0});
}
if (ord_out) *ord_out = ord;
if (key_out) *key_out = std::move(key);
return exe;
}

// Add one owner reference to the cached executable identified by key.
inline void retain_executable(const std::string& key) {
std::lock_guard<std::mutex> lk(exe_cache_mu());
auto& cache = exe_cache();
auto it = cache.find(key);
if (it != cache.end()) ++it->second.refs;
}

// Drop one owner reference; when the last owner drops the executable is
// destroyed and its hardware context is returned to the driver.
inline void release_executable(const std::string& key) {
hrx_executable_t to_free = nullptr;
{
std::lock_guard<std::mutex> lk(exe_cache_mu());
auto& cache = exe_cache();
auto it = cache.find(key);
if (it == cache.end()) return;
if (it->second.refs > 0) --it->second.refs;
if (it->second.refs == 0) {
to_free = it->second.exe;
cache.erase(it);
}
}
if (to_free) {
hrx_executable_release(to_free);
}
}

// Backward-compatible entry point (no ownership tracking). Retained for any
// caller that does not manage executable lifetime; prefer the
// lookup_or_build/retain/release trio used by npu_app.
inline hrx_executable_t build_or_get_executable(
const std::vector<uint8_t>& xclbin_bytes, const uint32_t* cc, size_t n,
uint32_t* ord_out) {
return lookup_or_build_executable(xclbin_bytes, cc, n, ord_out, nullptr);
}

// ---------------------------------------------------------------------------
// uuid / xclbin / device / hw_context
// ---------------------------------------------------------------------------
Expand Down
Loading
Loading