System Panic with IOUserSCSIParallelInterfaceController during Dispatch Queue Configuration

Hello everyone,

We are in the process of migrating a high-performance storage KEXT to DriverKit. During our initial validation phase, we noticed a performance gap between the DEXT and the KEXT, which prompted us to try and optimize our I/O handling process.

Background and Motivation:

Our test hardware is a RAID 0 array of two HDDs. According to AJA System Test, our legacy KEXT achieves a write speed of about 645 MB/s on this hardware, whereas the new DEXT reaches about 565 MB/s. We suspect the primary reason for this performance gap might be that the DEXT, by default, uses a serial work-loop to submit I/O commands, which fails to fully leverage the parallelism of the hardware array.

Therefore, to eliminate this bottleneck and improve performance, we configured a dedicated parallel dispatch queue (MyParallelIOQueue) for the UserProcessParallelTask method.

However, during our implementation attempt, we encountered a critical issue that caused a system-wide crash.

The Operation Causing the Panic:

We configured MyParallelIOQueue using the following combination of methods:

  1. In the .iig file: We appended the QUEUENAME(MyParallelIOQueue) macro after the override keyword of the UserProcessParallelTask method declaration.
  2. In the .cpp file: We manually created a queue with the same name by calling the IODispatchQueue::Create() function within our UserInitializeController method.

The Result:

This results in a macOS kernel panic during the DEXT loading process, forcing the user to perform a hard reboot.

After the reboot, checking with the systemextensionsctl list command reveals the DEXT's status as [activated waiting for user], which indicates that it encountered an unrecoverable, fatal error during its initialization.

Key Code Snippets to Reproduce the Panic:

  1. In .iig file - this was our exact implementation:

    class DRV_MAIN_CLASS_NAME: public IOUserSCSIParallelInterfaceController
    {
    public:
        virtual kern_return_t UserProcessParallelTask(...) override
            QUEUENAME(MyParallelIOQueue);
    };
    
  2. In .h file:

    struct DRV_MAIN_CLASS_NAME_IVars {
        // ...
        IODispatchQueue*    MyParallelIOQueue;
    };
    
  3. In UserInitializeController implementation:

    kern_return_t
    IMPL(DRV_MAIN_CLASS_NAME, UserInitializeController)
    {
        // ...
        // We also included code to manually create the queue.
        kern_return_t ret = IODispatchQueue::Create("MyParallelIOQueue",
                                                    kIODispatchQueueReentrant,
                                                    0,
                                                    &ivars->MyParallelIOQueue);
        if (ret != kIOReturnSuccess) {
            // ... error handling ...
        }
        // ...
        return kIOReturnSuccess;
    }
    

Our Question:

What is the officially recommended and most stable method for configuring UserProcessParallelTask_Impl() to use a parallel I/O queue?

Clarifying this is crucial for all developers pursuing high-performance storage solutions with DriverKit. Any explanation or guidance would be greatly appreciated.

Best Regards,

Charles

Answered by DTS Engineer in 865478022

Therefore, to eliminate this bottleneck and improve performance, we configured a dedicated parallel dispatch queue (MyParallelIOQueue) for the UserProcessParallelTask method.

Yeah... that won't work. UserProcessParallelTask is an OSAction target, which is already targeting a queue. I'd be curious to see how the panic() played out*, but I'm not surprised that you panicked.

*I suspect you deadlocked command submission long enough that the SCSI stack gave up and panicked, but that's purely a guess.

That leads to here:

What is the officially recommended and most stable method for configuring UserProcessParallelTask_Impl() to use a parallel I/O queue?

The answer here is to shift to UserProcessBundledParallelTasks. The architecture is a bit more complex, but it allows you to asynchronously receive and complete tasks in parallel while also reusing command and response buffers to minimize wiring cost. Take a look at the header files from SCSIControllerDriverKit for details on how this architecture works.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

I believe you filed a bug asking about this earlier, and I have my own bug on this (r.169737319). It fell off my radar for a bit, but I've asked the team for some guidance on the "right" way to handle this today.

Following up on myself after talking with the team, here is how I would suggest handling this:

In the first call to UserProcessBundledParallelTasks, you should:

  1. Store the OSAction you receive into your DEXTs own ivars.

  2. Intentionally retain() that OSAction. This retain will NOT be balanced, so you're intentionally over-retaining the OSAction (it will be destroyed when your DEXT is). You can actually retain it a few times if you want.

  3. On all future calls to UserProcessBundledParallelTasks, assert that the OSAction you receive is the same as the action you received in #1, intentionally crashing if it changes.

Note that the point of #3 is NOT to detect a valid state you should anticipate or "handle". It's purely there as an overall "safety" check that will either never trigger or will trigger years from now for some totally reason. On that second point, I'll also note that this is an EXCELLENT place to add extended comments to save the poor fellow[1] who is lost trying to figure out why you did this years from now.

In any case, if that assert ever triggers, that either indicates that we've changed something fundamental to the overall architecture or a bug in your DEXT has damaged/altered the action you're supposed to be using. Either way, it's better to crash at that point instead of attempting to function in a totally unexpected state.

At some point I hope we'll address and document this (I've suggested making the action a singleton object that disables retain/release) but I'd expect anything we do here to work fine with the flow above.

[1] Always remember, the poor fellow you save might just be you.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin,

Thank you for the guidance. Based on your suggestions, we have implemented architectural modifications. Below is the status of implementation and the current issue.

We store and retain IOMemoryDescriptor and IOMemoryMap objects in ivars. The ISR accesses the shared buffer address, which resolved the issue where GetAddress() returned NULL.

The ISR differentiates between command sources. In Bundled mode, the DEXT calls BundledParallelTaskCompletion without calling release(). In Legacy mode, it calls ParallelTaskCompletion followed by release(). These changes eliminated the 0x92000006 Kernel Panic and DEXT Corpse crashes.

The kernel dispatches Bundled commands immediately after UserInitializeTargetForID returns, but before UserCreateTargetForID completes. We found that reporting command completion while UserCreateTargetForID is still executing causes the UserCreateTargetForID call to hang.

Based on this behavior, we infer a re-entrancy deadlock. The registration thread waits for the SAM target object to be fully instantiated, while the DEXT simultaneously reports an I/O completion for that same target. We believe this causes a locking conflict within the kernel state machine. Since UserCreateTargetForID does not return, the serial discovery queue is blocked, stopping all subsequent target registrations.

Is our understanding correct? How should we resolve the issue we are currently facing?

Best Regards,

Charles

Hi Kevin,

Following up on my previous update regarding the registration hang and SAM layer panic. We performed further experiments using the Selection Timeout (SERVICE_DELIVERY_FAILURE) approach as you suggested. Below are the results:

1. Selection Timeout Experiment Results

We modified the DEXT to report SERVICE_DELIVERY_FAILURE immediately for Bundled commands arriving before the registration returns. We confirmed fControllerTaskIdentifier matches the request.

  • Stability Improvement: With this change, any attempt to unplug the hardware or deactivate the DEXT no longer triggers a Kernel Panic. Resource lifecycle management (retaining/releasing descriptors) is now functioning correctly.
  • Persistent Hang: Despite reporting the timeout, UserCreateTargetForID remains hung indefinitely and never returns on its own.

2. Log Evidence: The "Unlock" Mechanism

The logs show that the kernel registration thread is blocked until a termination signal is received.

Log A: Hang after Selection Timeout

default 14:00:07.773080  kernel  [AsyncCreateTargetForID_Impl] Calling UserCreateTargetForID for LUN 0
default 14:00:07.773519  kernel  [UserInitializeTargetForID_Impl] Target 0 callback success.

// DEXT reports Selection Timeout via Bundled API
default 14:00:07.774781  kernel  [UserProcessBundledParallelTasks_Impl] [Guard] Target 0 not ready. Reporting Selection Timeout.
default 14:00:07.774789  kernel  [UserProcessBundledParallelTasks_Impl] // --- } UserProcess Return

// DEADLOCK: No further output. AsyncCreateTargetForID_Impl execution is interrupted.

Log B: Stop Sequence Unblocking the Queue

Upon issuing the Deactivate command, the original UserCreateTargetForID for LUN 0 returns success immediately after Stop() starts. The serial queue then proceeds to subsequent LUNs.

default 14:02:33.557678  kernel  [Stop_Impl] // { ---
default 14:02:33.558307  kernel  [Stop_Impl_block_invoke] All cancels finished. Calling super::Stop.

// Registration for LUN 0 finally returns success
default 14:02:33.558402  kernel  [AsyncCreateTargetForID_Impl] Target 0 fully registered.
default 14:02:33.558406  kernel  [AsyncCreateTargetForID_Impl] // --- } LUN 0 Exit

// Serial queue proceeds; LUN 1 and LUN 2 fail with kIOReturnAborted during termination
default 14:02:33.558460  kernel  [AsyncCreateTargetForID_Impl] Calling UserCreateTargetForID for LUN 1
default 14:02:33.558484  kernel  [AsyncCreateTargetForID_Impl] Target 1 registration failed: 0xe00002bc
default 14:02:33.558559  kernel  [AsyncCreateTargetForID_Impl] Calling UserCreateTargetForID for LUN 2
default 14:02:33.558634  kernel  [AsyncCreateTargetForID_Impl] Target 2 registration failed: 0xe00002bc

3. Request for Guidance

Our data confirms that reporting either SUCCESS or FAILURE during registration does not allow UserCreateTargetForID to return under normal conditions. It only returns when the DEXT enters its termination phase.

Is there a specific signal, barrier, or status required in Bundled mode to notify the SAM layer that the probe is complete and allow UserCreateTargetForID to return normally? Or should we handle these initial commands differently to avoid this deadlock?

Best Regards,

Charles

First off, revisiting my own comments:

No, I don't think any special synchronization is required. If you're tracking the buffer through a direct pointer, then the relative gap between

Please check your email and the bug site for some specific feedback from the engineering team, as they have some comments specific to your code they wanted to pass back. I don't think they'll change the immediate issue but they're worth noting and integrating.

Is there a specific signal, barrier, or status required in Bundled mode to notify the SAM layer that the probe is complete and allow UserCreateTargetForID to return normally?

No. You're creating the deadlock, not the SAM layer.

Or should we handle these initial commands differently to avoid this deadlock?

What else is attached to the queue UserCreateTargetForID is called on? The answer should be "nothing", as my guess is that you're deadlocking on whatever call looped "back" to UserCreateTargetForID().

In terms of the expected call chain, looking at our code, I'd expect:

  1. UserInitializeTargetForID
  2. UserDoesHBASupportMultiPathing
  3. SAM stack issue INQUIRY

In terms of #3, our code has multiple retries (~8) on most of the code paths I've seen, so if you're only seeing one call, then I think the issue is actually that you're unable to complete the command, presumably because you deadlocked yourself.

The logs show that the kernel registration thread is blocked until a termination signal is received.

This is somewhat misleading. I think what's actually happening here is that kernel side tear down is severing your DEXT connections to the kernel, which basically ends up failing "everything".

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin,

Thank you for your feedback and the feedback from the engineering team. We have integrated all suggestions from your forum posts (ID: 875288022, 875587022) and the Bug Report, and have conducted a full round of testing. Below is our current status.

We now perform a memset on SCSIUserParallelResponse in the ISR and correctly populate the version, fControllerTaskIdentifier, and fBytesTransferred fields. The ISR now differentiates between Bundled/Legacy modes, calling BundledParallelTaskCompletion (without release()) and ParallelTaskCompletion (with release()) accordingly.

The aforementioned fixes have resolved all 0x92000006 Panics and DEXT Corpse crashes. Unplugging the hardware or deactivating the DEXT while the driver is in a hung state no longer triggers a panic.

We are in a logical deadlock. The kernel dispatches a probe command before UserCreateTargetForID returns, and both of our methods for handling this command result in a permanent hang of the registration process:

Scenario A (Reporting SUCCESS): We mark the target as "Ready" during the UserInitializeTargetForID phase, allowing the command to be processed by the hardware and completed as SUCCESS by the ISR (with a fully initialized data structure).

  • Result: UserCreateTargetForID hangs indefinitely. Logs confirm the ISR successfully calls BundledParallelTaskCompletion, but the registration call never returns.

Scenario B (Reporting Selection Timeout): We mark the target as "Ready" only after UserCreateTargetForID returns and intercept the preemptive command in UserProcess... to report SERVICE_DELIVERY_FAILURE.

  • Result: UserCreateTargetForID also hangs indefinitely.

In your latest reply, you suggested the deadlock might be caused by our queue configuration. We have reviewed our architecture:

  • Our UserCreateTargetForID runs on a dedicated serial queue (AuxiliaryQueue).
  • All I/O completions (BundledParallelTaskCompletion) occur on the InterruptQueue or DefaultQueue.
  • There are no shared IOLocks between these execution paths.

Based on this, a queue deadlock within the DEXT seems unlikely.

In both Scenarios A and B, we observed the exact same behavior: the hung UserCreateTargetForID call returns success immediately only when we manually deactivate the driver, triggering the Stop() sequence.

It appears the kernel's registration thread is waiting for a signal that the DEXT has not yet sent, and this signal is only triggered when the driver terminates.

We have now ruled out data structure corruption, API misuse, and DEXT-level queue deadlocks. Is there anything else we should be aware of that we might have missed?

Best Regards,

Charles

First off, I want to start with a clarification here:

We are in a logical deadlock. The kernel dispatches a probe command before UserCreateTargetForID returns, and both of our methods for handling this command result in a permanent hang of the registration process:

Calling "UserCreateTargetForID" means "please create the storage stack for this target". Returning from it means "I've finished creating the storage stack for this target". I'm not sure how far up the stack you'll actually get, but it's conceivable that we'd get all the way through partition map interpretation and (possibly) volume format detection BEFORE UserCreateTargetForID returns. You're basically guaranteed to get I/O request before UserCreateTargetForID returns.

[1] I think the upper levels of the SAM stack prevent this by returning from state before calling registerForService on their IOStorage family nubs, but there's no technical reason why they'd HAVE to work this way.

That leads to here:

We mark the target as "Ready" during the UserInitializeTargetForID phase, allowing the command to be processed by the hardware and completed as SUCCESS by the ISR (with a fully initialized data structure).

I'm still confused by this. Calling "UserCreateTargetForID" means "I'm ready this device to start working", which means your entire I/O "chain" for that device should be "ready" before you call it. In any case, the point here is that you shouldn't call UserCreateTargetForID until you’re ready to handle "arbitrary" I/O requests, just like you would once the controller is fully active.

Moving to here:

Our UserCreateTargetForID runs on a dedicated serial queue (AuxiliaryQueue).

Just to clarify, how does your larger code "around" UserCreateTargetForID actually work?

For reference, our controller does some basic configuration in UserStartController(), calls into AsyncEventHandler, then immediately returns. That handler does some synchronous I/O and DMA configuration, eventually calling UserCreateTargetForID(). However, the critical point here is that "nothing" else is happening on the default queue (where UserStartController() was called) once the creation process for UserCreateTargetForID starts.

Also, just to be clear, ONLY two methods should be tied to that method. Those are the declarations of UserCreateTargetForID which you inherit:

virtual kern_return_t
UserCreateTargetForID ( SCSIDeviceIdentifier    targetID,
						OSDictionary *          targetDict ) QUEUENAME ( AuxiliaryQueue );

And the HandleAsyncEvent method you set up to call it through:

virtual void
AsyncEventHandler     ( OSAction *					action TARGET,
						kern_return_t				status ) = 0;

virtual void
HandleAsyncEvent ( OSAction *				action,
				   kern_return_t			status )
				   TYPE ( ExampleSCSIDext::AsyncEventHandler ) QUEUENAME ( AuxiliaryQueue );

Covering the other details, the setup code for this looks like this:

kern_return_t
IMPL ( ExampleSCSIDext, UserInitializeController )
{
	kern_return_t ret = kIOReturnSuccess;
...	
	ret = IODispatchQueue::Create ( "FCQ", 0, 0, &ivars->fQueue1 );
	assert(kIOReturnSuccess == ret);
	
	ret = SetDispatchQueue ( "AuxiliaryQueue", ivars->fQueue1 );
	assert ( kIOReturnSuccess == ret );
		
	ret = CreateActionHandleAsyncEvent ( sizeof ( void * ), &ivars->fAsyncEventHandler );
	assert ( kIOReturnSuccess == ret );
...
}

And the call to it like this:

kern_return_t
IMPL ( AppleLSIFusionFC, UserStartController )
{
	kern_return_t ret = kIOReturnSuccess;
...

	AsyncEventHandler ( ivars->fAsyncEventHandler, kIOReturnSuccess );

	return ret;
}

It appears the kernel's registration thread is waiting for a signal that the DEXT has not yet sent, and this signal is only triggered when the driver terminates.

Sort of. I think what's actually happening is that the kernel is issuing an I/O request, then blocking because that I/O request isn't returning properly. In terms of direct investigation, the main thing to look at here is spindump while your DEXT is hung:

sudo spindump -o <destination path>

The spindump file will include the kernel frames which should, in theory, show what the kernel is actually hung on. However, if you want me to look into this, then please do that following:

  • Make sure your DEXT is logging as much as it possibly can. I'd log every method entry and exit, along with additional logging every time you call into the kernel. Basically, you want your DEXT to be as visible as possible in the system console.

  • Reproduce the hang, then trigger a sysdiagnose.

  • Pull the device to trigger driver teardown, let everything finish, then trigger another sysdiagnose.

Label both sysdiagnoses so I know which is which, then upload both of them to your bug. FYI, I'm asking for two sysdiagnoses (instead of just doing one after the test finishes) because I want to see the spindump and registry state at the point of the hang plus the log data from the hang. I don't necessarily "need" the second sysdiagnose, but it's possible that logging from teardown will show me something interesting.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin,

Thank you for your previous guidance regarding memory ownership and OSAction lifecycle management. The Bundled Mode implementation is now stable, and the driver operates without crashes.

We have conducted performance stress tests using an LSI 3108 RAID controller via Thunderbolt 3, configured with 4 HDDs in RAID 5. The results from AJA System Test Lite (4GB, 10bit RGB) and fio (BS=1M, iodepth=32, Direct=1) show that I/O throughput in Bundled Mode remains at approximately 800 MB/s, which is identical to the performance measured in Legacy Mode.

Analysis of the driver logs during these tests reveals that the count parameter in UserProcessBundledParallelTasks is consistently 1, even under high queue depth. The batching mechanism intended to reduce RPC and context switch overhead does not appear to be active.

Our implementation for reporting device capacity and task limits follows the standard IOUserSCSIParallelInterfaceController specifications. UserReportHighestSupportedDeviceID returns an ID of 63, and UserReportMaximumTaskCount is set to 64 as shown below:

kern_return_t IMPL(DRV_MAIN_CLASS_NAME, UserReportHighestSupportedDeviceID)
{
    *id = 63;
    return kIOReturnSuccess;
}

kern_return_t IMPL(DRV_MAIN_CLASS_NAME, UserReportMaximumTaskCount)
{
    *count = 64;
    return kIOReturnSuccess;
}

The processing entry point consistently reports only a single slot per call:

void 
DRV_MAIN_CLASS_NAME::UserProcessBundledParallelTasks_Impl(
        const uint16_t indices[32],
        uint16_t count,
        OSAction * completion)
{
    // Log consistently shows 'count' is 1
    uLog("[BUNDLED] Mode enabled, processing %u slots", count);
    
    if (!ivars->useBundledMode || !ivars->fCommandBuffers || !ivars->fResponseBuffers) {
        return;
    }
    
    for (uint16_t i = 0; i < count; i++) {
        uint16_t slotIndex = indices[i];
        // Dispatch logic...
    }
}

Example of the console log output during fio testing with iodepth=32:

default 20:45:33.098031+0800 kernel [UserProcessBundledParallelTasks_Impl] [BUNDLED] Mode enabled, processing 1 slots

Given these observations, we would like to understand the specific conditions or thresholds required for the kernel's block layer to batch multiple SCSI commands into a single RPC call. We are interested in whether the kernel's bundling logic prioritizes high IOPS (Random I/O) over high bandwidth (Sequential I/O), and if the 1MB block size used in our tests might cause the kernel to dispatch commands immediately to minimize latency instead of waiting to bundle them.

Furthermore, we would like to verify if there are other reported parameters beyond UserReportMaximumTaskCount that influence the frequency of batching, or if there is a recommended method to determine if the macOS Block Layer is attempting to bundle commands before they reach the DEXT layer.

Thank you for your expertise and time.

Best Regards,

Charles

We have conducted performance stress tests using an LSI 3108 RAID controller via Thunderbolt 3, configured with 4 HDDs in RAID 5. The results from AJA System Test Lite (4GB, 10-bit RGB) and fio (BS=1M, iodepth=32, Direct=1) show that I/O throughput in Bundled Mode remains at approximately 800 MB/s, which is identical to the performance measured in Legacy Mode.

