# Parallelism

> NumKong for Python

Capability detection is explicit, through module functions named after each device group:

```python
import numpy as np
import numkong as nk

# `enabled` is what dispatch runs within: detected on this CPU AND compiled into the wheel, settled as the library loads.
print(repr(nk.cpu_capabilities_enabled()))
print(nk.Capability.SKYLAKE in nk.cpu_capabilities_enabled()) # will AVX-512 kernels run here?

# The two raw axes, when you specifically mean one of them:
print(repr(nk.cpu_capabilities_detected())) # this CPU
print(repr(nk.cpu_capabilities_compiled())) # this build

# Prepare a thread that runs kernels, like Arm's FPCR; the library already asked for AMX tiles as it loaded.
nk.cpu_configure_thread(nk.cpu_capabilities_enabled())

# Narrow a single call, e.g. to test one capability: what cannot run here is dropped.
a = np.random.randn(1536).astype(np.float32)
print(nk.dot(a, a, capabilities=nk.Capability.SERIAL)) # the reference kernel
```

Every function that runs kernels takes the mask as `capabilities=` and a GPU stream as `stream=`, an integer pointer left `None` on the CPU.
It defaults to every CPU capability, which the library clamps to the enabled ones, so the binding keeps no mask of its own.
That mask keeps only CPU capabilities, as GPU tensors dispatch with their own device's, see [GPU Tensors through DLPack](https://www.unum.cloud/docs/numkong/python/tensors-and-views.md#gpu-tensors-through-dlpack).
Packed operands, like `PackedMatrix`, remember the mask that packed them and are read with it, since each capability lays its packs out differently.
The mask model is the one in [Dispatch Points & Capability Masks](https://github.com/ashvardanian/NumKong/blob/main/README.md#dispatch-points--capability-masks).

The current implementation releases the GIL around the native dense metric calls and around the packed and symmetric matrix kernels.
The repository also has threading tests for packed and symmetric row-range partitioning.

`GEMM`-like packed work and `SYRK`-like symmetric work should be documented differently:

```python
import concurrent.futures
import numpy as np
import numkong as nk

left = np.random.randn(4096, 768).astype(np.float32)
right = np.random.randn(8192, 768).astype(np.float32)
packed = nk.dots_pack(right, dtype="float32")
out = nk.zeros((4096, 8192), dtype="float64")  # out must be pre-allocated with correct shape and dtype

def packed_chunk(start, end):
    nk.dots_packed(left, packed, out=out, rows_begin=start, rows_end=end) # split left rows against one shared packed RHS

with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
    for start in range(0, 4096, 1024):
        pool.submit(packed_chunk, start, min(start + 1024, 4096))
```

```python
import concurrent.futures
import numpy as np
import numkong as nk

vectors = np.random.randn(4096, 768).astype(np.float32)
out = nk.zeros((4096, 4096), dtype="float64")  # out must be pre-allocated with correct shape and dtype

def symmetric_chunk(start, end):
    nk.dots_symmetric(vectors, out=out, rows_begin=start, rows_end=end) # split row windows of one square output

with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
    for start in range(0, 4096, 1024):
        pool.submit(symmetric_chunk, start, min(start + 1024, 4096))
```

OpenMP and other native schedulers still matter in lower layers.
For Python, the intended user-facing story is external partitioning around the GIL-free kernels you actually use.

