I don't think you need a custom thread pool, especially if you're doing a lot parallel I/O which naturally uses IOCP threads. This applies to any other API which only acquires a thread from the pool when its task has completed (e.g, Task.Delay).
Usually you do await task.ConfigureAwait(false) for continuation to happen on the same thread the task has completed on:
// non-IOCP (worker) continuation thread
await Task.Delay(1000).ConfigureAwait(false);
// IOCP continuation thread
await stream.ReadAsync(buff, 0, buff.Length).ConfigureAwait(false);
Now, in ASP.NET you don't even need to use ConfigureAwait. ASP.NET's AspNetSynchronizationContext doesn't maintain thread affinity for await continuations, unlike WinFormsSynchronizationContext or DispatcherSynchronizationContext do for a UI app. Your continuations will run on whatever thread the task has completed on, as is. AspNetSynchronizationContext just "enters" the ASP.NET context on that thread, to make sure your code has access to HttpContext and other relevant ASP.NET request ambient state information.
You can increase the default number of IOCP and worker threads with ThreadPool.SetMinThreads to account for ThreadPool stuttering problems.
If your only reason for custom thread pool is to limit the level of parallelism for your tasks, use TPL Dataflow or simply SemaphoreSlim.WaitAsync (check "Throttling asynchronous tasks").
That said, you can have a very precise control over the Task continuation if you implement a custom awaiter. Check this question for a basic example of that.