Skip to content

[WIP] Explore BESTWAIT hardware steering for scheduler optimization#57

Draft
zbelinsk wants to merge 3 commits into
masterfrom
bestwait-hw-steering
Draft

[WIP] Explore BESTWAIT hardware steering for scheduler optimization#57
zbelinsk wants to merge 3 commits into
masterfrom
bestwait-hw-steering

Conversation

@zbelinsk

Copy link
Copy Markdown
Contributor

This commit explores using the BESTWAIT register for hardware-assisted scheduling decisions in the reference implementation. The goal is to investigate whether BESTWAIT can improve scheduler performance or enable different implementation strategies.

What was attempted

  • Added BESTWAIT register support to hw.h/hw.spec
  • Implemented test infrastructure (H2K_bestwait test suite)
  • Modified check_sanity.ref.c to explore BESTWAIT-based scheduling
  • Updated futex_pi and setup reference implementations
  • Attempted to implement opt version of check_sanity with BESTWAIT

BESTWAIT is a global priority register that:

  • Holds the priority of the highest-priority task waiting to run
  • Triggers a reschedule interrupt when ANY thread's effective priority becomes worse than the BESTWAIT value
  • Does NOT deliver interrupts to specific threads; it's a global signal

Critical blockers preventing full implementation

  1. Cannot access per-thread STID.PRIO from software

    • The STID register (per-thread state) is not accessible from other threads
    • This prevents reading the effective priority of other threads
    • Blocks the core comparison logic needed for proper BESTWAIT usage
  2. Resched function cannot implement priority handoff

    • The H2K_dosched() function needs to compare thread priorities to determine which thread should handle the reschedule interrupt
    • Without access to STID.PRIO, we cannot implement the required logic

Performance implications

  • Current check_sanity.ref implementation helps performance
  • Attempted opt version also shows NO improvement over current implementation
  • The reference implementation remains exploratory

Only in ref:

The opt version was attempted but provides no performance benefit. The ref version is kept to document the exploration and architectural constraints for future scheduler work.

Next steps

  • Investigate if STID.PRIO can be exposed via privileged interface
  • Consider alternative scheduling implementation accommodating the bestwait reg usage

@github-actions github-actions Bot added the untested Mark untested PRs label Jun 21, 2026
@zbelinsk zbelinsk marked this pull request as draft June 22, 2026 07:03
@zbelinsk

Copy link
Copy Markdown
Contributor Author

Update- was reconsidering together with andreykarpenko-qc the current architecture.
It would be helpful if hardware would also reflect not just "resched interrupt sent", but also the least prioritized hw thread.
This way, we can drop the two places in code where we read the software array of running threads:

  1. kernel/sched/check_sanity/check_sanity.ref.c- in the if (!priomask) section.
  2. kernel/sched/dosched/dosched.ref.c- if ((H2K_gp->wait_mask == 0) && ...... section.

Since BESTWAIT is a comparator scanning all hw threads, it could be a cheap arch change. BESTAWIT could reflect not only "I have fired" but also "hw thread x is least prioritized". This helps with check_sanity.ref.c, but not for the dosched.ref.c- it requires a different mechanism rather than BESTWAIT- which sends interrupt (exactly what check_sanity does).

If we had "hardware way" for these two sites, we could drop runlist array from the code.

@bryanb-h2

Copy link
Copy Markdown
Contributor
  • The STID register (per-thread state) is not accessible from other threads

  • This prevents reading the effective priority of other threads

  • Blocks the core comparison logic needed for proper BESTWAIT usage

Might be a good idea, but I don't understand this stuff from the summary:

  • The STID register (per-thread state) is not accessible from other threads
  • This prevents reading the effective priority of other threads
  • Blocks the core comparison logic needed for proper BESTWAIT usage

@zbelinsk

zbelinsk commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author
  • e STID register (per-thread state) i
  • The STID register (per-thread state) is not accessible from other threads
  • This prevents reading the effective priority of other threads
  • Blocks the core comparison logic needed for proper BESTWAIT usage

Might be a good idea, but I don't understand this stuff from the summary:

  • The STID register (per-thread state) is not accessible from other threads
  • This prevents reading the effective priority of other threads
  • Blocks the core comparison logic needed for proper BESTWAIT usage

In the kernel sched code, we are keeping scheduling going, by doing two things:

  1. Unmasking least prioritized hthread.
  2. Sending the interrupt.

Today we have the software runlist array, for achieving the described functionallity- we query it. If we want to offload it to hardware, we need to query somehow- "who is the least prioritized"- which can't be done, since we are not able to access STID.PRIO of hthread Y from hthread X for reading, tho we can set that by using set_thread_stid_prio setter I have added in this pr.

Using BESTWAIT in the current state, while we need to update also the software runlist- produces no benefit (packets counting wise, check_sanity.opt is ~10 packets already).

@zbelinsk

zbelinsk commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Was thinking about this again, here are my conclusions:

Current state:

The BESTWAIT register currently works as follows:

  • Receives the lowest ready thread priority value in the lowest 9 bits.
  • Outputs an indication of "did I fire?" in the lowest 9 bits. The value is 0x1FF if fired, which is bigger than the worst possible priority (0x0FF).
  • We know for sure that if we send 0x1FF in the lowest 9 bits of BESTWAIT, it will not fire.

What can be added:

BESTWAIT is already a comparator that iterates over all HW threads. This means it can return not only "did I fire?" but also "did I fire, and who is the lowest-priority HW thread?", since the comparison across all HW threads is already being performed.

With that being said, if I want to fire an interrupt for reschedule, the content of the new proposed BESTWAIT would be (all described inputs and outputs are BESTWAIT reg):

input: [XXXXXXXX...XXX | lowest ready prio]
output: if fired -> [highest priority running hw thread | 0x1FF]
if not fired -> [highest priority running hw thread | lowest ready prio]

If I only want to retrieve the highest-priority running thread (without sending an interrupt):

input: [XXXXXXXX...XXX | 0x1FF]
output: always -> [highest priority running | 0x1FF]

Remark that 0x1FF means "cannot interrupt anyone", it is effectively higher than the highest possible priority (0x0FF).

This way, we reuse existing hardware in order to query the lowest running hw thread number at any given time.

Small limitation:

Since a HW thread priority is 8 bits long, a waiting thread is represented as priority 255 (0xFF, i.e. -1).

Consider the case where we have:

  • A sleeping HW thread (-1 == 0xFF == 255)
  • A ready thread with priority 255

In this case, we would not fire an interrupt because both values are identical.

To overcome this, we can continue using H2K->wait_mask (which already exists in the current implementation, no further implementation needed), allowing us to distinguish which HW threads are actually waiting.

Big win, offloading from software to hardware:

  • In check_sanity we keep the implementation proposed in this pr- but making H2K_lowprio_notify() to receive also the highest priority hw thread, since it can be queried from BESTWAIT post interrupt firing.
  • Also, in dosched, the line: if ((H2K_gp->wait_mask == 0) && (new->prio IS_WORSE_THAN H2K_runlist_worst_prio()))....
    Will not be software iterating runlist any more- we can receive it from the new proposed BESTWAIT: input [XXXXXXXX...XXX | 0x1FF] => output: [highest priority running | 0x1FF].

These are the only call sites across the code in which H2K_runlist_* API is used for anything that is not insert to or removing from runlist. Arguably, with this approach we could avoid having the H2K_runlist at all, since handled in hardware.

Screenshot 2026-07-08 203026

@eplondke Erich Plondke (eplondke) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking pretty good! Probably the easy thing for now is to update BESTWAIT at check_sanity time, but then eventually move it to the time where we update the readylist.

There are probably lots of scheduler-related tests that check for how we maintain the IMASK stuff, as we make it simpler a lot of those tests will have to change.

I do think you should add a new document specifically about how we're changing the scheduler, and especially highlight that with the new interrupt logic:

  • If all the threads have their IMASK enabled for interrupts, the hardware delivers the interrupt to the hardware thread with the worst STID.PRIO field
  • The hardware will compare the STID.PRIO field for all hardware threads and the BESTWAIT register, and if there is a hardware thread that is worse than the BESTWAIT register it will automatically raise the reschedule interrupt (which would then be steered to the worst hardware thread as above)
  • This should greatly simplify dosched and check_sanity

H2K_runlist_set_thread_prio(dest, prio);
/* Sync hardware STID.PRIO so BESTWAIT sees the boosted priority
* immediately -- avoids spurious preemption of the holder. */
set_thread_stid_prio(dest->hthread, prio);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should become the "setprio" instruction after the inline after the change, I think?

* comparator reads STID.PRIO; without the hardware sync the stale
* boosted value hides me from the comparator causing a priority inversion. */
H2K_runlist_set_thread_prio(me, me->base_prio);
set_thread_stid_prio(me->hthread, me->base_prio);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could also be the "setprio" instruction or re-writing our whole STID register

@@ -0,0 +1,208 @@
/*

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're getting rid of maintaining IMASK a lot of these tests will need to change. The good news is that hopefully they will get simpler?

@@ -11,18 +11,25 @@
#include <readylist.h>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can write BESTWAIT here, or we can do it when we update the ready list (that would be more exact!)
Let's assume we update BESTWAIT when we update the ready list.

  • No need to update lowprio interrupt masks; interrupt steering is in hardware
  • No need to raise reschedule interrupt, reschedule interrupt raised by hardware
  • No need to check wait mask / etc
  • check_sanity becomes empty, check_sanity_unlock becomes just the unlock of the BKL.

Comment thread kernel/util/hw/hw.h
return imask;
}

/* Write STID.PRIO for a named hardware thread (cross-thread, Monitor only).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent

Comment thread kernel/util/hw/hw.h

asm volatile ("dmcfgwr(%0, %1);" : : "r" (index), "r" (data));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good.

Comment thread kernel/util/hw/hw.spec
This function retrieves clears the contents of the IPEND status register.


H2K_get_bestwait

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your documentation!

This commit explores using the BESTWAIT register for hardware-assisted
scheduling decisions in the reference implementation. The goal is to
investigate whether BESTWAIT can improve scheduler performance or enable
different implementation strategies.

## What was attempted

- Added BESTWAIT register support to hw.h/hw.spec
- Implemented test infrastructure (H2K_bestwait test suite)
- Modified check_sanity.ref.c to explore BESTWAIT-based scheduling
- Updated futex_pi and setup reference implementations
- Attempted to implement opt version of check_sanity with BESTWAIT

BESTWAIT is a global priority register that:
- Holds the priority of the highest-priority task waiting to run
- Triggers a reschedule interrupt when ANY thread's effective priority
  becomes worse than the BESTWAIT value
- Does NOT deliver interrupts to specific threads; it's a global signal

## Critical blockers preventing full implementation

1. **Cannot access per-thread STID.PRIO from software**
   - The STID register (per-thread state) is not accessible from other threads
   - This prevents reading the effective priority of other threads
   - Blocks the core comparison logic needed for proper BESTWAIT usage

2. **Resched function cannot implement priority handoff**
   - The H2K_dosched() function needs to compare thread priorities to determine
     which thread should handle the reschedule interrupt
   - Without access to STID.PRIO, we cannot implement the required logic
   - This is the blocker for steering interrupts correctly

## Performance implications

- Current check_sanity.ref implementation helps performance
- Attempted opt version also shows NO improvement over current implementation
- The reference implementation remains exploratory

## Why only in ref?

The opt version was attempted but provides no performance benefit. The ref
version is kept to document the exploration and architectural constraints
for future scheduler work.

## Next steps

- Investigate if STID.PRIO can be exposed via privileged interface
- Consider alternative scheduling implementation accommodating the bestwait reg usage

Signed-off-by: Zeev Belinsky <zbelinsk@qti.qualcomm.com>
@zbelinsk zbelinsk force-pushed the bestwait-hw-steering branch from 3db7bc1 to 4239f0c Compare July 12, 2026 05:54
zbelinsk added 2 commits July 13, 2026 15:56
Replace the software low-priority steering (priomask / lowprio_imask /
H2K_runlist_worst_prio) with the hardware BESTWAIT + SCHEDCFG[8] priority
comparator. check_sanity now arms BESTWAIT with the best ready priority and
lets hardware post RESCHED to the worst-priority (STID.PRIO) hardware thread;
a software resched_int() backstop covers the 255==255 case the comparator
cannot fire on- will be removed in the upcoming changes. Boot opens IMASK (imask=0) so every thread is a steering
candidate, and setup drops H2K_lowprio_init.

runlist_worst_prio/_hthread, lowprio_notify/raise, highprio/lowprio_imask,
and the priomask bookkeeping in dosched/check_sanity are removed. The
runlist[]/runlist_prios[] mirror arrays are no longer read by the scheduler.
MAX_PRIO is split into WAITING_PRIO (255, the idle/sentinel priority) and
MAX_READY_PRIO (254, the cap for runnable threads) to match the comparator's
strict-greater semantics- will be applied in upcoming changes. runlist and lowprio unit tests are disabled in the
testlist accordingly.

Known scheduler-callsite races (documented in-tree, not yet root-caused):

- dosched go-to-sleep: change_imask(hthread,0) is issued before H2K_switch.
  It is logically redundant with the imask=0 the switch sleep path already
  does, but removing it loses wakeups -- the thread must become a valid
  RESCHED steering target (IMASK[RESCHED]=0) early enough for a producer's
  set_bestwait to steer to it. Without it the futex/pi test hangs.

- dosched H2K_runlist_push(new), and the H2K_runlist_remove(me) in yield and
  resched: these now write to throwaway wip_dummy_runlist[] arrays that
  nothing reads. The writes are retained only because removing them hangs the
  futex_pi test: the extra stores shift interrupt-arrival timing on the
  scheduler hot path enough to keep a wakeup/steering window from being lost.
  This is a timing shim over a real lost-wakeup race in the ready-append ->
  set_bestwait -> steer sequence, not a data dependency; every other callsite
  is fine without them.

- The H2K_runlist_remove(me) calls in yield.ref.c and resched.ref.c are the
  same case: dropping their dummy-array writes also hangs the futex_pi test,
  for the same timing reason as the dosched push above.

- The whole H2K_runlist mechanism (push/remove/set_thread_prio and the
  runlist[]/runlist_prios[] arrays) could be removed entirely across all
  callsites once this timing sensitivity is fixed: the arrays are write-only
  now (nothing reads them), so they carry no data dependency -- only the
  incidental instruction-timing that currently masks the race.

v81/ref: 135 tests pass, including kernel/futex/futex/test/tests/pi. Other test_variants havent been tested.

Signed-off-by: Zeev Belinsky <zbelinsk@qti.qualcomm.com>
Signed-off-by: zbelinsk <zbelinsk@qti.qualcomm.com>

@eplondke Erich Plondke (eplondke) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we go to idle do you change STID.PRIO to 255 before you go to sleep? I think that's in "switch".

/* Check whether a thread at a given priority is ready */
static inline u32_t H2K_ready_prio_valid(u32_t prio)
{
if (prio > MAX_PRIO) return 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

recommend keeping MAX_PRIO and setting it to 254 or smaller.

}
}
/* FIXME: check pending intpool interrupts */
H2K_runlist_remove(me);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting rid of the runlist entirely is a little scary... very cool if we could do that though!

@@ -348,10 +348,11 @@ boot_continuation:
SETCONST(H2K_GP,H2K_kg)

/* Set up rising edge triggered */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably want to fix the comments.

DPRINTF("SET_PRIO_TRAPMASK\n\n");
/* SET_PRIO_TRAPMASK bad prio */
ret = H2K_trap_config(CONFIG_VMBLOCK_INIT, vm, SET_PRIO_TRAPMASK, MAX_PRIO + 1, 0, NULL);
ret = H2K_trap_config(CONFIG_VMBLOCK_INIT, vm, SET_PRIO_TRAPMASK, WAITING_PRIO + 1, 0, NULL);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again I'd keep MAX_PRIO but change it to 254.

@eplondke

Copy link
Copy Markdown
Contributor

We want IMASK to stay clear for all threads with this change. You might want to look for all instances of setting IMASK directly (with IMASK = R) or indirectly (with setimask)... and of course you can get a break point in --interactive mode in the simulator and see if any threads have their IMASK set. If we are setting any IMASK bit to nonzero the hardware won't steer the interrupt to the right place (it will go to any unmasked thread instead of the worst priority one).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

untested Mark untested PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants