AI on Windows: Detecting NPU

Recently Microsoft has been promoting Copilot+ PCs as the next wave of tablets, laptops and PCs capable of handling your AI requirements. Other than having a Copilot button, what does this actually mean? In this post we’ll take a look at what makes these PCs different, how you can detect that your application is running on one of these PCs and what you might want to do with this knowledge.

If we ask Copilot what a Copilot+ PC is, the answer is somewhat vague, pointing out that the devices are “powered by high-performance processors” but falls short of defining them as a device that has a separate NPU. The Microsoft developer documentation site provides a more detailed explanation:

Copilot+ PCs are a new class of Windows 11 hardware powered by a high-performance Neural Processing Unit (NPU) — a specialized computer chip for AI-intensive processes like real-time translations and image generation—that can perform more than 40 trillion operations per second (TOPS). Copilot+ PCs provide all–day battery life and access to the most advanced AI features and models

In addition to the consumer features that having an NPU enables (eg Windows Studio Effects and Recall), it also enables developer capabilities via the Windows Copilot Runtime and associated APIs. Of course, if you’re going to start adding features to your application that leverage the Copilot runtime, you’re going to need a way to detect if the device you’re running on supports them.

Currently the APIs are all in the experimental release of the Windows App Sdk, and require your device to be on the developer branch of the Windows Insider program. As the APIs are still in active development, there are some limitations. Being able to detect whether a specific feature is supported on the current devices is one such limitation. Each of the feature APIs offer an IsAvailable method (eg TextRecognizer.IsAvailable()) that can be used to detect if the corresponding model is ready and available. If not, you then have to call the MakeAvailableAsync method (eg TextRecognizer.MakeAvailableAsync()). This will fail with an exception on devices where the model isn’t supported.

Detecting support by attempting to invoke MakeAvailableAsync (and having to wait for the model to be downloaded) is a pretty average experience. Luckily the team behind the AI Dev Gallery have some code that can be used to detect if there’s an NPU present on the device. Here’s a snippet of the code:

using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.DXCore;

    private static readonly Guid DXCORE_ADAPTER_ATTRIBUTE_D3D12_GENERIC_ML = new(0xb71b0d41, 0x1088, 0x422f, 0xa2, 0x7c, 0x2, 0x50, 0xb7, 0xd3, 0xa9, 0x88);
    private static bool? _hasNpu;
    public static bool HasNpu()
    {
        if (_hasNpu.HasValue)
        {
            return _hasNpu.Value;
        }

        IDXCoreAdapterFactory adapterFactory;
        if (PInvoke.DXCoreCreateAdapterFactory(typeof(IDXCoreAdapterFactory).GUID, out var adapterFactoryObj) != HRESULT.S_OK)
        {
            throw new InvalidOperationException("Failed to create adapter factory");
        }

        adapterFactory = (IDXCoreAdapterFactory)adapterFactoryObj;

        // First try getting all GENERIC_ML devices, which is the broadest set of adapters
        // and includes both GPUs and NPUs; however, running this sample on an older build of
        // Windows may not have drivers that report GENERIC_ML.
        IDXCoreAdapterList adapterList;

        adapterFactory.CreateAdapterList([DXCORE_ADAPTER_ATTRIBUTE_D3D12_GENERIC_ML], typeof(IDXCoreAdapterList).GUID, out var adapterListObj);
        adapterList = (IDXCoreAdapterList)adapterListObj;

        // Fall back to CORE_COMPUTE if GENERIC_ML devices are not available. This is a more restricted
        // set of adapters and may filter out some NPUs.
        if (adapterList.GetAdapterCount() == 0)
        {
            adapterFactory.CreateAdapterList(
                [PInvoke.DXCORE_ADAPTER_ATTRIBUTE_D3D12_CORE_COMPUTE],
                typeof(IDXCoreAdapterList).GUID,
                out adapterListObj);
            adapterList = (IDXCoreAdapterList)adapterListObj;
        }

        if (adapterList.GetAdapterCount() == 0)
        {
            throw new InvalidOperationException("No compatible adapters found.");
        }

        // Sort the adapters by preference, with hardware and high-performance adapters first.
        ReadOnlySpan<DXCoreAdapterPreference> preferences =
        [
            DXCoreAdapterPreference.Hardware,
                DXCoreAdapterPreference.HighPerformance
        ];

        adapterList.Sort(preferences);

        List<IDXCoreAdapter> adapters = [];

        for (uint i = 0; i < adapterList.GetAdapterCount(); i++)
        {
            IDXCoreAdapter adapter;
            adapterList.GetAdapter(i, typeof(IDXCoreAdapter).GUID, out var adapterObj);
            adapter = (IDXCoreAdapter)adapterObj;

            adapter.GetPropertySize(
                DXCoreAdapterProperty.DriverDescription,
                out var descriptionSize);

            string adapterDescription;
            IntPtr buffer = IntPtr.Zero;
            try
            {
                buffer = Marshal.AllocHGlobal((int)descriptionSize);
                unsafe
                {
                    adapter.GetProperty(
                        DXCoreAdapterProperty.DriverDescription,
                        descriptionSize,
                        buffer.ToPointer());
                }

                adapterDescription = Marshal.PtrToStringAnsi(buffer) ?? string.Empty;
            }
            finally
            {
                Marshal.FreeHGlobal(buffer);
            }

            // Remove trailing null terminator written by DXCore.
            while (!string.IsNullOrEmpty(adapterDescription) && adapterDescription[^1] == '\0')
            {
                adapterDescription = adapterDescription[..^1];
            }

            adapters.Add(adapter);
            if (adapterDescription.Contains("NPU"))
            {
                _hasNpu = true;
                return true;
            }
        }

        _hasNpu = false;
        return false;
    }

You’ll need to add a package reference to Microsoft.Windows.CsWin32 and include a NativeMethods.txt file to tailor the code generator to include the required methods.

Once you’ve added this code, you can call the HasNpu method and be able to detect, without relying on exceptions being thrown, whether the device you’re running on has an Npu and thus whether it supports the Copilot Runtime. Note that you should always include exception handling around the calls to MakeAvailableAsync, as there are other reasons that this might fail and you want to ensure your application handles these scenarios gracefully.

Leave a comment