The error message is telling you that the perform function is not polymorphic since it using a table with a type a coming from the binding in the set_array function. In particular, there is no reasons to assume that the type a from the kind argument of perform has anything to do with the type a of the Ndarray.
I would recommend to avoid nesting functions with universal quantifications, and if you do so, it would be clearer to use different type names:
let set_array: type a b.
Path.t -> Owl_types.slice -> (a, b) Ndarray.t -> t
-> (unit, [> set_error]) result
= ...
let perform
: type elt tag. (elt, tag) Bigarray.kind -> elt
-> (unit, [> set_error]) result
= ...
However, in this case the problem starts with the type:
let set_array: type a b.
Path.t -> Owl_types.slice -> (a, b) Ndarray.t -> t
-> (unit, [> set_error]) result
With this type, you are promising that the function set_array works with any type a and b with no information on those types. This can only work if the set_array avoid any kind of operations that requires to know those types which is not the case here.
To avoid this conundrum, you need to add some runtime information about the Ndarray that you are manipulating. For instance, you could add a kind argument to convey this information.
But once again, it is not clear what you gain in your use case by piling up GADTs, compared to replacing the ndarray type by the simpler barray variant that I showed you previously.