Do either of these tests generate parallel I/O (meaning, multiple threads are generating I/O to the same target)? More specifically, I'd look at:

  • Parallel I/O within the test itself.

  • I/O targeting different partitions on the same target.

  • I/O targeting unrelated targets on the same controller.

Also, what does your overall I/O flow look like? Is your card receiving commands fast enough that it's processing them in parallel, even though they arrived as separate commands? What's your peak simultaneous task count?

Analysis of the driver logs during these tests reveals that the count parameter in UserProcessBundledParallelTasks is consistently 1, even under high queue depth. The batching mechanism intended to reduce RPC and context switch overhead does not appear to be active.

One quick note here is that the batching mechanism is actually trying to address two different issues simultaneously:

  1. Batching commands so as to reduce the number of IPC calls between your DEXT and the kernel.

  2. Preallocate and reuse I/O buffers to reduce the overhead of copying data in/out of the kernel.

Note that the first one in particular primarily benefits cases where the system is flooded with LOTS of very small I/O requests (eg ~4k). Otherwise, the I/O cost itself becomes the primary performance factor.

We are interested in whether the kernel's bundling logic prioritizes high IOPS (Random I/O) over high bandwidth (Sequential I/O), and if the 1MB block size used in our tests might cause the kernel to dispatch commands immediately to minimize latency instead of waiting to bundle them.

I think "bundling logic" overstates what's actually going on here. By the time the command reaches the SCSI controller layer, the system isn't really trying/willing to "hold" any request, so it's going to try and send it to your driver "as soon as possible". The way this actually works is that:

  1. The kernel driver receives and processes requests on one thread (preparing them for your DEXT).

  2. Once the command is prepped, it's transferred to another thread (which actually sends the command to your DEXT).

If the total command volume is relatively low, that process is effectively "serial“ - the send queue is always idle when the prep queue has a command ready, so it sends the command "immediately". As command volume increases, you'll eventually reach the point where the send queue IS busy (because it's sending a command to your DEXT), so that command is prepped.

Where bundled I/O matters is in how those queued commands are handled - bundled I/O means that the kernel can send that "backlog" in a single bundled command instead of sending individual commands for each entry.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin,

Thank you for your guidance on the UserProcessBundledParallelTasks architecture and IPC optimization. After several adjustments, our DEXT driver is now running stably. We have completed a comprehensive performance comparison between Bundled Mode and Legacy Mode. Below is a summary of our findings.

Following your suggestion, we analyzed the console logs during 4K Random I/O stress tests (iodepth=32). By analyzing approximately 6,363 calls, we confirmed that the kernel actively performs command batching:

Results:

Slots per Call (Count)FrequencyObservation
1 Slot5,654Standard/Low load; kernel dispatches immediately.
2 Slots628Active Batching initiated under increased pressure.
3 - 4 Slots75Sustained high-concurrency batching.
5+ Slots6High-load peaks (Max observed: 11 slots per call).

This data confirms that the batching mechanism effectively reduces the number of RPC/IPC calls to the DEXT when the system is under pressure.

We conducted benchmarks using fio on RAID 5 array (4 HDDs) connected via Thunderbolt 3 (4K Random R/W, Buffered I/O, 4 Jobs, Queue Depth 32).

Key Performance:

  • Mixed Random (70% Read / 30% Write):
    • Read Throughput: 278 MiB/s (Legacy) → 536 MiB/s (Bundled) [~1.93x improvement]
    • Write Throughput: 120 MiB/s (Legacy) → 231 MiB/s (Bundled) [~1.92x improvement]
  • Random Read (4K):
    • Throughput: 121 MiB/s (Legacy) → 142 MiB/s (Bundled) [~17% improvement]
  • Random Write (4K):
    • Performance was comparable (~35-36 MiB/s), limited by the physical seek characteristics of the HDDs.

As you anticipated, the advantages of Bundled Mode are most significant in mixed random workloads, where we observed nearly a 2x increase in throughput. This validates that reducing IPC overhead by utilizing shared command/response buffers significantly boosts performance when the system is saturated with a large volume of small I/O requests.

Thank you again for your expert assistance and professional advice throughout this process.

Best regards,

Charles

System Panic with IOUserSCSIParallelInterfaceController during Dispatch Queue Configuration
 
 
Q