NRS Fairshare
ThemisIO → Lustre NRS: Synthesized Implementation Plan
Problem Statement
The SC23 paper Fine-grained Policy-driven I/O Sharing for Burst Buffers (ThemisIO) demonstrates that statistical-token-based scheduling with opportunity fairness, transition-matrix composite policies, and δ-delayed global fairness achieves 13.5–13.7% higher throughput and 59.1–99.8% lower I/O interference slowdown than FIFO, TBF, and GIFT. Lustre's NRS subsystem already provides the right request-reordering hook points, but none of the current policies (FIFO, CRR-N, ORR/TRR, TBF, Delay) implement ThemisIO's core innovations.
The goal: bring ThemisIO's useful ideas into lustre/ptlrpc/nrs* as a new
nrs_fairshare policy, without breaking NRS semantics or disrupting existing
policies.
ThemisIO Core Ideas
| Concept | Description | Lustre Analog |
|---|---|---|
| Statistical tokens | Divide [0,1] into segments proportional to fair-share; draw random number to pick entity. Replaces multi-tier token queues with single flat mechanism. | None—TBF uses deterministic token buckets with admin-set rates. |
| Transition-matrix composition | Composite policies (e.g., group→user→size-fair) expressed as chain of transition matrices; product yields flat [0,1] ranges. O(1) runtime per dequeue. | None—TBF rules are flat ordered list with first-match semantics. |
| Opportunity fairness | Enforce fairness only when demand exceeds capacity. Under-loaded systems pass requests at full speed. | None—TBF always rate-limits. |
| δ-delayed global fairness | Periodic all-gather (δ ≈ 50–500 ms) across servers synchronizes job-status table so local fair-share ranges converge to global fairness. | TBF tbf_global_rate handles cross-CPT token stealing within one server, but no cross-server mechanism.
|
| Runtime metadata only | Job-id, user-id, job-size read from I/O request at runtime. No admin-supplied rates or offline profiling. | TBF classifies by jobid/uid/gid/projid/nid/opcode, but rates are admin-configured. |
Gap Analysis: TBF vs. ThemisIO
- Static vs. dynamic allocation — TBF requires admin-specified RPC/s per rule. ThemisIO derives fair shares automatically from active entities.
- No composite policies — TBF rules are a flat ordered list with first-match semantics. A combined key
uid+jobiddoes NOT encode user-then-job fairness correctly—it creates a flat cross-product, not a hierarchy. ThemisIO handles this natively via matrix composition. - No opportunity fairness — TBF always rate-limits, even when idle.
- No cross-server coordination —
tbf_global_ratehandles cross-CPT token stealing within one server. No cross-OST/MDS δ-fairness. - Cost model mismatch — TBF uses
cost_model=rpcs|pages. ThemisIO uses I/O cycles (wall-clock time slicing).
Architectural Decision: Single New Policy
Decision: Build nrs_fairshare as a new standalone NRS policy. Do NOT extend TBF.
Rationale:
- TBF's flat first-match rule list cannot express hierarchical fair-share without restructuring its core data model. Bolting composite policies onto TBF would be nearly as much work as a new policy, while carrying regression risk for existing TBF users.
- NRS enforces one-primary-policy-per-head (see
nrs_policy_primaryinstruct ptlrpc_nrs), so "combine fairshare + TBF" means a single new policy, not two side-by-side. - A new additive policy has zero disruption to existing TBF, ORR, CRR-N users.
- The new policy reuses TBF's classifier layer (jobid/uid/gid/projid/nid/opcode key extraction) and debugfs patterns, but owns scheduling.
Design Specification
File Layout
lustre/include/lustre_nrs_fairshare.h — public header (data structures, enums) lustre/ptlrpc/nrs_fairshare.c — policy implementation
Key Data Structures
/*
* Sharing entity types that can appear in a policy hierarchy.
*/
enum nrs_fs_entity_type {
NRS_FS_ENTITY_JOBID,
NRS_FS_ENTITY_UID,
NRS_FS_ENTITY_GID,
NRS_FS_ENTITY_PROJID,
NRS_FS_ENTITY_NID,
__NRS_FS_ENTITY_TYPE_MAX,
};
/*
* A sharing entity: one job, one user, one group, etc.
* Tracks active/inactive state, accumulated I/O cost, and the
* computed statistical-token range [fs_lo, fs_hi) ⊂ [0,1).
*
* Entities are stored in a per-head rhashtable keyed by
* (entity_type, key_bytes).
*/
struct nrs_fs_entity {
struct rhash_head fse_rhash;
struct rcu_head fse_rcu;
refcount_t fse_ref;
enum nrs_fs_entity_type fse_type;
/* Variable-length key (jobid string, uid, nid, etc.) */
u32 fse_key_len;
u8 fse_key[]; /* flexible array */
/* Statistical token range assigned by last δ-recalculation.
* Stored as fixed-point u32 fractions of U32_MAX for fast
* comparison against get_random_u32().
*/
u32 fse_lo; /* inclusive */
u32 fse_hi; /* exclusive */
/* Active/backlogged state */
bool fse_active; /* has queued RPCs */
ktime_t fse_last_seen; /* heartbeat for liveness */
/* Per-entity sub-queue of pending requests (FIFO within entity).
* Optional: offset-ordered sub-queue for ORR-style locality on
* spinning-disk OSTs (Stage 4 enhancement).
*/
struct list_head fse_req_list;
unsigned long fse_req_count;
/* Observability counters */
u64 fse_dispatched; /* total RPCs dispatched */
u64 fse_cost_pages; /* total page cost dispatched */
u64 fse_opportunity; /* RPCs dispatched via opp-fair */
s64 fse_fairness_debt; /* cumulative over/under-share */
};
/*
* Per-NRS-head (per-CPT) policy state.
*/
struct nrs_fs_head {
struct ptlrpc_nrs_resource fsh_res;
/* Entity hash table */
struct rhashtable fsh_entity_hash;
struct rhashtable_params fsh_hash_params;
/* Current policy configuration */
enum nrs_fs_entity_type fsh_levels[__NRS_FS_ENTITY_TYPE_MAX];
int fsh_depth; /* number of hierarchy levels */
/*
* Flattened transition-matrix product: array of (entity_ptr, lo, hi)
* triples representing the [0,1) range assignment for each leaf entity.
* Recomputed every δ interval. Protected by RCU: the δ-recomputation
* thread publishes a new array; op_req_get readers dereference under
* rcu_read_lock.
*/
struct nrs_fs_range __rcu *fsh_ranges;
int fsh_range_count;
/* Opportunity fairness threshold: if nrs_req_queued (from the
* parent ptlrpc_nrs) is below this, bypass fairshare and dequeue
* FIFO across all entities.
*/
unsigned long fsh_opp_threshold;
/* δ-recalculation interval in ms (default 100, tunable 10–1000) */
unsigned int fsh_delta_ms;
/* Total active entities on this CPT (used for equal-share default) */
atomic_t fsh_active_count;
/* Number of CPTs for this service (for cross-CPT normalization) */
int fsh_ncpts;
/* Sequence counter incremented on each δ-recalculation */
u64 fsh_generation;
/* Cost model: pages (default) or rpcs */
enum nrs_tbf_cost_model fsh_cost_model;
};
/*
* Flattened range entry (leaf of the transition-matrix product).
*/
struct nrs_fs_range {
struct nrs_fs_entity *fsr_entity;
u32 fsr_lo;
u32 fsr_hi;
};
/*
* Per-request NRS private data (stored in nrq->nr_u).
*/
struct nrs_fs_req {
struct nrs_fs_entity *fsr_entity; /* owning entity */
struct list_head fsr_list; /* linkage in entity sub-queue */
u64 fsr_cost; /* page or RPC cost */
ktime_t fsr_enqueue_time;
};
Scheduling Flow (op_req_get)
nrs_fairshare_req_get(policy, peek, force):
head = policy->pol_private
1. OPPORTUNITY FAIRNESS CHECK
If policy->pol_nrs->nrs_req_queued < head->fsh_opp_threshold:
→ Scan all active entities, dequeue the request with the
earliest enqueue timestamp (global FIFO).
→ Mark the dispatched request as opportunity-dispatched
(fse_opportunity++).
→ Return request.
2. STATISTICAL TOKEN DRAW
sample = get_random_u32()
ranges = rcu_dereference(head->fsh_ranges)
Binary search ranges[] for the entry where fsr_lo <= sample < fsr_hi.
winner = ranges[match].fsr_entity
3. ENTITY SUB-QUEUE DEQUEUE
If winner->fse_req_list is non-empty:
→ Dequeue oldest request (FIFO within entity).
→ Update winner->fse_dispatched, fse_cost_pages, fse_fairness_debt.
→ If winner->fse_req_list is now empty, mark fse_active = false.
→ Return request.
Else (entity exhausted between recomputation and dequeue):
→ Fall through to FIFO dequeue across all entities as fallback.
4. FORCE / PEEK
Handle peek (return without removing) and force (ignore fairness,
return any queued request) per NRS contract.
Transition-Matrix Composition
For a configured policy like uid_then_jobid_fair:
- Level 0 matrix M₀: rows = 1 (root), columns = active UIDs. Each column gets equal share: 1/num_active_uids.
- Level 1 matrix M₁: rows = UIDs, columns = active jobids per UID. Each column within a row gets equal share: 1/num_jobs_for_that_uid.
- Product P = M₀ × M₁ yields a flat vector of (leaf_entity, share) pairs.
- Map shares to [0, U32_MAX) ranges: entity_i gets [sum(shares[0..i-1]) × U32_MAX, sum(shares[0..i]) × U32_MAX).
For primitive policies (e.g., jobid_fair), depth = 1, no matrix
multiplication needed—just equal partitioning of [0, U32_MAX).
Implementation note: In kernel space, avoid floating-point. Use u64 fixed-point arithmetic (e.g., shares as fractions of U32_MAX). The matrix product is computed in the δ-recalculation kthread, not on the hot dispatch path.
Cost Model
Decision: Use page-weighted cost (reuse TBF's NRS_TBF_CM_PAGES
infrastructure) as the initial token cost basis.
- For BRW (bulk read/write) RPCs: cost = number of pages in the bulk transfer.
- For non-BRW RPCs: cost = 1.
- This naturally weights large and small I/Os without explicit knobs, and approximates ThemisIO's "I/O cycle" concept within Lustre's RPC-oriented dispatch model.
- Wall-clock time-slicing (true ThemisIO analog) deferred to a future enhancement—it would require tracking per-entity dispatch time in
op_req_stop, which adds overhead to the completion hot path.
Cross-CPT Fairness (δ-Delayed, Intra-Server)
Within one server, each service partition (CPT) runs its own nrs_fs_head.
Without coordination, a 4-CPT server could over-allocate shares.
Mechanism (analogous to tbf_global_rate):
- A per-service kthread (or hrtimer callback) wakes every
fsh_delta_ms. - It reads active entity sets from all CPTs for this service.
- It computes the global transition-matrix product (union of all active entities across CPTs, equal shares).
- It publishes the new
fsh_rangesarray to each CPT'snrs_fs_headvia RCU pointer swap (rcu_assign_pointer). - Each CPT's
op_req_getreads viarcu_dereference— zero contention on the dispatch hot path.
Cross-server (cross-OST/MDS) fairness: Out of scope for v1. Would require a new control RPC or piggybacking on LNet ping. Documented as a future phase.
ORR-Style Locality Hybrid (Stage 4)
Fair-share scheduling may regress I/O locality on spinning-disk OSTs because requests from different entities are interleaved, breaking sequential access patterns.
Mitigation: Within each entity's sub-queue (fse_req_list), optionally
sort BRW requests by object and offset (reusing ORR's key structure from
nrs_orr.c). The fairshare policy selects WHICH entity to serve; the entity
sub-queue determines the ORDER within that entity's batch.
- Disabled by default (FIFO sub-queues, suitable for NVMe/SSD).
- Enabled via lprocfs:
echo 1 > .../nrs_fairshare_locality. - Only applies to
ost_ioservice BRW RPCs.
Implementation Stages
Each stage is independently shippable and provides standalone value.
Goal: Single-level jobid_fair and uid_fair policies with opportunity
fairness bypass on the ost_io regular queue.
Deliverables:
lustre_nrs_fairshare.hwith data structuresnrs_fairshare.cwith all NRS ops:op_policy_start/stop— allocate/freenrs_fs_head, start/stop δ-kthreadop_res_get— extract key from RPC (reuse TBF key extraction for jobid/uid/gid/projid/nid), look up or createnrs_fs_entityop_req_enqueue— append to entity's sub-queue, mark activeop_req_get— opportunity fairness check → statistical token draw → entity sub-queue FIFO dequeueop_req_dequeue— remove from entity sub-queueop_req_stop— update entity cost counters, check livenessop_lprocfs_init/fini— basic debugfs entries
- Kbuild wiring in
lustre/ptlrpc/Makefile - Registration in
nrs.cviaptlrpc_nrs_policy_register(&nrs_conf_fairshare) - δ-recalculation kthread for single-CPT (local entity recount + range rebuild)
- lprocfs interface:
nrs_fairshare_mode— read/write:jobid_fair,uid_fair,gid_fairnrs_fairshare_delta_ms— δ interval (default 100)nrs_fairshare_opp_threshold— opportunity fairness thresholdnrs_fairshare_stats— YAML per-entity stats (dispatched, cost, opportunity, fairness_debt, queue_depth)
- Tests in
conf-sanity.sh:- Policy start/stop/restart persistence
- Parameter read/write validation
- Fallback to FIFO when fairshare is stopped
- Tests in
sanityn.sh:- Two-client contention: verify fair dispatch ratio within ±10%
- Under-loaded: verify opportunity fairness (no throughput degradation vs FIFO)
Stage 2: Composite Policies via Transition Matrices
Goal: Hierarchical policies like uid_then_jobid_fair,
gid_then_uid_fair.
Deliverables:
- Transition-matrix computation in δ-kthread (u64 fixed-point)
- Extended
nrs_fairshare_modesyntax:uid_then_jobid_fair,gid_then_uid_then_jobid_fair - Internal entity tree: root → level-0 entities → level-1 entities → … The flattened range array is the product of per-level share vectors.
- Tests in
sanityn.sh:- User-then-job-fair: two users, one with 2 jobs, one with 4 jobs. Verify user-level equal split, then within-user equal split among jobs.
- Three-tier group-user-jobid: verify hierarchical share allocation.
Stage 3: Cross-CPT δ-Synchronization
Goal: Global (server-wide) fairness across all CPTs.
Deliverables:
- Promote δ-kthread to service-level (not per-CPT): gather active entities from all CPTs, compute unified transition-matrix product, publish via RCU to each
nrs_fs_head. - Handle entity migration: an entity active on CPT 0 but not CPT 2 still gets its global share; the per-CPT range array includes all global entities, not just locally-active ones.
- lprocfs:
nrs_fairshare_cross_cpt— enable/disable (default: enabled) - Tests in
sanityn.sh:- Multi-CPT fairness: pin clients to different CPTs, verify global fair split.
- Cross-CPT entity appears/disappears: verify range recomputation.
Stage 4: ORR Locality Hybrid and Refinements
Goal: Preserve I/O locality on spinning-disk OSTs; polish for production.
Deliverables:
- Offset-ordered sub-queues within entities (reuse ORR key extraction)
nrs_fairshare_localitylprocfs knob- Weight/priority support:
echo "uid_fair weight=uid:1000:2,uid:1001:3"for proportional (non-equal) sharing - Wall-clock time-slice cost model (optional advanced mode)
- Extended YAML stats: borrowed share, opportunity usage ratio, per-entity fairness debt histogram
- Performance benchmarking vs FIFO, TBF, CRR-N on IOR, mdtest, real apps
- Documentation in
Documentation/lustre/nrs_fairshare.txt
Observability and Operator Interface
lprocfs/debugfs Entries (under each service)
| Entry | R/W | Description |
|---|---|---|
nrs_fairshare_mode |
RW | Sharing policy: jobid_fair, uid_fair, gid_fair, uid_then_jobid_fair, etc.
|
nrs_fairshare_delta_ms |
RW | δ-recalculation interval (default 100, range 10–1000) |
nrs_fairshare_opp_threshold |
RW | Queued-request threshold for opportunity fairness bypass |
nrs_fairshare_cost_model |
RW | pages (default) or rpcs
|
nrs_fairshare_locality |
RW | 0 (FIFO sub-queues, default) or 1 (ORR-style offset ordering)
|
nrs_fairshare_cross_cpt |
RW | 0 or 1 (default 1): cross-CPT entity synchronization
|
nrs_fairshare_stats |
RO | YAML dump of per-entity statistics |
nrs_fairshare_stats_reset |
WO | Reset per-entity counters |
YAML Stats Format
- entity: jobid:batch_sim.12345
type: jobid
active: true
range: [0.000, 0.333)
dispatched: 148290
cost_pages: 592160
opportunity: 3041
fairness_debt: -12.4
queue_depth: 7
- entity: jobid:ml_train.67890
...
Metadata Gap: Job Size
ThemisIO's size-fair policy requires job-size (node count), which is not
currently embedded in Lustre RPCs.
Staged approach:
- v1 (Stages 1–3): Implement
jobid_fair,uid_fair,gid_fairand composite variants. These do NOT require job-size. This covers the majority of useful policies. - v1 approximation for size-fair: Count distinct client NIDs per jobid within a sliding window as a proxy for job size. Imperfect (some NIDs may not issue I/O in every window) but functional.
- v2 (future): Add job-size field to
ptlrpc_bodyor embed in jobid sub-fields (e.g.,jobid.nodecount). Requires client-side changes and wire-protocol versioning.
Testing Strategy
conf-sanity.sh (policy lifecycle)
- Test: start fairshare, verify active via
nrs_policiesread-back - Test: stop fairshare, verify fallback to FIFO
- Test: set/get all tunable parameters (delta_ms, opp_threshold, mode, cost_model, locality, cross_cpt)
- Test: parameter persistence across policy restart
- Test: invalid parameter rejection (delta_ms=0, unknown mode string)
sanityn.sh (fairness under contention)
- Test: two clients,
jobid_fair— verify ~50/50 dispatch ratio (±10%) - Test: three clients,
uid_fair, two clients same UID — verify UID-level equal share, not client-level - Test: under-loaded single client — verify no throughput regression vs FIFO (opportunity fairness)
- Test:
uid_then_jobid_faircomposite — verify hierarchical share split - Test: cross-CPT fairness — pin two clients to different CPTs, verify global 50/50 split
- Test: entity arrival/departure — start two jobs, add third mid-test, verify shares rebalance within 2×δ
Unit Tests (in-kernel or via test harness)
- Transition-matrix product correctness for known inputs
- Fixed-point arithmetic overflow/underflow edge cases
- Entity hash table create/lookup/remove under concurrent access
- Range binary search correctness (boundary cases: sample=0, sample=U32_MAX-1)
Notes and Considerations
- Burst-buffer vs. kernel translation: ThemisIO operates in user-space on dedicated I/O nodes. Lustre NRS is in-kernel on OSS/MDS. The statistical random-number approach (
get_random_u32()) translates directly. The δ-delayed global fairness kthread translates to a kernel worker. The main difference is thatop_req_getruns underscp_req_lock(spinlock), so the hot path must be O(log N) or better — binary search over the flattened range array satisfies this. - Statistical convergence: ThemisIO's effectiveness depends on sufficient I/O volume per entity for the statistical approach to converge (paper §3 notes this limitation). For overloaded servers with many entities each issuing few RPCs, the random draw may produce unfair short-term allocation. Mitigation: the δ-recalculation incorporates fairness-debt tracking; entities that were under-served get a slightly expanded range in the next interval (proportional-integral correction). This is a refinement over base ThemisIO.
- Locking on hot path:
op_req_getruns underscp_req_lock. The range-array lookup is read-only (RCU-protected). Entity sub-queue dequeue modifiesfse_req_list— but since each NRS head is per-CPT and dispatch is serialized byscp_req_lock, no additional lock is needed for the sub-queue. - NRS single-policy constraint: Only one primary policy can be active per NRS head. "Combine ORR plus fairshare" means the ORR-style offset ordering is integrated INTO the fairshare policy's entity sub-queues, not run as a separate policy.
- Key extraction reuse: TBF's key extraction functions (
nrs_tbf_jobid_str,nrs_tbf_nid_str,nrs_tbf_id_str, etc.) and thenrs_tbf_fieldenum are good candidates for extraction into a shared helper or direct reuse. If extraction is too invasive for upstream review, the fairshare policy can duplicate the ~50 lines of key extraction logic. - Existing TBF users are unaffected. The new policy is purely additive. Operators opt-in via
echo fairshare > .../nrs_policies. TBF continues to work exactly as before.