mirror of
https://github.com/immich-app/immich.git
synced 2025-01-02 12:48:35 +02:00
feat(ml): ARMNN acceleration (#5667)
* feat(ml): ARMNN acceleration for CLIP * wrap ANN as ONNX-Session * strict typing * normalize ARMNN CLIP embedding * mutex to handle concurrent execution * make inputs contiguous * fine-grained locking; concurrent network execution --------- Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
This commit is contained in:
parent
29747437f6
commit
753292956e
11
docker/mlaccel-armnn.yml
Normal file
11
docker/mlaccel-armnn.yml
Normal file
@ -0,0 +1,11 @@
|
||||
version: "3.8"
|
||||
|
||||
# ML acceleration on supported Mali ARM GPUs using ARM-NN
|
||||
|
||||
services:
|
||||
mlaccel:
|
||||
devices:
|
||||
- /dev/mali0:/dev/mali0
|
||||
volumes:
|
||||
- /lib/firmware/mali_csffw.bin:/lib/firmware/mali_csffw.bin:ro # Mali firmware for your chipset (not always required depending on the driver)
|
||||
- /usr/lib/libmali.so:/usr/lib/libmali.so:ro # Mali driver for your chipset (always required)
|
@ -13,17 +13,40 @@ ENV VIRTUAL_ENV="/opt/venv" PATH="/opt/venv/bin:${PATH}"
|
||||
COPY poetry.lock pyproject.toml ./
|
||||
RUN poetry install --sync --no-interaction --no-ansi --no-root --only main
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:8f64a67710f3d981cf3008d6f9f1dbe61accd7927f165f4e37ea3f8b883ccc3f
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ENV ARMNN_PATH=/opt/armnn
|
||||
COPY ann /opt/ann
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||
mkdir /opt/armnn && \
|
||||
curl -SL "https://github.com/ARM-software/armnn/releases/download/v23.11/ArmNN-linux-aarch64.tar.gz" | tar -zx -C /opt/armnn && \
|
||||
cd /opt/ann && \
|
||||
sh build.sh; \
|
||||
else \
|
||||
mkdir /opt/armnn; \
|
||||
fi
|
||||
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:8f64a67710f3d981cf3008d6f9f1dbe61accd7927f165f4e37ea3f8b883ccc3f
|
||||
ARG TARGETPLATFORM
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends tini libmimalloc2.0 && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||
apt-get update && apt-get install -y --no-install-recommends ocl-icd-libopencl1 mesa-opencl-icd && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
mkdir --parents /etc/OpenCL/vendors && \
|
||||
echo "/usr/lib/libmali.so" > /etc/OpenCL/vendors/mali.icd && \
|
||||
mkdir /opt/armnn; \
|
||||
fi
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
TRANSFORMERS_CACHE=/cache \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PATH="/opt/venv/bin:$PATH" \
|
||||
PYTHONPATH=/usr/src
|
||||
PYTHONPATH=/usr/src \
|
||||
LD_LIBRARY_PATH=/opt/armnn
|
||||
|
||||
# prevent core dumps
|
||||
RUN echo "hard core 0" >> /etc/security/limits.conf && \
|
||||
@ -31,7 +54,10 @@ RUN echo "hard core 0" >> /etc/security/limits.conf && \
|
||||
echo 'ulimit -S -c 0 > /dev/null 2>&1' >> /etc/profile
|
||||
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
COPY --from=builder /opt/armnn/libarmnn.so.?? /opt/armnn/libarmnnOnnxParser.so.?? /opt/armnn/libarmnnDeserializer.so.?? /opt/armnn/libarmnnTfLiteParser.so.?? /opt/armnn/libprotobuf.so.?.??.?.? /opt/ann/libann.s[o] /opt/ann/build.sh /opt/armnn
|
||||
COPY ann/ann.py /usr/src/ann/ann.py
|
||||
COPY start.sh log_conf.json ./
|
||||
COPY app .
|
||||
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
CMD ["./start.sh"]
|
||||
|
1
machine-learning/ann/__init__.py
Normal file
1
machine-learning/ann/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .ann import Ann, is_available
|
281
machine-learning/ann/ann.cpp
Normal file
281
machine-learning/ann/ann.cpp
Normal file
@ -0,0 +1,281 @@
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
|
||||
#include "armnn/IRuntime.hpp"
|
||||
#include "armnn/INetwork.hpp"
|
||||
#include "armnn/Types.hpp"
|
||||
#include "armnnDeserializer/IDeserializer.hpp"
|
||||
#include "armnnTfLiteParser/ITfLiteParser.hpp"
|
||||
#include "armnnOnnxParser/IOnnxParser.hpp"
|
||||
|
||||
using namespace armnn;
|
||||
|
||||
struct IOInfos
|
||||
{
|
||||
std::vector<BindingPointInfo> inputInfos;
|
||||
std::vector<BindingPointInfo> outputInfos;
|
||||
};
|
||||
|
||||
// from https://rigtorp.se/spinlock/
|
||||
struct SpinLock
|
||||
{
|
||||
std::atomic<bool> lock_ = {false};
|
||||
|
||||
void lock()
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
if (!lock_.exchange(true, std::memory_order_acquire))
|
||||
{
|
||||
break;
|
||||
}
|
||||
while (lock_.load(std::memory_order_relaxed))
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
void unlock() { lock_.store(false, std::memory_order_release); }
|
||||
};
|
||||
|
||||
class Ann
|
||||
{
|
||||
|
||||
public:
|
||||
int load(const char *modelPath,
|
||||
bool fastMath,
|
||||
bool fp16,
|
||||
bool saveCachedNetwork,
|
||||
const char *cachedNetworkPath)
|
||||
{
|
||||
INetworkPtr network = loadModel(modelPath);
|
||||
IOptimizedNetworkPtr optNet = OptimizeNetwork(network.get(), fastMath, fp16, saveCachedNetwork, cachedNetworkPath);
|
||||
const IOInfos infos = getIOInfos(optNet.get());
|
||||
NetworkId netId;
|
||||
mutex.lock();
|
||||
Status status = runtime->LoadNetwork(netId, std::move(optNet));
|
||||
mutex.unlock();
|
||||
if (status != Status::Success)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
spinLock.lock();
|
||||
ioInfos[netId] = infos;
|
||||
mutexes.emplace(netId, std::make_unique<std::mutex>());
|
||||
spinLock.unlock();
|
||||
return netId;
|
||||
}
|
||||
|
||||
void execute(NetworkId netId, const void **inputData, void **outputData)
|
||||
{
|
||||
spinLock.lock();
|
||||
const IOInfos *infos = &ioInfos[netId];
|
||||
auto m = mutexes[netId].get();
|
||||
spinLock.unlock();
|
||||
InputTensors inputTensors;
|
||||
inputTensors.reserve(infos->inputInfos.size());
|
||||
size_t i = 0;
|
||||
for (const BindingPointInfo &info : infos->inputInfos)
|
||||
inputTensors.emplace_back(info.first, ConstTensor(info.second, inputData[i++]));
|
||||
OutputTensors outputTensors;
|
||||
outputTensors.reserve(infos->outputInfos.size());
|
||||
i = 0;
|
||||
for (const BindingPointInfo &info : infos->outputInfos)
|
||||
outputTensors.emplace_back(info.first, Tensor(info.second, outputData[i++]));
|
||||
m->lock();
|
||||
runtime->EnqueueWorkload(netId, inputTensors, outputTensors);
|
||||
m->unlock();
|
||||
}
|
||||
|
||||
void unload(NetworkId netId)
|
||||
{
|
||||
mutex.lock();
|
||||
runtime->UnloadNetwork(netId);
|
||||
mutex.unlock();
|
||||
}
|
||||
|
||||
int tensors(NetworkId netId, bool isInput = false)
|
||||
{
|
||||
spinLock.lock();
|
||||
const IOInfos *infos = &ioInfos[netId];
|
||||
spinLock.unlock();
|
||||
return (int)(isInput ? infos->inputInfos.size() : infos->outputInfos.size());
|
||||
}
|
||||
|
||||
unsigned long shape(NetworkId netId, bool isInput = false, int index = 0)
|
||||
{
|
||||
spinLock.lock();
|
||||
const IOInfos *infos = &ioInfos[netId];
|
||||
spinLock.unlock();
|
||||
const TensorShape shape = (isInput ? infos->inputInfos : infos->outputInfos)[index].second.GetShape();
|
||||
unsigned long s = 0;
|
||||
for (unsigned int d = 0; d < shape.GetNumDimensions(); d++)
|
||||
s |= ((unsigned long)shape[d]) << (d * 16); // stores up to 4 16-bit values in a 64-bit value
|
||||
return s;
|
||||
}
|
||||
|
||||
Ann(int tuningLevel, const char *tuningFile)
|
||||
{
|
||||
IRuntime::CreationOptions runtimeOptions;
|
||||
BackendOptions backendOptions{"GpuAcc",
|
||||
{
|
||||
{"TuningLevel", tuningLevel},
|
||||
{"MemoryOptimizerStrategy", "ConstantMemoryStrategy"}, // SingleAxisPriorityList or ConstantMemoryStrategy
|
||||
}};
|
||||
if (tuningFile)
|
||||
backendOptions.AddOption({"TuningFile", tuningFile});
|
||||
runtimeOptions.m_BackendOptions.emplace_back(backendOptions);
|
||||
runtime = IRuntime::CreateRaw(runtimeOptions);
|
||||
};
|
||||
~Ann()
|
||||
{
|
||||
IRuntime::Destroy(runtime);
|
||||
};
|
||||
|
||||
private:
|
||||
INetworkPtr loadModel(const char *modelPath)
|
||||
{
|
||||
const auto path = std::string(modelPath);
|
||||
if (path.rfind(".tflite") == path.length() - 7) // endsWith()
|
||||
{
|
||||
auto parser = armnnTfLiteParser::ITfLiteParser::CreateRaw();
|
||||
return parser->CreateNetworkFromBinaryFile(modelPath);
|
||||
}
|
||||
else if (path.rfind(".onnx") == path.length() - 5) // endsWith()
|
||||
{
|
||||
auto parser = armnnOnnxParser::IOnnxParser::CreateRaw();
|
||||
return parser->CreateNetworkFromBinaryFile(modelPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::ifstream ifs(path, std::ifstream::in | std::ifstream::binary);
|
||||
auto parser = armnnDeserializer::IDeserializer::CreateRaw();
|
||||
return parser->CreateNetworkFromBinary(ifs);
|
||||
}
|
||||
}
|
||||
|
||||
static BindingPointInfo getInputTensorInfo(LayerBindingId inputBindingId, TensorInfo info)
|
||||
{
|
||||
const auto newInfo = TensorInfo{info.GetShape(), info.GetDataType(),
|
||||
info.GetQuantizationScale(),
|
||||
info.GetQuantizationOffset(),
|
||||
true};
|
||||
return {inputBindingId, newInfo};
|
||||
}
|
||||
|
||||
IOptimizedNetworkPtr OptimizeNetwork(INetwork *network, bool fastMath, bool fp16, bool saveCachedNetwork, const char *cachedNetworkPath)
|
||||
{
|
||||
const bool allowExpandedDims = false;
|
||||
const ShapeInferenceMethod shapeInferenceMethod = ShapeInferenceMethod::ValidateOnly;
|
||||
|
||||
OptimizerOptionsOpaque options;
|
||||
options.SetReduceFp32ToFp16(fp16);
|
||||
options.SetShapeInferenceMethod(shapeInferenceMethod);
|
||||
options.SetAllowExpandedDims(allowExpandedDims);
|
||||
|
||||
BackendOptions gpuAcc("GpuAcc", {{"FastMathEnabled", fastMath}});
|
||||
if (cachedNetworkPath)
|
||||
{
|
||||
gpuAcc.AddOption({"SaveCachedNetwork", saveCachedNetwork});
|
||||
gpuAcc.AddOption({"CachedNetworkFilePath", cachedNetworkPath});
|
||||
}
|
||||
options.AddModelOption(gpuAcc);
|
||||
|
||||
// No point in using ARMNN for CPU, use ONNX (quantized) instead.
|
||||
// BackendOptions cpuAcc("CpuAcc",
|
||||
// {
|
||||
// {"FastMathEnabled", fastMath},
|
||||
// {"NumberOfThreads", 0},
|
||||
// });
|
||||
// options.AddModelOption(cpuAcc);
|
||||
|
||||
BackendOptions allowExDimOpt("AllowExpandedDims",
|
||||
{{"AllowExpandedDims", allowExpandedDims}});
|
||||
options.AddModelOption(allowExDimOpt);
|
||||
BackendOptions shapeInferOpt("ShapeInferenceMethod",
|
||||
{{"InferAndValidate", shapeInferenceMethod == ShapeInferenceMethod::InferAndValidate}});
|
||||
options.AddModelOption(shapeInferOpt);
|
||||
|
||||
std::vector<BackendId> backends = {
|
||||
BackendId("GpuAcc"),
|
||||
// BackendId("CpuAcc"),
|
||||
// BackendId("CpuRef"),
|
||||
};
|
||||
return Optimize(*network, backends, runtime->GetDeviceSpec(), options);
|
||||
}
|
||||
|
||||
IOInfos getIOInfos(IOptimizedNetwork *optNet)
|
||||
{
|
||||
struct InfoStrategy : IStrategy
|
||||
{
|
||||
void ExecuteStrategy(const IConnectableLayer *layer,
|
||||
const BaseDescriptor &descriptor,
|
||||
const std::vector<ConstTensor> &constants,
|
||||
const char *name,
|
||||
const LayerBindingId id = 0) override
|
||||
{
|
||||
IgnoreUnused(descriptor, constants, id);
|
||||
const LayerType lt = layer->GetType();
|
||||
if (lt == LayerType::Input)
|
||||
ioInfos.inputInfos.push_back(getInputTensorInfo(id, layer->GetOutputSlot(0).GetTensorInfo()));
|
||||
else if (lt == LayerType::Output)
|
||||
ioInfos.outputInfos.push_back({id, layer->GetInputSlot(0).GetTensorInfo()});
|
||||
}
|
||||
IOInfos ioInfos;
|
||||
};
|
||||
|
||||
InfoStrategy infoStrategy;
|
||||
optNet->ExecuteStrategy(infoStrategy);
|
||||
return infoStrategy.ioInfos;
|
||||
}
|
||||
|
||||
IRuntime *runtime;
|
||||
std::map<NetworkId, IOInfos> ioInfos;
|
||||
std::map<NetworkId, std::unique_ptr<std::mutex>> mutexes; // mutex per network to not execute the same the same network concurrently
|
||||
std::mutex mutex; // global mutex for load/unload calls to the runtime
|
||||
SpinLock spinLock; // fast spin lock to guard access to the ioInfos and mutexes maps
|
||||
};
|
||||
|
||||
extern "C" void *init(int logLevel, int tuningLevel, const char *tuningFile)
|
||||
{
|
||||
LogSeverity level = static_cast<LogSeverity>(logLevel);
|
||||
ConfigureLogging(true, true, level);
|
||||
|
||||
Ann *ann = new Ann(tuningLevel, tuningFile);
|
||||
return ann;
|
||||
}
|
||||
|
||||
extern "C" void destroy(void *ann)
|
||||
{
|
||||
delete ((Ann *)ann);
|
||||
}
|
||||
|
||||
extern "C" int load(void *ann,
|
||||
const char *path,
|
||||
bool fastMath,
|
||||
bool fp16,
|
||||
bool saveCachedNetwork,
|
||||
const char *cachedNetworkPath)
|
||||
{
|
||||
return ((Ann *)ann)->load(path, fastMath, fp16, saveCachedNetwork, cachedNetworkPath);
|
||||
}
|
||||
|
||||
extern "C" void unload(void *ann, NetworkId netId)
|
||||
{
|
||||
((Ann *)ann)->unload(netId);
|
||||
}
|
||||
|
||||
extern "C" void execute(void *ann, NetworkId netId, const void **inputData, void **outputData)
|
||||
{
|
||||
((Ann *)ann)->execute(netId, inputData, outputData);
|
||||
}
|
||||
|
||||
extern "C" unsigned long shape(void *ann, NetworkId netId, bool isInput, int index)
|
||||
{
|
||||
return ((Ann *)ann)->shape(netId, isInput, index);
|
||||
}
|
||||
|
||||
extern "C" int tensors(void *ann, NetworkId netId, bool isInput)
|
||||
{
|
||||
return ((Ann *)ann)->tensors(netId, isInput);
|
||||
}
|
162
machine-learning/ann/ann.py
Normal file
162
machine-learning/ann/ann.py
Normal file
@ -0,0 +1,162 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ctypes import CDLL, Array, c_bool, c_char_p, c_int, c_ulong, c_void_p
|
||||
from os.path import exists
|
||||
from typing import Any, Generic, Protocol, Type, TypeVar
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from app.config import log
|
||||
|
||||
try:
|
||||
CDLL("libmali.so") # fail if libmali.so is not mounted into container
|
||||
libann = CDLL("libann.so")
|
||||
libann.init.argtypes = c_int, c_int, c_char_p
|
||||
libann.init.restype = c_void_p
|
||||
libann.load.argtypes = c_void_p, c_char_p, c_bool, c_bool, c_bool, c_char_p
|
||||
libann.load.restype = c_int
|
||||
libann.execute.argtypes = c_void_p, c_int, Array[c_void_p], Array[c_void_p]
|
||||
libann.unload.argtypes = c_void_p, c_int
|
||||
libann.destroy.argtypes = (c_void_p,)
|
||||
libann.shape.argtypes = c_void_p, c_int, c_bool, c_int
|
||||
libann.shape.restype = c_ulong
|
||||
libann.tensors.argtypes = c_void_p, c_int, c_bool
|
||||
libann.tensors.restype = c_int
|
||||
is_available = True
|
||||
except OSError as e:
|
||||
log.debug("Could not load ANN shared libraries, using ONNX: %s", e)
|
||||
is_available = False
|
||||
|
||||
T = TypeVar("T", covariant=True)
|
||||
|
||||
|
||||
class Newable(Protocol[T]):
|
||||
def new(self) -> None:
|
||||
...
|
||||
|
||||
|
||||
class _Singleton(type, Newable[T]):
|
||||
_instances: dict[_Singleton[T], Newable[T]] = {}
|
||||
|
||||
def __call__(cls, *args: Any, **kwargs: Any) -> Newable[T]:
|
||||
if cls not in cls._instances:
|
||||
obj: Newable[T] = super(_Singleton, cls).__call__(*args, **kwargs)
|
||||
cls._instances[cls] = obj
|
||||
else:
|
||||
obj = cls._instances[cls]
|
||||
obj.new()
|
||||
return obj
|
||||
|
||||
|
||||
class Ann(metaclass=_Singleton):
|
||||
def __init__(self, log_level: int = 3, tuning_level: int = 1, tuning_file: str | None = None) -> None:
|
||||
if not is_available:
|
||||
raise RuntimeError("libann is not available!")
|
||||
if tuning_file and not exists(tuning_file):
|
||||
raise ValueError("tuning_file must point to an existing (possibly empty) file!")
|
||||
if tuning_level == 0 and tuning_file is None:
|
||||
raise ValueError("tuning_level == 0 reads existing tuning information and requires a tuning_file")
|
||||
if tuning_level < 0 or tuning_level > 3:
|
||||
raise ValueError("tuning_level must be 0 (load from tuning_file), 1, 2 or 3.")
|
||||
if log_level < 0 or log_level > 5:
|
||||
raise ValueError("log_level must be 0 (trace), 1 (debug), 2 (info), 3 (warning), 4 (error) or 5 (fatal)")
|
||||
self.log_level = log_level
|
||||
self.tuning_level = tuning_level
|
||||
self.tuning_file = tuning_file
|
||||
self.output_shapes: dict[int, tuple[tuple[int], ...]] = {}
|
||||
self.input_shapes: dict[int, tuple[tuple[int], ...]] = {}
|
||||
self.ann: int | None = None
|
||||
self.new()
|
||||
|
||||
def new(self) -> None:
|
||||
if self.ann is None:
|
||||
self.ann = libann.init(
|
||||
self.log_level,
|
||||
self.tuning_level,
|
||||
self.tuning_file.encode() if self.tuning_file is not None else None,
|
||||
)
|
||||
self.ref_count = 0
|
||||
|
||||
self.ref_count += 1
|
||||
|
||||
def destroy(self) -> None:
|
||||
self.ref_count -= 1
|
||||
if self.ref_count <= 0 and self.ann is not None:
|
||||
libann.destroy(self.ann)
|
||||
self.ann = None
|
||||
|
||||
def __del__(self) -> None:
|
||||
if self.ann is not None:
|
||||
libann.destroy(self.ann)
|
||||
self.ann = None
|
||||
|
||||
def load(
|
||||
self,
|
||||
model_path: str,
|
||||
fast_math: bool = True,
|
||||
fp16: bool = False,
|
||||
save_cached_network: bool = False,
|
||||
cached_network_path: str | None = None,
|
||||
) -> int:
|
||||
if not model_path.endswith((".armnn", ".tflite", ".onnx")):
|
||||
raise ValueError("model_path must be a file with extension .armnn, .tflite or .onnx")
|
||||
if not exists(model_path):
|
||||
raise ValueError("model_path must point to an existing file!")
|
||||
if cached_network_path is not None and not exists(cached_network_path):
|
||||
raise ValueError("cached_network_path must point to an existing (possibly empty) file!")
|
||||
if save_cached_network and cached_network_path is None:
|
||||
raise ValueError("save_cached_network is True, cached_network_path must be specified!")
|
||||
net_id: int = libann.load(
|
||||
self.ann,
|
||||
model_path.encode(),
|
||||
fast_math,
|
||||
fp16,
|
||||
save_cached_network,
|
||||
cached_network_path.encode() if cached_network_path is not None else None,
|
||||
)
|
||||
|
||||
self.input_shapes[net_id] = tuple(
|
||||
self.shape(net_id, input=True, index=i) for i in range(self.tensors(net_id, input=True))
|
||||
)
|
||||
self.output_shapes[net_id] = tuple(
|
||||
self.shape(net_id, input=False, index=i) for i in range(self.tensors(net_id, input=False))
|
||||
)
|
||||
return net_id
|
||||
|
||||
def unload(self, network_id: int) -> None:
|
||||
libann.unload(self.ann, network_id)
|
||||
del self.output_shapes[network_id]
|
||||
|
||||
def execute(self, network_id: int, input_tensors: list[NDArray[np.float32]]) -> list[NDArray[np.float32]]:
|
||||
if not isinstance(input_tensors, list):
|
||||
raise ValueError("input_tensors needs to be a list!")
|
||||
net_input_shapes = self.input_shapes[network_id]
|
||||
if len(input_tensors) != len(net_input_shapes):
|
||||
raise ValueError(f"input_tensors lengths {len(input_tensors)} != network inputs {len(net_input_shapes)}")
|
||||
for net_input_shape, input_tensor in zip(net_input_shapes, input_tensors):
|
||||
if net_input_shape != input_tensor.shape:
|
||||
raise ValueError(f"input_tensor shape {input_tensor.shape} != network input shape {net_input_shape}")
|
||||
if not input_tensor.flags.c_contiguous:
|
||||
raise ValueError("input_tensors must be c_contiguous numpy ndarrays")
|
||||
output_tensors: list[NDArray[np.float32]] = [
|
||||
np.ndarray(s, dtype=np.float32) for s in self.output_shapes[network_id]
|
||||
]
|
||||
input_type = c_void_p * len(input_tensors)
|
||||
inputs = input_type(*[t.ctypes.data_as(c_void_p) for t in input_tensors])
|
||||
output_type = c_void_p * len(output_tensors)
|
||||
outputs = output_type(*[t.ctypes.data_as(c_void_p) for t in output_tensors])
|
||||
libann.execute(self.ann, network_id, inputs, outputs)
|
||||
return output_tensors
|
||||
|
||||
def shape(self, network_id: int, input: bool = False, index: int = 0) -> tuple[int]:
|
||||
s = libann.shape(self.ann, network_id, input, index)
|
||||
a = []
|
||||
while s != 0:
|
||||
a.append(s & 0xFFFF)
|
||||
s >>= 16
|
||||
return tuple(a)
|
||||
|
||||
def tensors(self, network_id: int, input: bool = False) -> int:
|
||||
tensors: int = libann.tensors(self.ann, network_id, input)
|
||||
return tensors
|
1
machine-learning/ann/build.sh
Normal file
1
machine-learning/ann/build.sh
Normal file
@ -0,0 +1 @@
|
||||
g++ -shared -O3 -o libann.so -fuse-ld=gold -std=c++17 -I$ARMNN_PATH/include -larmnn -larmnnDeserializer -larmnnTfLiteParser -larmnnOnnxParser -L$ARMNN_PATH ann.cpp
|
2
machine-learning/ann/export/.gitignore
vendored
Normal file
2
machine-learning/ann/export/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
armnn*
|
||||
output/
|
4
machine-learning/ann/export/build-converter.sh
Executable file
4
machine-learning/ann/export/build-converter.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
cd armnn-23.11/
|
||||
g++ -o ../armnnconverter -O1 -DARMNN_ONNX_PARSER -DARMNN_SERIALIZER -DARMNN_TF_LITE_PARSER -fuse-ld=gold -std=c++17 -Iinclude -Isrc/armnnUtils -Ithird-party -larmnn -larmnnDeserializer -larmnnTfLiteParser -larmnnOnnxParser -larmnnSerializer -L../armnn src/armnnConverter/ArmnnConverter.cpp
|
8
machine-learning/ann/export/download-armnn.sh
Executable file
8
machine-learning/ann/export/download-armnn.sh
Executable file
@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
# binaries
|
||||
mkdir armnn
|
||||
curl -SL "https://github.com/ARM-software/armnn/releases/download/v23.11/ArmNN-linux-x86_64.tar.gz" | tar -zx -C armnn
|
||||
|
||||
# source to build ArmnnConverter
|
||||
curl -SL "https://github.com/ARM-software/armnn/archive/refs/tags/v23.11.tar.gz" | tar -zx
|
201
machine-learning/ann/export/env.yaml
Normal file
201
machine-learning/ann/export/env.yaml
Normal file
@ -0,0 +1,201 @@
|
||||
name: annexport
|
||||
channels:
|
||||
- pytorch
|
||||
- nvidia
|
||||
- conda-forge
|
||||
dependencies:
|
||||
- _libgcc_mutex=0.1=conda_forge
|
||||
- _openmp_mutex=4.5=2_kmp_llvm
|
||||
- aiohttp=3.9.1=py310h2372a71_0
|
||||
- aiosignal=1.3.1=pyhd8ed1ab_0
|
||||
- arpack=3.8.0=nompi_h0baa96a_101
|
||||
- async-timeout=4.0.3=pyhd8ed1ab_0
|
||||
- attrs=23.1.0=pyh71513ae_1
|
||||
- aws-c-auth=0.7.3=h28f7589_1
|
||||
- aws-c-cal=0.6.1=hc309b26_1
|
||||
- aws-c-common=0.9.0=hd590300_0
|
||||
- aws-c-compression=0.2.17=h4d4d85c_2
|
||||
- aws-c-event-stream=0.3.1=h2e3709c_4
|
||||
- aws-c-http=0.7.11=h00aa349_4
|
||||
- aws-c-io=0.13.32=he9a53bd_1
|
||||
- aws-c-mqtt=0.9.3=hb447be9_1
|
||||
- aws-c-s3=0.3.14=hf3aad02_1
|
||||
- aws-c-sdkutils=0.1.12=h4d4d85c_1
|
||||
- aws-checksums=0.1.17=h4d4d85c_1
|
||||
- aws-crt-cpp=0.21.0=hb942446_5
|
||||
- aws-sdk-cpp=1.10.57=h85b1a90_19
|
||||
- blas=2.120=openblas
|
||||
- blas-devel=3.9.0=20_linux64_openblas
|
||||
- brotli-python=1.0.9=py310hd8f1fbe_9
|
||||
- bzip2=1.0.8=hd590300_5
|
||||
- c-ares=1.23.0=hd590300_0
|
||||
- ca-certificates=2023.11.17=hbcca054_0
|
||||
- certifi=2023.11.17=pyhd8ed1ab_0
|
||||
- charset-normalizer=3.3.2=pyhd8ed1ab_0
|
||||
- click=8.1.7=unix_pyh707e725_0
|
||||
- colorama=0.4.6=pyhd8ed1ab_0
|
||||
- coloredlogs=15.0.1=pyhd8ed1ab_3
|
||||
- cuda-cudart=11.7.99=0
|
||||
- cuda-cupti=11.7.101=0
|
||||
- cuda-libraries=11.7.1=0
|
||||
- cuda-nvrtc=11.7.99=0
|
||||
- cuda-nvtx=11.7.91=0
|
||||
- cuda-runtime=11.7.1=0
|
||||
- dataclasses=0.8=pyhc8e2a94_3
|
||||
- datasets=2.14.7=pyhd8ed1ab_0
|
||||
- dill=0.3.7=pyhd8ed1ab_0
|
||||
- filelock=3.13.1=pyhd8ed1ab_0
|
||||
- flatbuffers=23.5.26=h59595ed_1
|
||||
- freetype=2.12.1=h267a509_2
|
||||
- frozenlist=1.4.0=py310h2372a71_1
|
||||
- fsspec=2023.10.0=pyhca7485f_0
|
||||
- ftfy=6.1.3=pyhd8ed1ab_0
|
||||
- gflags=2.2.2=he1b5a44_1004
|
||||
- glog=0.6.0=h6f12383_0
|
||||
- glpk=5.0=h445213a_0
|
||||
- gmp=6.3.0=h59595ed_0
|
||||
- gmpy2=2.1.2=py310h3ec546c_1
|
||||
- huggingface_hub=0.17.3=pyhd8ed1ab_0
|
||||
- humanfriendly=10.0=pyhd8ed1ab_6
|
||||
- icu=73.2=h59595ed_0
|
||||
- idna=3.6=pyhd8ed1ab_0
|
||||
- importlib-metadata=7.0.0=pyha770c72_0
|
||||
- importlib_metadata=7.0.0=hd8ed1ab_0
|
||||
- joblib=1.3.2=pyhd8ed1ab_0
|
||||
- keyutils=1.6.1=h166bdaf_0
|
||||
- krb5=1.21.2=h659d440_0
|
||||
- lcms2=2.15=h7f713cb_2
|
||||
- ld_impl_linux-64=2.40=h41732ed_0
|
||||
- lerc=4.0.0=h27087fc_0
|
||||
- libabseil=20230125.3=cxx17_h59595ed_0
|
||||
- libarrow=12.0.1=hb87d912_8_cpu
|
||||
- libblas=3.9.0=20_linux64_openblas
|
||||
- libbrotlicommon=1.0.9=h166bdaf_9
|
||||
- libbrotlidec=1.0.9=h166bdaf_9
|
||||
- libbrotlienc=1.0.9=h166bdaf_9
|
||||
- libcblas=3.9.0=20_linux64_openblas
|
||||
- libcrc32c=1.1.2=h9c3ff4c_0
|
||||
- libcublas=11.10.3.66=0
|
||||
- libcufft=10.7.2.124=h4fbf590_0
|
||||
- libcufile=1.8.1.2=0
|
||||
- libcurand=10.3.4.101=0
|
||||
- libcurl=8.5.0=hca28451_0
|
||||
- libcusolver=11.4.0.1=0
|
||||
- libcusparse=11.7.4.91=0
|
||||
- libdeflate=1.19=hd590300_0
|
||||
- libedit=3.1.20191231=he28a2e2_2
|
||||
- libev=4.33=hd590300_2
|
||||
- libevent=2.1.12=hf998b51_1
|
||||
- libffi=3.4.2=h7f98852_5
|
||||
- libgcc-ng=13.2.0=h807b86a_3
|
||||
- libgfortran-ng=13.2.0=h69a702a_3
|
||||
- libgfortran5=13.2.0=ha4646dd_3
|
||||
- libgoogle-cloud=2.12.0=hac9eb74_1
|
||||
- libgrpc=1.54.3=hb20ce57_0
|
||||
- libhwloc=2.9.3=default_h554bfaf_1009
|
||||
- libiconv=1.17=hd590300_1
|
||||
- libjpeg-turbo=2.1.5.1=hd590300_1
|
||||
- liblapack=3.9.0=20_linux64_openblas
|
||||
- liblapacke=3.9.0=20_linux64_openblas
|
||||
- libnghttp2=1.58.0=h47da74e_1
|
||||
- libnpp=11.7.4.75=0
|
||||
- libnsl=2.0.1=hd590300_0
|
||||
- libnuma=2.0.16=h0b41bf4_1
|
||||
- libnvjpeg=11.8.0.2=0
|
||||
- libopenblas=0.3.25=pthreads_h413a1c8_0
|
||||
- libpng=1.6.39=h753d276_0
|
||||
- libprotobuf=3.21.12=hfc55251_2
|
||||
- libsentencepiece=0.1.99=h180e1df_0
|
||||
- libsqlite=3.44.2=h2797004_0
|
||||
- libssh2=1.11.0=h0841786_0
|
||||
- libstdcxx-ng=13.2.0=h7e041cc_3
|
||||
- libthrift=0.18.1=h8fd135c_2
|
||||
- libtiff=4.6.0=h29866fb_1
|
||||
- libutf8proc=2.8.0=h166bdaf_0
|
||||
- libuuid=2.38.1=h0b41bf4_0
|
||||
- libwebp-base=1.3.2=hd590300_0
|
||||
- libxcb=1.15=h0b41bf4_0
|
||||
- libxml2=2.11.6=h232c23b_0
|
||||
- libzlib=1.2.13=hd590300_5
|
||||
- llvm-openmp=17.0.6=h4dfa4b3_0
|
||||
- lz4-c=1.9.4=hcb278e6_0
|
||||
- mkl=2022.2.1=h84fe81f_16997
|
||||
- mkl-devel=2022.2.1=ha770c72_16998
|
||||
- mkl-include=2022.2.1=h84fe81f_16997
|
||||
- mpc=1.3.1=hfe3b2da_0
|
||||
- mpfr=4.2.1=h9458935_0
|
||||
- mpmath=1.3.0=pyhd8ed1ab_0
|
||||
- multidict=6.0.4=py310h2372a71_1
|
||||
- multiprocess=0.70.15=py310h2372a71_1
|
||||
- ncurses=6.4=h59595ed_2
|
||||
- numpy=1.26.2=py310hb13e2d6_0
|
||||
- onnx=1.14.0=py310ha3deec4_1
|
||||
- onnx2torch=1.5.13=pyhd8ed1ab_0
|
||||
- onnxruntime=1.16.3=py310hd4b7fbc_1_cpu
|
||||
- open-clip-torch=2.23.0=pyhd8ed1ab_1
|
||||
- openblas=0.3.25=pthreads_h7a3da1a_0
|
||||
- openjpeg=2.5.0=h488ebb8_3
|
||||
- openssl=3.2.0=hd590300_1
|
||||
- orc=1.9.0=h2f23424_1
|
||||
- packaging=23.2=pyhd8ed1ab_0
|
||||
- pandas=2.1.4=py310hcc13569_0
|
||||
- pillow=10.0.1=py310h29da1c1_1
|
||||
- pip=23.3.1=pyhd8ed1ab_0
|
||||
- protobuf=4.21.12=py310heca2aa9_0
|
||||
- pthread-stubs=0.4=h36c2ea0_1001
|
||||
- pyarrow=12.0.1=py310h0576679_8_cpu
|
||||
- pyarrow-hotfix=0.6=pyhd8ed1ab_0
|
||||
- pysocks=1.7.1=pyha2e5f31_6
|
||||
- python=3.10.13=hd12c33a_0_cpython
|
||||
- python-dateutil=2.8.2=pyhd8ed1ab_0
|
||||
- python-flatbuffers=23.5.26=pyhd8ed1ab_0
|
||||
- python-tzdata=2023.3=pyhd8ed1ab_0
|
||||
- python-xxhash=3.4.1=py310h2372a71_0
|
||||
- python_abi=3.10=4_cp310
|
||||
- pytorch=1.13.1=cpu_py310hd11e9c7_1
|
||||
- pytorch-cuda=11.7=h778d358_5
|
||||
- pytorch-mutex=1.0=cuda
|
||||
- pytz=2023.3.post1=pyhd8ed1ab_0
|
||||
- pyyaml=6.0.1=py310h2372a71_1
|
||||
- rdma-core=28.9=h59595ed_1
|
||||
- re2=2023.03.02=h8c504da_0
|
||||
- readline=8.2=h8228510_1
|
||||
- regex=2023.10.3=py310h2372a71_0
|
||||
- requests=2.31.0=pyhd8ed1ab_0
|
||||
- s2n=1.3.49=h06160fa_0
|
||||
- sacremoses=0.0.53=pyhd8ed1ab_0
|
||||
- safetensors=0.3.3=py310hcb5633a_1
|
||||
- sentencepiece=0.1.99=hff52083_0
|
||||
- sentencepiece-python=0.1.99=py310hebdb9f0_0
|
||||
- sentencepiece-spm=0.1.99=h180e1df_0
|
||||
- setuptools=68.2.2=pyhd8ed1ab_0
|
||||
- six=1.16.0=pyh6c4a22f_0
|
||||
- sleef=3.5.1=h9b69904_2
|
||||
- snappy=1.1.10=h9fff704_0
|
||||
- sympy=1.12=pypyh9d50eac_103
|
||||
- tbb=2021.11.0=h00ab1b0_0
|
||||
- texttable=1.7.0=pyhd8ed1ab_0
|
||||
- timm=0.9.12=pyhd8ed1ab_0
|
||||
- tk=8.6.13=noxft_h4845f30_101
|
||||
- tokenizers=0.14.1=py310h320607d_2
|
||||
- torchvision=0.14.1=cpu_py310hd3d2ac3_1
|
||||
- tqdm=4.66.1=pyhd8ed1ab_0
|
||||
- transformers=4.35.2=pyhd8ed1ab_0
|
||||
- typing-extensions=4.9.0=hd8ed1ab_0
|
||||
- typing_extensions=4.9.0=pyha770c72_0
|
||||
- tzdata=2023c=h71feb2d_0
|
||||
- ucx=1.14.1=h64cca9d_5
|
||||
- urllib3=2.1.0=pyhd8ed1ab_0
|
||||
- wcwidth=0.2.12=pyhd8ed1ab_0
|
||||
- wheel=0.42.0=pyhd8ed1ab_0
|
||||
- xorg-libxau=1.0.11=hd590300_0
|
||||
- xorg-libxdmcp=1.1.3=h7f98852_0
|
||||
- xxhash=0.8.2=hd590300_0
|
||||
- xz=5.2.6=h166bdaf_0
|
||||
- yaml=0.2.5=h7f98852_2
|
||||
- yarl=1.9.3=py310h2372a71_0
|
||||
- zipp=3.17.0=pyhd8ed1ab_0
|
||||
- zlib=1.2.13=hd590300_5
|
||||
- zstd=1.5.5=hfc55251_0
|
||||
- pip:
|
||||
- git+https://github.com/fyfrey/TinyNeuralNetwork.git
|
157
machine-learning/ann/export/run.py
Normal file
157
machine-learning/ann/export/run.py
Normal file
@ -0,0 +1,157 @@
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
from abc import abstractmethod
|
||||
|
||||
import onnx
|
||||
import open_clip
|
||||
import torch
|
||||
from onnx2torch import convert
|
||||
from onnxruntime.tools.onnx_model_utils import fix_output_shapes, make_input_shape_fixed
|
||||
from tinynn.converter import TFLiteConverter
|
||||
|
||||
|
||||
class ExportBase(torch.nn.Module):
|
||||
input_shape: tuple[int, ...]
|
||||
|
||||
def __init__(self, device: torch.device, name: str):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.name = name
|
||||
self.optimize = 5
|
||||
self.nchw_transpose = False
|
||||
|
||||
@abstractmethod
|
||||
def forward(self, input_tensor: torch.Tensor) -> torch.Tensor | tuple[torch.Tensor]:
|
||||
pass
|
||||
|
||||
def dummy_input(self) -> torch.FloatTensor:
|
||||
return torch.rand((1, 3, 224, 224), device=self.device)
|
||||
|
||||
|
||||
class ArcFace(ExportBase):
|
||||
input_shape = (1, 3, 112, 112)
|
||||
|
||||
def __init__(self, onnx_model_path: str, device: torch.device):
|
||||
name, _ = os.path.splitext(os.path.basename(onnx_model_path))
|
||||
super().__init__(device, name)
|
||||
onnx_model = onnx.load_model(onnx_model_path)
|
||||
make_input_shape_fixed(onnx_model.graph, onnx_model.graph.input[0].name, self.input_shape)
|
||||
fix_output_shapes(onnx_model)
|
||||
self.model = convert(onnx_model).to(device)
|
||||
if self.device.type == "cuda":
|
||||
self.model = self.model.half()
|
||||
|
||||
def forward(self, input_tensor: torch.Tensor) -> torch.FloatTensor:
|
||||
embedding: torch.FloatTensor = self.model(
|
||||
input_tensor.half() if self.device.type == "cuda" else input_tensor
|
||||
).float()
|
||||
assert isinstance(embedding, torch.FloatTensor)
|
||||
return embedding
|
||||
|
||||
def dummy_input(self) -> torch.FloatTensor:
|
||||
return torch.rand(self.input_shape, device=self.device)
|
||||
|
||||
|
||||
class RetinaFace(ExportBase):
|
||||
input_shape = (1, 3, 640, 640)
|
||||
|
||||
def __init__(self, onnx_model_path: str, device: torch.device):
|
||||
name, _ = os.path.splitext(os.path.basename(onnx_model_path))
|
||||
super().__init__(device, name)
|
||||
self.optimize = 3
|
||||
self.model = convert(onnx_model_path).eval().to(device)
|
||||
if self.device.type == "cuda":
|
||||
self.model = self.model.half()
|
||||
|
||||
def forward(self, input_tensor: torch.Tensor) -> tuple[torch.FloatTensor]:
|
||||
out: torch.Tensor = self.model(input_tensor.half() if self.device.type == "cuda" else input_tensor)
|
||||
return tuple(o.float() for o in out)
|
||||
|
||||
def dummy_input(self) -> torch.FloatTensor:
|
||||
return torch.rand(self.input_shape, device=self.device)
|
||||
|
||||
|
||||
class ClipVision(ExportBase):
|
||||
input_shape = (1, 3, 224, 224)
|
||||
|
||||
def __init__(self, model_name: str, weights: str, device: torch.device):
|
||||
super().__init__(device, model_name + "__" + weights)
|
||||
self.model = open_clip.create_model(
|
||||
model_name,
|
||||
weights,
|
||||
precision="fp16" if device.type == "cuda" else "fp32",
|
||||
jit=False,
|
||||
require_pretrained=True,
|
||||
device=device,
|
||||
)
|
||||
|
||||
def forward(self, input_tensor: torch.Tensor) -> torch.FloatTensor:
|
||||
embedding: torch.Tensor = self.model.encode_image(
|
||||
input_tensor.half() if self.device.type == "cuda" else input_tensor,
|
||||
normalize=True,
|
||||
).float()
|
||||
return embedding
|
||||
|
||||
|
||||
def export(model: ExportBase) -> None:
|
||||
model.eval()
|
||||
for param in model.parameters():
|
||||
param.requires_grad = False
|
||||
dummy_input = model.dummy_input()
|
||||
model(dummy_input)
|
||||
jit = torch.jit.trace(model, dummy_input) # type: ignore[no-untyped-call,attr-defined]
|
||||
tflite_model_path = f"output/{model.name}.tflite"
|
||||
os.makedirs("output", exist_ok=True)
|
||||
|
||||
converter = TFLiteConverter(
|
||||
jit,
|
||||
dummy_input,
|
||||
tflite_model_path,
|
||||
optimize=model.optimize,
|
||||
nchw_transpose=model.nchw_transpose,
|
||||
)
|
||||
# segfaults on ARM, must run on x86_64 / AMD64
|
||||
converter.convert()
|
||||
|
||||
armnn_model_path = f"output/{model.name}.armnn"
|
||||
os.environ["LD_LIBRARY_PATH"] = "armnn"
|
||||
subprocess.run(
|
||||
[
|
||||
"./armnnconverter",
|
||||
"-f",
|
||||
"tflite-binary",
|
||||
"-m",
|
||||
tflite_model_path,
|
||||
"-i",
|
||||
"input_tensor",
|
||||
"-o",
|
||||
"output_tensor",
|
||||
"-p",
|
||||
armnn_model_path,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if platform.machine() not in ("x86_64", "AMD64"):
|
||||
raise RuntimeError(f"Can only run on x86_64 / AMD64, not {platform.machine()}")
|
||||
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
if device.type != "cuda":
|
||||
logging.warning(
|
||||
"No CUDA available, cannot create fp16 model! proceeding to create a fp32 model (use only for testing)"
|
||||
)
|
||||
models = [
|
||||
ClipVision("ViT-B-32", "openai", device),
|
||||
ArcFace("buffalo_l_rec.onnx", device),
|
||||
RetinaFace("buffalo_l_det.onnx", device),
|
||||
]
|
||||
for model in models:
|
||||
export(model)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with torch.no_grad():
|
||||
main()
|
@ -26,6 +26,7 @@ class Settings(BaseSettings):
|
||||
request_threads: int = os.cpu_count() or 4
|
||||
model_inter_op_threads: int = 1
|
||||
model_intra_op_threads: int = 2
|
||||
ann: bool = True
|
||||
|
||||
class Config:
|
||||
env_prefix = "MACHINE_LEARNING_"
|
||||
|
68
machine-learning/app/models/ann.py
Normal file
68
machine-learning/app/models/ann.py
Normal file
@ -0,0 +1,68 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
from numpy import ascontiguousarray
|
||||
|
||||
from ann.ann import Ann
|
||||
from app.schemas import ndarray_f32, ndarray_i32
|
||||
|
||||
from ..config import log, settings
|
||||
|
||||
|
||||
class AnnSession:
|
||||
"""
|
||||
Wrapper for ANN to be drop-in replacement for ONNX session.
|
||||
"""
|
||||
|
||||
def __init__(self, model_path: Path):
|
||||
tuning_file = Path(settings.cache_folder) / "gpu-tuning.ann"
|
||||
with tuning_file.open(mode="a"):
|
||||
# make sure tuning file exists (without clearing contents)
|
||||
# once filled, the tuning file reduces the cost/time of the first
|
||||
# inference after model load by 10s of seconds
|
||||
pass
|
||||
self.ann = Ann(tuning_level=3, tuning_file=tuning_file.as_posix())
|
||||
log.info("Loading ANN model %s ...", model_path)
|
||||
cache_file = model_path.with_suffix(".anncache")
|
||||
save = False
|
||||
if not cache_file.is_file():
|
||||
save = True
|
||||
with cache_file.open(mode="a"):
|
||||
# create empty model cache file
|
||||
pass
|
||||
|
||||
self.model = self.ann.load(
|
||||
model_path.as_posix(),
|
||||
save_cached_network=save,
|
||||
cached_network_path=cache_file.as_posix(),
|
||||
)
|
||||
log.info("Loaded ANN model with ID %d", self.model)
|
||||
|
||||
def __del__(self) -> None:
|
||||
self.ann.unload(self.model)
|
||||
log.info("Unloaded ANN model %d", self.model)
|
||||
self.ann.destroy()
|
||||
|
||||
def get_inputs(self) -> list[AnnNode]:
|
||||
shapes = self.ann.input_shapes[self.model]
|
||||
return [AnnNode(None, s) for s in shapes]
|
||||
|
||||
def get_outputs(self) -> list[AnnNode]:
|
||||
shapes = self.ann.output_shapes[self.model]
|
||||
return [AnnNode(None, s) for s in shapes]
|
||||
|
||||
def run(
|
||||
self,
|
||||
output_names: list[str] | None,
|
||||
input_feed: dict[str, ndarray_f32] | dict[str, ndarray_i32],
|
||||
run_options: Any = None,
|
||||
) -> list[ndarray_f32]:
|
||||
inputs: list[ndarray_f32] = [ascontiguousarray(v) for v in input_feed.values()]
|
||||
return self.ann.execute(self.model, inputs)
|
||||
|
||||
|
||||
class AnnNode(NamedTuple):
|
||||
name: str | None
|
||||
shape: tuple[int, ...]
|
@ -10,8 +10,11 @@ import onnxruntime as ort
|
||||
from huggingface_hub import snapshot_download
|
||||
from typing_extensions import Buffer
|
||||
|
||||
import ann.ann
|
||||
|
||||
from ..config import get_cache_dir, get_hf_model_name, log, settings
|
||||
from ..schemas import ModelType
|
||||
from .ann import AnnSession
|
||||
|
||||
|
||||
class InferenceModel(ABC):
|
||||
@ -138,6 +141,21 @@ class InferenceModel(ABC):
|
||||
self.cache_dir.unlink()
|
||||
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _make_session(self, model_path: Path) -> AnnSession | ort.InferenceSession:
|
||||
armnn_path = model_path.with_suffix(".armnn")
|
||||
if settings.ann and ann.ann.is_available and armnn_path.is_file():
|
||||
session = AnnSession(armnn_path)
|
||||
elif model_path.is_file():
|
||||
session = ort.InferenceSession(
|
||||
model_path.as_posix(),
|
||||
sess_options=self.sess_options,
|
||||
providers=self.providers,
|
||||
provider_options=self.provider_options,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"the file model_path='{model_path}' does not exist")
|
||||
return session
|
||||
|
||||
|
||||
# HF deep copies configs, so we need to make session options picklable
|
||||
class PicklableSessionOptions(ort.SessionOptions): # type: ignore[misc]
|
||||
|
@ -6,7 +6,6 @@ from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
from PIL import Image
|
||||
from tokenizers import Encoding, Tokenizer
|
||||
|
||||
@ -33,24 +32,12 @@ class BaseCLIPEncoder(InferenceModel):
|
||||
def _load(self) -> None:
|
||||
if self.mode == "text" or self.mode is None:
|
||||
log.debug(f"Loading clip text model '{self.model_name}'")
|
||||
|
||||
self.text_model = ort.InferenceSession(
|
||||
self.textual_path.as_posix(),
|
||||
sess_options=self.sess_options,
|
||||
providers=self.providers,
|
||||
provider_options=self.provider_options,
|
||||
)
|
||||
self.text_model = self._make_session(self.textual_path)
|
||||
log.debug(f"Loaded clip text model '{self.model_name}'")
|
||||
|
||||
if self.mode == "vision" or self.mode is None:
|
||||
log.debug(f"Loading clip vision model '{self.model_name}'")
|
||||
|
||||
self.vision_model = ort.InferenceSession(
|
||||
self.visual_path.as_posix(),
|
||||
sess_options=self.sess_options,
|
||||
providers=self.providers,
|
||||
provider_options=self.provider_options,
|
||||
)
|
||||
self.vision_model = self._make_session(self.visual_path)
|
||||
log.debug(f"Loaded clip vision model '{self.model_name}'")
|
||||
|
||||
def _predict(self, image_or_text: Image.Image | str) -> ndarray_f32:
|
||||
@ -61,12 +48,10 @@ class BaseCLIPEncoder(InferenceModel):
|
||||
case Image.Image():
|
||||
if self.mode == "text":
|
||||
raise TypeError("Cannot encode image as text-only model")
|
||||
|
||||
outputs: ndarray_f32 = self.vision_model.run(None, self.transform(image_or_text))[0][0]
|
||||
case str():
|
||||
if self.mode == "vision":
|
||||
raise TypeError("Cannot encode text as vision-only model")
|
||||
|
||||
outputs = self.text_model.run(None, self.tokenize(image_or_text))[0][0]
|
||||
case _:
|
||||
raise TypeError(f"Expected Image or str, but got: {type(image_or_text)}")
|
||||
|
@ -3,7 +3,6 @@ from typing import Any
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
from insightface.model_zoo import ArcFaceONNX, RetinaFace
|
||||
from insightface.utils.face_align import norm_crop
|
||||
|
||||
@ -27,23 +26,8 @@ class FaceRecognizer(InferenceModel):
|
||||
super().__init__(clean_name(model_name), cache_dir, **model_kwargs)
|
||||
|
||||
def _load(self) -> None:
|
||||
self.det_model = RetinaFace(
|
||||
session=ort.InferenceSession(
|
||||
self.det_file.as_posix(),
|
||||
sess_options=self.sess_options,
|
||||
providers=self.providers,
|
||||
provider_options=self.provider_options,
|
||||
),
|
||||
)
|
||||
self.rec_model = ArcFaceONNX(
|
||||
self.rec_file.as_posix(),
|
||||
session=ort.InferenceSession(
|
||||
self.rec_file.as_posix(),
|
||||
sess_options=self.sess_options,
|
||||
providers=self.providers,
|
||||
provider_options=self.provider_options,
|
||||
),
|
||||
)
|
||||
self.det_model = RetinaFace(session=self._make_session(self.det_file))
|
||||
self.rec_model = ArcFaceONNX(self.rec_file.as_posix(), session=self._make_session(self.rec_file))
|
||||
|
||||
self.det_model.prepare(
|
||||
ctx_id=0,
|
||||
|
@ -13,7 +13,7 @@ from PIL import Image
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from .config import settings
|
||||
from .models.base import PicklableSessionOptions
|
||||
from .models.base import InferenceModel, PicklableSessionOptions
|
||||
from .models.cache import ModelCache
|
||||
from .models.clip import OpenCLIPEncoder
|
||||
from .models.facial_recognition import FaceRecognizer
|
||||
@ -36,9 +36,10 @@ class TestCLIP:
|
||||
mocker.patch.object(OpenCLIPEncoder, "model_cfg", clip_model_cfg)
|
||||
mocker.patch.object(OpenCLIPEncoder, "preprocess_cfg", clip_preprocess_cfg)
|
||||
mocker.patch.object(OpenCLIPEncoder, "tokenizer_cfg", clip_tokenizer_cfg)
|
||||
|
||||
mocked = mocker.patch.object(InferenceModel, "_make_session", autospec=True).return_value
|
||||
mocked.run.return_value = [[self.embedding]]
|
||||
mocker.patch("app.models.clip.Tokenizer.from_file", autospec=True)
|
||||
mocked = mocker.patch("app.models.clip.ort.InferenceSession", autospec=True)
|
||||
mocked.return_value.run.return_value = [[self.embedding]]
|
||||
|
||||
clip_encoder = OpenCLIPEncoder("ViT-B-32::openai", cache_dir="test_cache", mode="vision")
|
||||
embedding = clip_encoder.predict(pil_image)
|
||||
@ -47,7 +48,7 @@ class TestCLIP:
|
||||
assert isinstance(embedding, np.ndarray)
|
||||
assert embedding.shape[0] == clip_model_cfg["embed_dim"]
|
||||
assert embedding.dtype == np.float32
|
||||
clip_encoder.vision_model.run.assert_called_once()
|
||||
mocked.run.assert_called_once()
|
||||
|
||||
def test_basic_text(
|
||||
self,
|
||||
@ -60,9 +61,10 @@ class TestCLIP:
|
||||
mocker.patch.object(OpenCLIPEncoder, "model_cfg", clip_model_cfg)
|
||||
mocker.patch.object(OpenCLIPEncoder, "preprocess_cfg", clip_preprocess_cfg)
|
||||
mocker.patch.object(OpenCLIPEncoder, "tokenizer_cfg", clip_tokenizer_cfg)
|
||||
|
||||
mocked = mocker.patch.object(InferenceModel, "_make_session", autospec=True).return_value
|
||||
mocked.run.return_value = [[self.embedding]]
|
||||
mocker.patch("app.models.clip.Tokenizer.from_file", autospec=True)
|
||||
mocked = mocker.patch("app.models.clip.ort.InferenceSession", autospec=True)
|
||||
mocked.return_value.run.return_value = [[self.embedding]]
|
||||
|
||||
clip_encoder = OpenCLIPEncoder("ViT-B-32::openai", cache_dir="test_cache", mode="text")
|
||||
embedding = clip_encoder.predict("test search query")
|
||||
@ -71,7 +73,7 @@ class TestCLIP:
|
||||
assert isinstance(embedding, np.ndarray)
|
||||
assert embedding.shape[0] == clip_model_cfg["embed_dim"]
|
||||
assert embedding.dtype == np.float32
|
||||
clip_encoder.text_model.run.assert_called_once()
|
||||
mocked.run.assert_called_once()
|
||||
|
||||
|
||||
class TestFaceRecognition:
|
||||
|
Loading…
Reference in New Issue
Block a user