Skip to content

Feature Request: Switch backends dynamically at runtime? #264

Closed

Description

@BrainSlugs83

Right now, to switch backends, I have to uninstall one nuget package, and then reinstall another. -- This means that if I make an app, I would have to create different builds for every configuration.

Instead I'd rather just give someone a dropdown box in a UI and let them pick the backend, and have the app switch on the fly to whatever they have selected.

Ideally this would just be an enum in the ModelParams, something like Backend = LLamaSharp.Cuda, or Backend = LLamaSharp.Cpu, etc. (Heck, even if it has to be a global static parameter, that's better than having to make separate builds.)

One possible way it could be implemented is by shipping all of the libllama.dll files in the output directory with a slightly different name (i.e. libllama-cuda.dll, libllama-avx.dll, etc.) and dynamically loading the DLL into memory based on which backend is being requested.

Activity

  1. martindevans commented on Nov 7, 2023

    @martindevans
    Member

    It sounds like there's two parts to this request:

    • Ability to configure the backend before it is loaded.
      • At the moment this isn't possible, because the DLL loading is in the static constructor for NativeApi, so the instant you access that class the DLL is loaded. We could introduce another class (e.g. NativeApiConfig) with static config methods/properties on it.
    • Ability to unload the backend and load another.
      • This could probably be done by using NativeLibrary.Load and NativeLibrary.Free (docs).
  2. BrainSlugs83 commented on Nov 8, 2023

    @BrainSlugs83
    Author

    It looks like you're already trying to load them one at a time in the static API... what would be the harm in loading all of the ones that are found? and just putting the IntPtrs into an array.

    We could set the one we want to use via a static enum property -- and the IntPtr could just be a read only property that returns the appropriate IntPtr based on which enum is selected. (Or throws an exception if a missing one was selected.)

    We could have another static method that is just like IEnumerable<Backend> AvailableBackends { get; } that does a yield return Backend.xyz if that backend is available...

    That would allow hotswap of the dlls at runtime.

    Edit: I suppose you wouldn't want the backend to switch out from under any already created LLMs, or whatever... but even then... you could still load them all into an array and use an enum (just don't do the static property stuff). Just pass the enum in as a model param, and it grabs the backend pointer at creation time.

    That would allow you to use multiple backends at the same time.

  3. martindevans commented on Nov 8, 2023

    @martindevans
    Member

    Loading all of the backends at once wouldn't work with the current system because the native methods are written like this:

    [DllImport("libllama")]
    public static extern void demo_method();

    That will use the already loaded libllama.dll (if there is one) otherwise it will find and load it when called. Given that I'm not sure exactly what would happen it you loaded multiple, but I'm sure it wouldn't be good!

    If we just wanted to allow unloading one backend DLL and loading another I think that could probably be done by keeping the IntPtr returned from NativeLibrary.Load and passing it into NativeLibrary.Free later (currently we don't keep that pointer because never unload libraries). In this case no rewriting of the native symbols would be required because there's only ever one loaded library.

    If we wanted to allow multiple backends that's a lot more complex. I'm not even 100% sure if it's possible, is it guaranteed that two versions of libllama.dll don't try to use some per-process state internally? Assuming it is we'd probably have to define a non-static "LLamaBackend" object and then fetch all of the native methods using GetExport on a specific backend. Of course we'd probably also want to allow some kind of default backend, so users can continue using the library roughly as-is without having to create a backend object.

    If you're interested in experimenting with any of this stuff I'm happy to help with that. Some prototypes in a fork showing some proof-of-concept unloading/reloading/multi-loading would be great!

    P.S. one extra complexity I just thought of: NativeLibrary doesn't exist in NETSTANDARD2_0, so there's no cross platform way to do all this stuff there. It can be done, but you need to call the approprate OS methods directly.

  4. saddam213 commented on Nov 9, 2023

    @saddam213
    Collaborator

    What if we wrote a bunch of delegates as a sort of wrapped API, and use LoadLibrary to swap dll's out during runtime

    However the performance hit could be heavy, also cleanup could become troublesome

        static class KernelNativeMethods
        {
            [DllImport("kernel32.dll")]
            public static extern IntPtr LoadLibrary(string dllToLoad);
    
            [DllImport("kernel32.dll")]
            public static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);
    
            [DllImport("kernel32.dll")]
            public static extern bool FreeLibrary(IntPtr hModule);
        }
    
        class Program
        {
            [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
            private delegate bool LLamaEmptyCall();
    
            static void Main(string[] args)
            {
                IntPtr libllama = KernelNativeMethods.LoadLibrary(@"libllama-cuda12.dll");
     
                IntPtr functionToCallAddress = KernelNativeMethods.GetProcAddress(libllama, "llama_mmap_supported");
    
                LLamaEmptyCall llama_empty_call = (LLamaEmptyCall)Marshal.GetDelegateForFunctionPointer(functionToCallAddress, typeof(LLamaEmptyCall));
    
                bool result = llama_empty_call();
    
                Console.WriteLine(result);
    
                // Free
                KernelNativeMethods.FreeLibrary(libllama);
            }
        }
    
    
  5. moved this to 🔖 In Discussion in LLamaSharp Devon Nov 9, 2023
  6. moved this from 🔖 In Discussion to 📋 TODO in LLamaSharp Devon Nov 9, 2023
  7. martindevans commented on Nov 9, 2023

    @martindevans
    Member

    What if we wrote a bunch of delegates as a sort of wrapped API, and use LoadLibrary to swap dll's out during runtime

    As I understand it that's roughly equivalent to what I was suggesting with NativeLibrary.Load (cross platform equivalent of kernel32.LoadLibrary and NativeLibrary.GetExport (cross platform equivalent of kernel32.GetProcAddress).

    A few problems that I'm not sure about with loading multiple backends:

    • The backend would have to keep an outgoing weak reference to everything created with it, so that when the backend is destroyed it can first destroy all resources. Not impossible, but complex.
    • Is it legal to have multiple versions of llama.cpp loaded at once? e.g. is it possible that they could be using some per-process global state that would break in subtle ways?
    • Do we actually want to go as far as supporting multiple backends, or is just supporting swapping backends good enough? If you can load the CUDA backend then can't you use that with n_gpu_layers = 0 instead of reloading to a pure CPU backend?
  8. saddam213 commented on Nov 9, 2023

    @saddam213
    Collaborator

    Just support for swapping would be handy,

    I am one of the few that run LLamaSharp on servers with GPUs but don't want to use GPU's as they are busy running other apps, so Just hoping we include a way to override the incoming auto-dectect code

    In the past n_gpu_layers = 0 resulted in a crash, this could have been resolved as its been several months since I tried

  9. SanftMonster commented on Nov 9, 2023

    @SanftMonster
    Collaborator

    To me either supporting multiple backends or swapping backends seems too aggressive now. The largest problem is that there're memories held in native library which we know few about. Things may be out of our control when swapping the backend. I suggest to allow user to select a preferred backend only once before loading llama model now (the "ability to configure the backend before it is loaded" discussed above).

    In the condition @saddam213 mentioned, there's a point which needs to be further discussed. If there's a model m running on GPU and now I want it to run only on cpu instead, is the time of re-loading model and context acceptable? Besides, when GPUs are busy running other apps, is the gpu memory also in short? The answer decides whether it's supposed to clear the memories allocated in GPU.
    If clearing it every time is ok, I think save the current states and reloading the whole process with another backend is reasonable. Otherwise it will be complex. If we switch to another backend, there's a huge risk of memory leak. Even if we solved the memory leak, after executing some inference with the swapped backend, how can we synchronize the new state with the old one?

    When we call NativeLibrary.Free, the memories allocated by dll on heap won't be released. If we want to avoid memory leak, we must have an API in c++ to clear all the memories allocated itself. Then we call the API from C# before loading another backend.

    Though I'm against dynamically switching backend after loading now, I think this issue is a commonly encountered one when building a service base on LLamaSharp and worth working on. I was just saying we shouldn't make the changes until we eliminate the risks.

  10. hswlab commented on Nov 13, 2023

    @hswlab
    Contributor

    I'm working on an Electron/LLamaSharp application with .NET6 and was wondering if it would be possible to give the user the option to select the backend in the APP settings via a dropdown. The dll would then be downloaded from Github (https://github.com/ggerganov/llama.cpp/releases/latest) and unpacked. The application would then need to be restarted. When starting the application, LLamaSharp just needs to know in which directory the unpacked dll is located.

    I noticed this code, where an alternative path to the backend dll could possibly be loaded.

    image

    I havn't tested the NativeLibraryConfig yet, but this looks very promising for dynamic loading of the backend.
    Would it work to call the NativeLibraryConfig.WithLibrary method in the Program.cs at application start with a custom path?

    Edit:
    It seems that WithLibrary(string libraryPath) is not a static method. I think I can't use it so easily as I thought :)

    Edit2:
    I think I figured it out how to use this config. I set this at the beginning of the Program.cs:
    NativeLibraryConfig.Default.WithLibrary("C:\Users\my_path\llama.dll");

    Now the correct path is loaded, but the exception is still called. Probably I'm loading the wrong dll or I'm missing something. I will check it tomorrow ^^'.
    image

  11. SanftMonster commented on Nov 13, 2023

    @SanftMonster
    Collaborator

    Edit: It seems that WithLibrary(string libraryPath) is not a static method. I think I can't use it so easily as I thought :)

    Please use NativeLibraryConfig.Default.WithLibrary. Note that this API may be changed to NativeLibraryConfig.Instance in #281.

    Would it work to call the NativeLibraryConfig.WithLibrary method in the Program.cs at application start with a custom path?

    I think so, it was designed to do this :)

    I haven't tested the NativeLibraryConfig yet, but this looks very promising for dynamic loading of the backend.

    If you would like to, you can remove the check here to load the library anywhere at runtime to help to check if there's memory leak or other bad behaviours.

  12. martindevans commented on Nov 13, 2023

    @martindevans
    Member

    Looks like you've already worked it out, but NativeLibraryConfig.Default.WithLibrary( is the way to do this :)

    Please note though that you cannot just download the latest DLL from llama.cpp - you must download exactly right commit version. There is absolutely no compatibility from version to version!

  13. SanftMonster commented on Nov 13, 2023

    @SanftMonster
    Collaborator

    Edit2: I think I figured it out how to use this config. I set this at the beginning of the Program.cs: NativeLibraryConfig.Default.WithLibrary("C:\Users\my_path\llama.dll");

    Please ensure you loaded a library with name libllama.dll, instead of llama.dll.

  14. 4 remaining items

  15. BrainSlugs83 commented on Nov 20, 2023

    @BrainSlugs83
    Author

    As far as I know it must just be the name of the DLL, not an entire path.

    I tested the above with both absolute and relative paths, and they both work. Same directory is not needed -- however, if using relative paths, it needs to be a relative path from the current directory.

    Before we start a deep discussion about it, could you please further describe why you want to switch dynamically between cpu backend and cuda backend? If the reason is to switch between a gpu offloaded model and a cpu offloaded one, in #298 we found llama.cpp has supported using pure cpu with cuda-built library.

    That's actually great information that I was not aware of before -- for the time being it sounds like I can now just stick with the CUDA backend. (But there's no guarantee that will work for folks who don't have CUDA installed -- for the release version of my app...)

    But at least, previously, I'd run into bugs and instabilities with either CUDA or non-CUDA depending on the version of LlamaSharp, and in the interest of trying to narrow down the bug, I found it very painful to have to constantly change NuGet packages, and I was thinking of other benefits as well such as runtime switching for an end user application. (Because it seems performance varies wildly between the two as well from version to version. 🫤)

    I was also thinking of other targets as well like Metal, or a non-avx CPU lib -- or perhaps down the road if other third party backend targets exist (such as an OpenCL, Windows ONNX Acceleration, or even a Vulkan backend), it would be great for compatibility to be able to switch between them on the fly.

  16. SanftMonster commented on Nov 20, 2023

    @SanftMonster
    Collaborator

    But there's no guarantee that will work for folks who don't have CUDA installed -- for the release version of my app...

    Yes, we are cautious about this feature so we still separate cpu and CUDA backend packages. However I believe this llama.cpp feature means to support running on non-cuda devices.

    Besides, in the condition you described, I think the cuda feature auto-detection included in v0.8.0 has supported it. We are thinking about gathering all the native libraries in one backend package (but haven't made the final decision) and automatically choose a library. It will choose cuda backend only when cuda is available on the device.

  17. jhancock4d commented on Nov 20, 2023

    @jhancock4d

    Couldn't we just automatically and only load those that the CPU/GPUs support based on interrogating the OS? Then calculate the memory required and run it GPU if available and run it CPU if not?

  18. martindevans commented on Nov 20, 2023

    @martindevans
    Member

    Couldn't we just automatically and only load those that the CPU/GPUs support based on interrogating the OS?

    That's actually what we already do. CUDA binaries are laoded based on the version of CUDA installed and failing that the best CPU binary is loaded (based on which AVX version your CPU supports).

    Then calculate the memory required and run it GPU if available and run it CPU if not?

    There was some discussion #42 about automatic layers count calculation. It seems like that's complicated even for llama.cpp (which has more information to work it out than LLamaSharp does), so that probably won't happen any time soon/ever unfortunately.

  19. jhancock4d commented on Nov 21, 2023

    @jhancock4d

    Ok, well LM Studio is doing something, because if I add the CPU and Cuda to a project, it always uses the CPU and ignores CUDA being there, and if I have CUDA and I run out of memory for the request, you just get an out of memory message and your app dies.

    In LM Studio, it works using CUDA first if the context fits into GPU VRAM and if it doesn't, then it uses CPU. I can see it doing this with task manager.

    It would be nice to have the same results so that our apps aren't so brittle.

  20. BrainSlugs83 commented on Nov 27, 2023

    @BrainSlugs83
    Author

    Yes, we are cautious about this feature so we still separate cpu and CUDA backend packages. However I believe this llama.cpp feature means to support running on non-cuda devices.

    Yeah, the other issue is there are multiple versions of CUDA, right? -- So even if I just packaged an app with CUDA, and were to set GPU layers to 0, I would still have to have two separate release builds of my application for the GPU users depending on if the user had CUDA11 or CUDA12. -- So again, it would be good to be able to switch at runtime... without needed separate NuGet packages for each backend.

  21. SanftMonster commented on Nov 27, 2023

    @SanftMonster
    Collaborator

    Yeah, the other issue is there are multiple versions of CUDA, right? -- So even if I just packaged an app with CUDA, and were to set GPU layers to 0, I would still have to have two separate release builds of my application for the GPU users depending on if the user had CUDA11 or CUDA12. -- So again, it would be good to be able to switch at runtime... without needed separate NuGet packages for each backend.

    We have already supported detection of cuda version to load a suitable library. The only thing left is to integrate all the backend packages to one package. Actually I intended to make this break change of backend library in v1.0.0. I could add support for specifying base directory in v0.8.1, so that you can take use of this feature by keeping a certain file structure of your native libraries. V0.8.1 will be out within 1 or 2 days.

  22. jhancock4d commented on Nov 27, 2023

    @jhancock4d

    Would this allow graceful fallback? Would it allow for an OpenCL version of the library as well? (AMD) Ideally we just want to have it work as fast as it can, and not have to expose knowledge of the running machine to be able to use this library. So it needs to just work like LM Studio does.

  23. martindevans commented on Nov 27, 2023

    @martindevans
    Member

    I don't think we do anything specific for OpenCL at the moment. but this:

    Ideally we just want to have it work as fast as it can

    Is definitely the long term intention of the loading system. As much of it should be automated as possible.

  24. SanftMonster commented on Nov 28, 2023

    @SanftMonster
    Collaborator

    @BrainSlugs83 v0.8.1 has been out, which allows to specify the search directories of native libraries.

  25. BrainSlugs83 commented on Nov 30, 2023

    @BrainSlugs83
    Author

    We have already supported detection of cuda version to load a suitable library.

    Oh, dang I got confused by this:

    result.Add($"{prefix}cuda11/{libraryName}{suffix}");

    Looking closer, I see that you are correct; this is like a fallback for when CUDA isn't detected. So that makes sense.

    Actually I intended to make this break change of backend library in v1.0.0

    I'm fine with waiting for a proper merged library if it's coming in 1.0.0. 🙂
    It's not an urgent ask -- it's just something that will make life easier for us once it's in there.

    Would this allow graceful fallback? Would it allow for an OpenCL version of the library as well? (AMD)

    The llama.cpp folks already have an OpenCL version (and maybe a ROCm one IIUC), and I think they are planning a Vulkan backend as well (saw it under their discussions...) -- but the Vulkan build is a bit stalled.

    v0.8.1 has been out, which allows to specify the search directories of native libraries.

    Nice, I'll take a look! 🙂

  26. martindevans commented on Feb 2, 2024

    @martindevans
    Member

    OpenCL support will be merged in with #479, and will probably be included in the next release (still needs some work doing to create the new nuget packages).

  27. SanftMonster commented on Apr 21, 2024

    @SanftMonster
    Collaborator

    #670 suggests a way to use LLamaSharp without backend packages, which might be related with the point of this issue. For your attention here. :)

  28. moved this from 📋 TODO to ✅ Done in LLamaSharp Devon Apr 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions