Skip to main content

rustfs_erasure_codec/core/
parallel.rs

1use crate::Field;
2
3use super::{
4    CODE_SLICE_DEFAULT_CHUNK_BYTES, CODE_SLICE_LARGE_CHUNK_BYTES, CODE_SLICE_MIN_CHUNK_BYTES,
5    PARALLEL_MIN_SHARD_BYTES, ReedSolomon,
6};
7
8#[cfg(feature = "std")]
9pub const PARALLEL_POLICY_VERSION: u32 = 2;
10#[cfg(feature = "std")]
11const RS_PARALLEL_POLICY_MIN_PARALLEL_SHARD_BYTES_ENV: &str =
12    "RS_PARALLEL_POLICY_MIN_PARALLEL_SHARD_BYTES";
13#[cfg(feature = "std")]
14const RS_PARALLEL_POLICY_MIN_BYTES_PER_JOB_ENV: &str = "RS_PARALLEL_POLICY_MIN_BYTES_PER_JOB";
15#[cfg(feature = "std")]
16const RS_PARALLEL_POLICY_MAX_JOBS_ENV: &str = "RS_PARALLEL_POLICY_MAX_JOBS";
17#[cfg(feature = "std")]
18const RS_PARALLEL_POLICY_L2_CACHE_BYTES_ENV: &str = "RS_PARALLEL_POLICY_L2_CACHE_BYTES";
19
20#[cfg(feature = "std")]
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct ParallelPolicy {
23    pub min_parallel_shard_bytes: usize,
24    pub min_bytes_per_job: usize,
25    pub max_jobs: usize,
26    /// Estimated L2 cache size per core in bytes. Used to bound chunk sizes so
27    /// each job's working set fits in L2. Set to 0 to disable cache-aware sizing.
28    pub l2_cache_bytes: usize,
29}
30
31#[cfg(feature = "std")]
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct ParallelDecision {
34    pub use_parallel: bool,
35    pub jobs: usize,
36    pub chunk_len: usize,
37}
38
39#[cfg(feature = "std")]
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub(crate) struct RuntimeParallelPolicyCache {
42    pub available_parallelism: usize,
43    pub data: ParallelPolicy,
44    pub reconstruct_data: ParallelPolicy,
45    pub reconstruct_full_data: ParallelPolicy,
46    pub reconstruct_full_parity: ParallelPolicy,
47}
48
49#[cfg(feature = "std")]
50impl Default for ParallelPolicy {
51    fn default() -> Self {
52        Self {
53            min_parallel_shard_bytes: PARALLEL_MIN_SHARD_BYTES,
54            min_bytes_per_job: CODE_SLICE_LARGE_CHUNK_BYTES,
55            max_jobs: 0,
56            l2_cache_bytes: super::cache_detect::detect_l2_cache_bytes()
57                .unwrap_or(super::cache_detect::DEFAULT_L2_CACHE_BYTES),
58        }
59    }
60}
61
62#[cfg(feature = "std")]
63impl ParallelPolicy {
64    /// Decide whether to use parallel execution and how to chunk the work.
65    pub fn decide(
66        &self,
67        shard_size: usize,
68        data_shards: usize,
69        output_shards: usize,
70        available_parallelism: usize,
71    ) -> ParallelDecision {
72        if shard_size == 0 || data_shards == 0 || output_shards == 0 {
73            return ParallelDecision {
74                use_parallel: false,
75                jobs: 1,
76                chunk_len: shard_size,
77            };
78        }
79
80        let available_parallelism = available_parallelism.max(1);
81        let max_jobs = if self.max_jobs == 0 {
82            available_parallelism
83        } else {
84            core::cmp::min(self.max_jobs, available_parallelism)
85        }
86        .max(1);
87
88        let min_parallel_shard_bytes = self.min_parallel_shard_bytes.max(1);
89        let min_bytes_per_job = self.min_bytes_per_job.max(CODE_SLICE_MIN_CHUNK_BYTES);
90
91        let mut chunk_count = shard_size.div_ceil(min_bytes_per_job).max(1);
92
93        // Cache-aware chunking: if L2 cache size is known, limit chunk count so
94        // each job's working set (chunk * active shards) fits in L2.
95        if self.l2_cache_bytes > 0 {
96            let active_shards = data_shards.saturating_add(output_shards).max(1);
97            let ideal_chunk = self.l2_cache_bytes / active_shards;
98            if ideal_chunk > 0 {
99                let cache_chunk_count = shard_size.div_ceil(ideal_chunk).max(1);
100                chunk_count = chunk_count.max(cache_chunk_count);
101            }
102        }
103        let max_useful_jobs = if output_shards <= 2 {
104            chunk_count
105        } else {
106            output_shards.saturating_mul(chunk_count)
107        }
108        .max(1);
109        let jobs = core::cmp::min(max_jobs, max_useful_jobs).max(1);
110
111        if shard_size < min_parallel_shard_bytes || jobs < 2 {
112            return ParallelDecision {
113                use_parallel: false,
114                jobs: 1,
115                chunk_len: core::cmp::min(Self::serial_chunk_len(shard_size), shard_size),
116            };
117        }
118
119        let chunks_per_output = core::cmp::max(1, jobs.div_ceil(output_shards));
120        let chunk_len = shard_size
121            .div_ceil(chunks_per_output)
122            .clamp(CODE_SLICE_MIN_CHUNK_BYTES, min_bytes_per_job);
123
124        ParallelDecision {
125            use_parallel: true,
126            jobs,
127            chunk_len: core::cmp::min(chunk_len, shard_size),
128        }
129    }
130
131    fn serial_chunk_len(shard_size: usize) -> usize {
132        if shard_size <= CODE_SLICE_MIN_CHUNK_BYTES {
133            shard_size
134        } else if shard_size <= CODE_SLICE_DEFAULT_CHUNK_BYTES {
135            CODE_SLICE_MIN_CHUNK_BYTES
136        } else if shard_size <= 4 * 1024 * 1024 {
137            CODE_SLICE_DEFAULT_CHUNK_BYTES
138        } else {
139            CODE_SLICE_LARGE_CHUNK_BYTES
140        }
141    }
142
143    /// Builder-style setter for L2 cache size.
144    pub fn with_l2_cache_bytes(mut self, bytes: usize) -> Self {
145        self.l2_cache_bytes = bytes;
146        self
147    }
148
149    /// Apply environment variable overrides (`RS_PARALLEL_POLICY_*`) to this policy.
150    pub fn with_env_overrides(self) -> Self {
151        let mut policy = self;
152        if let Some(value) = parse_env_usize(RS_PARALLEL_POLICY_MIN_PARALLEL_SHARD_BYTES_ENV)
153            && value > 0
154        {
155            policy.min_parallel_shard_bytes = value;
156        }
157        if let Some(value) = parse_env_usize(RS_PARALLEL_POLICY_MIN_BYTES_PER_JOB_ENV)
158            && value > 0
159        {
160            policy.min_bytes_per_job = value;
161        }
162        if let Some(value) = parse_env_usize(RS_PARALLEL_POLICY_MAX_JOBS_ENV) {
163            policy.max_jobs = value;
164        }
165        if let Some(value) = parse_env_usize(RS_PARALLEL_POLICY_L2_CACHE_BYTES_ENV) {
166            policy.l2_cache_bytes = value;
167        }
168        policy
169    }
170}
171
172#[cfg(feature = "std")]
173impl RuntimeParallelPolicyCache {
174    pub(crate) fn new(data: ParallelPolicy) -> Self {
175        Self {
176            available_parallelism: detect_available_parallelism(),
177            data,
178            reconstruct_data: data,
179            reconstruct_full_data: data,
180            reconstruct_full_parity: data,
181        }
182    }
183
184    pub(crate) fn reconstruct_policy(&self, data_only: bool) -> ParallelPolicy {
185        if data_only {
186            self.reconstruct_data
187        } else {
188            self.reconstruct_full_data
189        }
190    }
191
192    pub(crate) fn reconstruct_stage_policies(
193        &self,
194        data_only: bool,
195    ) -> (ParallelPolicy, ParallelPolicy) {
196        if data_only {
197            (self.reconstruct_data, self.reconstruct_data)
198        } else {
199            (self.reconstruct_full_data, self.reconstruct_full_parity)
200        }
201    }
202}
203
204#[cfg(feature = "std")]
205fn parse_env_usize(name: &str) -> Option<usize> {
206    std::env::var(name)
207        .ok()
208        .and_then(|value| value.parse::<usize>().ok())
209}
210
211#[cfg(feature = "std")]
212fn detect_available_parallelism() -> usize {
213    std::thread::available_parallelism()
214        .map(|parallelism| parallelism.get())
215        .unwrap_or(1)
216}
217
218impl<F: Field> ReedSolomon<F> {
219    /// Compute the parallel execution decision for the current codec and shard size.
220    #[cfg(feature = "std")]
221    pub fn parallel_policy(&self, shard_len: usize, output_shards: usize) -> ParallelDecision {
222        let decision = self.parallel_policy_with(
223            shard_len,
224            output_shards,
225            self.policy_cache.available_parallelism,
226        );
227
228        #[cfg(debug_assertions)]
229        if std::env::var_os("RS_PARALLEL_POLICY_DEBUG").is_some() {
230            eprintln!(
231                "rs-parallel-policy v{} shard_len={} outputs={} -> use_parallel={} jobs={} chunk_len={}",
232                PARALLEL_POLICY_VERSION,
233                shard_len,
234                output_shards,
235                decision.use_parallel,
236                decision.jobs,
237                decision.chunk_len
238            );
239        }
240
241        self.runtime_profile_metrics
242            .record_parallel_policy(decision);
243        decision
244    }
245
246    #[cfg(feature = "std")]
247    pub(crate) fn parallel_policy_with(
248        &self,
249        shard_len: usize,
250        output_shards: usize,
251        available_parallelism: usize,
252    ) -> ParallelDecision {
253        self.effective_parallel_policy().decide(
254            shard_len,
255            self.data_shard_count,
256            output_shards,
257            available_parallelism,
258        )
259    }
260
261    /// Returns the parallel policy version number.
262    #[cfg(feature = "std")]
263    pub fn parallel_policy_version(&self) -> u32 {
264        PARALLEL_POLICY_VERSION
265    }
266
267    /// Returns the effective parallel policy (including cache-aware adjustments).
268    #[cfg(feature = "std")]
269    pub fn effective_parallel_policy(&self) -> ParallelPolicy {
270        self.policy_cache.data
271    }
272
273    #[cfg(feature = "std")]
274    pub(crate) fn resolve_policy_cache() -> RuntimeParallelPolicyCache {
275        Self::resolve_policy_cache_with_options(super::CodecOptions::default())
276    }
277
278    #[cfg(feature = "std")]
279    pub(crate) fn resolve_policy_cache_with_options(
280        options: super::CodecOptions,
281    ) -> RuntimeParallelPolicyCache {
282        let mut data = ParallelPolicy::default().with_env_overrides();
283        if options.max_parallel_jobs > 0 {
284            data.max_jobs = options.max_parallel_jobs;
285        }
286        if core::any::type_name::<F>() == core::any::type_name::<crate::galois_8::Field>() {
287            crate::galois_8::resolve_runtime_parallel_policy_cache(data)
288        } else {
289            RuntimeParallelPolicyCache::new(data)
290        }
291    }
292}
293
294#[cfg(all(test, feature = "std"))]
295mod tests {
296    use super::*;
297
298    #[test]
299    fn test_default_policy_has_l2_cache() {
300        let policy = ParallelPolicy::default();
301        assert!(
302            policy.l2_cache_bytes > 0,
303            "L2 cache should be detected or defaulted"
304        );
305    }
306
307    #[test]
308    fn test_cache_aware_increases_chunk_count() {
309        // Small L2 cache forces more chunks
310        let policy = ParallelPolicy {
311            l2_cache_bytes: 64 * 1024, // 64 KiB
312            ..Default::default()
313        };
314        let decision = policy.decide(1024 * 1024, 10, 4, 8);
315        // With 64K L2 / 14 active shards ≈ 4.5K per chunk
316        // 1MB / 4.5K ≈ 227 chunks → many parallel jobs
317        assert!(decision.jobs > 1, "should parallelize with small cache");
318    }
319
320    #[test]
321    fn test_l2_cache_zero_disables_cache_aware() {
322        let policy = ParallelPolicy {
323            l2_cache_bytes: 0,
324            ..Default::default()
325        };
326        let decision = policy.decide(1024 * 1024, 10, 4, 8);
327        // Should still work, just without cache-aware sizing
328        assert!(decision.jobs >= 1);
329    }
330
331    #[test]
332    fn test_with_l2_cache_bytes_builder() {
333        let policy = ParallelPolicy::default().with_l2_cache_bytes(512 * 1024);
334        assert_eq!(policy.l2_cache_bytes, 512 * 1024);
335    }
336
337    #[test]
338    fn test_cache_aware_small_shard_no_effect() {
339        // Shard smaller than min_parallel_shard_bytes → serial regardless
340        let policy = ParallelPolicy {
341            l2_cache_bytes: 256 * 1024,
342            ..Default::default()
343        };
344        let decision = policy.decide(1024, 10, 4, 8);
345        assert!(!decision.use_parallel);
346    }
347}