-
Notifications
You must be signed in to change notification settings - Fork 78
Work stealing #507
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Work stealing #507
Conversation
|
Related issue: #185 |
wks
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The enum-map crate provides static methods to look up Enum items without concrete instances. You may consider bumping the dependency version of enum-map, too, if that's convenient.
src/scheduler/scheduler.rs
Outdated
| let first_stw_stage = work_buckets.iter().nth(1).map(|(id, _)| id).unwrap(); | ||
| let mut open_stages: Vec<WorkBucketStage> = vec![first_stw_stage]; | ||
| // The rest will open after the previous stage is done. | ||
| let stages = work_buckets | ||
| .iter() | ||
| .map(|(stage, _)| stage) | ||
| .collect::<Vec<_>>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You may use static methods from the Enum<T> type in enum-map 0.6.2:
let first_stw_stage: WorkBucketStage = Enum::<WorkBucketStage>::from_usize(1);
let possible_values: usize = <WorkBucketStage as Enum::<WorkBucketStage>>::POSSIBLE_VALUES;
let stages = (0..possible_values)
.map(|i| {
let stage: WorkBucketStage = Enum::<WorkBucketStage>::from_usize(i);
stage
}).collect::<Vec<_>>(); It is a bit awkward because the enum-map crate which mmtk-core currently depends on is way too old (0.6.2 vs the latest 2.2.0).
If we update Cargo.toml and bump the version to enum-map = "=2.1.0", we will be able to do them much more elegantly:
let first_stw_stage = WorkBucketStage::from_usize(1);
let stages = (0..WorkBucketStage::LENGTH).map(WorkBucketStage::from_usize);If you feel convenient, you can update the dependency for us in this PR, too. Note that 2.2.0 requires rustc 1.60.0 which our moma machines currently don't have. As a workaround, enum-map = "=2.1.0" will lock the version to exactly 2.1.0. We should let our administrator update our installations when appropriate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wks @qinsoon I think we should leave the code as is for now. I won't be physically in Canberra so I will hold off image updates of CI machines until I get back unless it's urgent (i.e. security vulnerability).
I think this also raises a meta-question of what's our MSRV policy. Currently, our MSRV is 1.59.0, which is not that old. Any change to the MSRV could potentially affect any other projects that depend on us. Of course, we can make the decision that we aggressively update MSRV. But that's something we should at least discuss as a group.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Depending on way too old stuff is kinda bad, in my opinion. If we can even just bump to v2.1.0 without disrupting the MSRV version, I think we should do that. (At some point we should do an audit that updates our dependencies)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I switched to enum-map = "=2.1.0" and removed the code that fetches the first stage by using .iter().
src/scheduler/scheduler.rs
Outdated
| let first_stw_stage = work_buckets.iter().nth(1).map(|(id, _)| id).unwrap(); | ||
| let mut open_stages: Vec<WorkBucketStage> = vec![first_stw_stage]; | ||
| // The rest will open after the previous stage is done. | ||
| let stages = work_buckets |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest creating a constant of Vec<WorkBucketStage> to explicitly list all the buckets in a specified order. Items in enum has no explicit order (no Ord,PartialOrd for the enum), and iter() on EnumMap does not specify the iteration order as well (https://docs.rs/enum-map/latest/enum_map/struct.Iter.html). This implementation basically assumes 1. Rust implements the enum as u8, 2. we define the variants in the enum in a correct order, 3. enum_map iterates the map in an increasing order of the key All of these assumptions are not documented, and could be changed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a few uses of work_buckets.iter(), work_buckets.values().nth(), etc., with assumptions on its order. I suggest updating them as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The enum-map crate provides static methods to map between usize and the enum type. See my comment below.
On the documentation of the derive macro Enum, the example code even asserts that the elements are mapped to 0, 1, 2, ... in order, although it never explicitly states so. https://docs.rs/enum-map/latest/enum_map/derive.Enum.html#enums-without-payload
To be safe, we may list the stages by ourselves for now, but I'll open an issue and ask the enum-map developers to make the guarantee in its docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it. Thanks. However, the mapping between enum and usize is just part of the issue. The problem also includes the order of how EnumMap iterates its keys/values. The code in the PR assumes the first element in work_buckets.iter() and work_buckets.values() is the first stw phase. Generally, a map is an unordered collection, and the order to iterate a map is implementation specific. It just happens to work for enum map for its current implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I switched to enum-map = "=2.1.0" and removed the ordering assumption.
|
|
||
| /// Poll a ready-to-execute work pakcet in the following order: | ||
| /// | ||
| /// 1. Any packet that should be processed only by this worker. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This order is different from what we have in master. But it is fine. #600 will be outdated after we merge this PR. This is just a note for myself, no change is needed.
| #[inline(always)] | ||
| pub fn add_prioritized(&self, work: Box<dyn GCWork<VM>>) { | ||
| self.prioritized_queue.as_ref().unwrap().push(work); | ||
| self.notify_one_worker(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Notifying other workers should not be the responsibility of an individual bucket. It should be the responsibility of the GCWorkScheduler because
GCWorkSchedulerowns theWorkerGroup, and- All call sites of
WorkBucket::addalways getscheduler.work_bucket[stage]before callingadd, usually in this form:
self.mmtk.scheduler.work_buckets[WorkBucketStage::Closure]
.add(ProcessModBuf::<E>::new(modbuf, self.meta));This indicates that it is the GCWorkScheduler's responsibility to (1) add a work packet to a concrete bucket, and (2) notify other workers.
I have two suggestions. Either should work, but I prefer the first one.
- Make
scheduler.work_bucketsprivate;- Introduce a
Scheduler::add_work_packet(&self, bucket, packet)method (or copy frommemory_manager::add_work_packet) - Force everyone to add work packets using
Scheduler::add_work_packet - Notify other workers in
Scheduler::add_work_packet
- Introduce a
- Replace
WorkBucket::groupwith a call-back function (or trait)- It should be called when one or more work packets are added.
- In
GCWorkScheduler::new(i.e. when those buckets are created), register callbacks on those buckets to call methods on the worker group (you just created the worker group before creating buckets).
And of course we can leave it as is for now, and refactor later, given that the first way may affect too many files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about this a little bit more. Looks like option 1 above is a necessary step before implementing the design proposed at #546. It builds a bottleneck for adding work packets, and the design in #546 can utilize it to either redirect each packet to the main queue or temporarily store it in a bucket.
I will implement option 1 as a separate PR (this will likely to introduce many changes in mmtk-core and bindings), and have a try on #546.
src/scheduler/scheduler.rs
Outdated
| let first_stw_stage = work_buckets.iter().nth(1).map(|(id, _)| id).unwrap(); | ||
| let mut open_stages: Vec<WorkBucketStage> = vec![first_stw_stage]; | ||
| // The rest will open after the previous stage is done. | ||
| let stages = work_buckets |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The enum-map crate provides static methods to map between usize and the enum type. See my comment below.
On the documentation of the derive macro Enum, the example code even asserts that the elements are mapped to 0, 1, 2, ... in order, although it never explicitly states so. https://docs.rs/enum-map/latest/enum_map/derive.Enum.html#enums-without-payload
To be safe, we may list the stages by ourselves for now, but I'll open an issue and ask the enum-map developers to make the guarantee in its docs.
qinsoon
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
|
The fail in JikesRVM tests is a bit strange. +++ ./dist/RFastAdaptiveSemiSpace_x86_64_m32-linux/rvm -X:gc:no_reference_types=false -Xms75M -Xmx75M -jar /home/runner/work/mmtk-core/mmtk-core/mmtk-jikesrvm/.github/scripts/../../repos/jikesrvm/dacapo/dacapo-2006-10-MR2.jar bloat
===== DaCapo bloat starting =====
Error: Process completed with exit code 1.There is no error message. I cannot reproduce it locally, and I haven't seen this kind of error before. I will just retry the run and see what happens. |
wks
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
|
The JikesRVM tests keep failing constantly (3/3 runs failed). The JikesRVM binding and the mmtk-core are both v0.12, which passed the tests in mmtk/mmtk-jikesrvm#110. So it is possibly either a bug in this PR, or a bug revealed by this PR. I will investigate into this as it may relate to weak reference processing (it always failed in the weak ref tests). @wenyuzhao If you can think up anything that may cause the issue, also let me know. |
|
I tried my small test case on OpenJDK, and it either passes or crash with out-of-memory. The SoftReference is never erroneously cleared. The following command makes the test pass on my machine, but shrinking the heap to env MMTK_NO_REFERENCE_TYPES=false MMTK_THREADS=1 RUST_BACKTRACE=1 MMTK_PLAN=SemiSpace /home/wks/projects/mmtk-github/openjdk/build/linux-x86_64-normal-server-release/jdk/bin/java -XX:+UseThirdPartyHeap -server -XX:MetaspaceSize=100M -Xm{s,x}22M MainI guess the bug is still related to clearing SoftReference, but in the VM-specific part. |
|
It is also reproducible if I replace |
|
Here is an even simpler test case. This time you can use any heap size. It doesn't matter. Update: Now it checks the Just like before, it is impossible to reproduce it on OpenJDK. import java.lang.ref.WeakReference;
import java.util.ArrayList;
public class Simple {
Object hard;
WeakReference<Object> soft;
public void populate() {
Object obj = new Object();
hard = obj;
soft = new WeakReference(obj);
}
public void check(int round) {
Object obj = soft.get();
if (obj == null) {
throw new RuntimeException("obj is null! round=" + round);
}
}
public static void main(String[] args) {
int gc_rounds = Integer.parseInt(args[0]);
Simple main = new Simple();
System.out.println("Populating...");
main.populate();
System.out.println("Doing lots of GC...");
for (int i = 0; i < gc_rounds; i++) {
System.gc();
main.check(i);
}
System.out.println("Checking...");
main.check(gc_rounds);
Object obj2 = main.hard;
System.out.println(obj2);
System.out.println("Done.");
}
} |
Does this test pass with |
|
@qinsoon Yes. It always passes with mmtk-core master, no matter the number of threads. By the way, with the |
|
There is a chance that a |
|
A more detailed log of a problematic GC (timestamp and type parameters are omitted): It looks like the buckets for reference processing are opened too early. I guess what happened is, some work packets are asynchronous. Those mmtk-core work packets finished very quickly, but there are VM threads working in the background, and may deliver more work packets afterwards. However, the GC workers in the core thinks all buckets are empty. It then decide that more work packets should be opened. The weak reference processing work packets are added by the |
|
@wks Nice debugging! In ScheduleCollection, we do not schedule anything for the Closure bucket, but rather rely on the binding to generate the closure packets. Actually we do not schedule anything between scanning stacks (or stopping mutators - i can’t remember) and weak ref processing. That means there could be a bug that at some point, the schedule sees there is no work in the closure bucket and all conditions for opening a new bucket is satisfied, so it activates the weak processing bucket. This also explains why CI may pass occasionally after Wenyu fixed an issue about designated work (before the fix, the scheduler might open new buckets even if some workers have designated work). The fix probably eliminated some cases, so it reduces the chance we see the bug in CI. @wenyuzhao |
|
@wks Can you please advise which machine were you using and what is your command line args? It's weird that I tried the test case with both SemiSpace and MarkSweep multiple times, and I did not see any crashes. I'm using My command: |
|
I found the cause. One difference between the JikesRVM binding and the OpenJDK binding is that in OpenJDK, any GC worker can execute the The // StopMutators::do_work
trace!("stop_all_mutators start");
mmtk.plan.base().prepare_for_stack_scanning();
<E::VM as VMBinding>::VMCollection::stop_all_mutators::<E>(worker.tls);
trace!("stop_all_mutators end");
mmtk.scheduler.notify_mutators_paused(mmtk); // THIS LINE!!!!!!!!!!!!
if <E::VM as VMBinding>::VMScanning::SCAN_MUTATORS_IN_SAFEPOINT {
...
for mutator in <E::VM as VMBinding>::VMActivePlan::mutators() {
mmtk.scheduler.work_buckets[WorkBucketStage::Prepare]
.add(ScanStackRoot::<E>(mutator)); // ADD WORK PACKETS
}
}
mmtk.scheduler.work_buckets[WorkBucketStage::Prepare].add(ScanVMSpecificRoots::<E>::new()); // ADD WORK PACKETSNote that it notifies the GC workers before adding more work packets. This is not a problem for OpenJDK, because the However, on JikesRVM, diff --git a/src/scheduler/gc_work.rs b/src/scheduler/gc_work.rs
index 615458f0..a4a2636b 100644
--- a/src/scheduler/gc_work.rs
+++ b/src/scheduler/gc_work.rs
@@ -10,6 +10,7 @@ use std::marker::PhantomData;
use std::mem;
use std::ops::{Deref, DerefMut};
use std::sync::atomic::Ordering;
+use std::time::Duration;
pub struct ScheduleCollection;
@@ -186,6 +187,9 @@ impl<E: ProcessEdgesWork> GCWork<E::VM> for StopMutators<E> {
<E::VM as VMBinding>::VMCollection::stop_all_mutators::<E>(worker.tls);
trace!("stop_all_mutators end");
mmtk.scheduler.notify_mutators_paused(mmtk);
+ std::thread::sleep(Duration::from_millis(10));
if <E::VM as VMBinding>::VMScanning::SCAN_MUTATORS_IN_SAFEPOINT {
// Prepare mutators if necessary
// FIXME: This test is probably redundant. JikesRVM requires to call `prepare_mutator` once after mutators are pausedThen GC workers will wake up, and found that all open buckets are drained. Then the GC workers will open all subsequent work packets. Because the delay after So the current "all GC workers have stopped" is insufficient. It does not take the running GC coordinator thread into account. |
I am using my own laptop in a LXC container. It should be reproducible on any machine. My command line is: The number of threads matters. It is more likely to reproduce it with 2 threads instead of 1. Also note that I updated the test script in the afternoon, so that you can specify how many |
|
The issue is not because the controller proceeded StopMutators packets. It was not a problem before, because the coordinator was the one that opens new buckets. So it cannot open new buckets until the coordinator StopMutators job is finished. Now it's the last yielding GC worker to open new buckets (to reduce inter-thread messages). I changed the open condition of the buckets so that no new buckets can open unless the coordinator finished the coordinator's work packets. Hope this can solve the problem. |
|
@wks Can you please confirm that the change fixes the bug on your side? I'm able to reproduce the bug now and looks like it is fixed. |
wks
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see some problem with the memory orders used in various atomic operations. They may, in theory, cause some problem. If unsure, we can always use SeqCst.
|
@wenyuzhao I confirm that the bug is no longer reproducible on my local machine. However, I also see some memory order issue. See my previous comments. |
Some dependencies have new versions. It is a good chance to bump the dependency versions, too, after we bump the Rust toolchain versions. One notable dependency is `enum-map`. Its latest version is 2.4.2, but we locked its version to 2.1.0 because it required a newer Rust toolchain. Now we can depend on its latest version, instead. We also changed the `rust-version` property in `Cargo.toml` to `1.61.0` because `enum-map-derive` depends on that, and `1.61.0` is not new compared to the `1.66.1` we use. See: #507 (comment) This closes #693
Some dependencies have new versions. It is a good chance to bump the dependency versions, too, after we bump the Rust toolchain versions. One notable dependency is `enum-map`. Its latest version is 2.4.2, but we locked its version to 2.1.0 because it required a newer Rust toolchain. Now we can depend on its latest version, instead. We also changed the `rust-version` property in `Cargo.toml` to `1.61.0` because `enum-map-derive` depends on that, and `1.61.0` is not new compared to the `1.66.1` we use. See: mmtk#507 (comment) This closes mmtk#693
This PR adds work-stealing support to the work-packet system.
Performance: http://squirrel.anu.edu.au/plotty-public/wenyuz/v8/p/j2sUrH
TODO: