diff --git a/homework/doc/arc.md b/homework/doc/arc.md
index dc1abdc627..6fc6e104a8 100644
--- a/homework/doc/arc.md
+++ b/homework/doc/arc.md
@@ -12,8 +12,8 @@ The skeleton code is a heavily modified version of `Arc` from the standard libra
 We don't recommend reading the original source code before finishing this homework
 because that version is more complex.
 
-## ***2023 fall semester notice: Use `SeqCst`***
-Due to lack of time, we cannot cover the weak memory semantics.
+## ***2024 spring semester notice: Use `SeqCst`***
+We won't cover the weak memory semantics in this semester.
 So you may ignore the instructions on `Ordering` stuff below and
 use `Ordering::SeqCst` for `ordering: Ordering` parameters for `std::sync::atomic` functions.
 
diff --git a/homework/doc/hash_table.md b/homework/doc/hash_table.md
index 623e634d2a..9cb5f60fe9 100644
--- a/homework/doc/hash_table.md
+++ b/homework/doc/hash_table.md
@@ -10,7 +10,7 @@ This homework is in 2 parts:
    optimize the implementation by relaxing the ordering on the atomic accesses.
    We recommend working on this part after finishing the [Arc homework](./arc.md).
 
-## ***2023 fall semester notice: Part 2 is cancelled***
+## ***2024 spring semester notice: Part 2 is cancelled***
 We won't cover the weak memory semantics in this semester.
 
 ## Part 1: Split-ordered list in sequentially consistent memory model
diff --git a/homework/doc/hazard_pointer.md b/homework/doc/hazard_pointer.md
index 5ed2713a9c..f9ac453285 100644
--- a/homework/doc/hazard_pointer.md
+++ b/homework/doc/hazard_pointer.md
@@ -15,7 +15,7 @@ This homework is in 2 parts:
    optimize the implementation by relaxing the ordering.
    We recommend working on this part after finishing the [Arc homework](./arc.md).
 
-## ***2023 fall semester notice: Part 2 is cancelled***
+## ***2024 spring semester notice: Part 2 is cancelled***
 We won't cover the weak memory semantics in this semester.
 To ensure that the grader works properly, you must use `Ordering:SeqCst` for all operations.
 
diff --git a/homework/doc/list_set.md b/homework/doc/list_set.md
index 3fc88cf784..8ed4b1d6bd 100644
--- a/homework/doc/list_set.md
+++ b/homework/doc/list_set.md
@@ -1,55 +1,30 @@
 # Concurrent set based on Lock-coupling linked list
-**Implement concurrent set data structures with sorted singly linked list using (optimistic) fine-grained lock-coupling.**
+**Implement concurrent set data structures with sorted singly linked list using fine-grained lock-coupling.**
 
 Suppose you want a set data structure that supports concurrent operations.
 The simplest possible approach would be taking a non-concurrent set implementation and protecting it with a global lock.
 However, this is not a great idea if the set is accessed frequently because a thread's operation blocks all the other threads' operations.
 
-In this homework, you will write two implementations of the set data structure based on singly linked list protected by fine-grained locks.
+In this homework, you will write an implementation of the set data structure based on singly linked list protected by fine-grained locks.
 * The nodes in the list are sorted by their value, so that one can efficiently check if a value is in the set.
 * Each node has its own lock that protects its `next` field.
   When traversing the list, the locks are acquired and released in the hand-over-hand manner.
   This allows multiple operations run more concurrently.
 
-You will implement two variants.
-* In `list_set/fine_grained.rs`, the lock is the usual `Mutex`.
-* In `list_set/optimistic_fine_grained.rs`, the lock is a `SeqLock`.
-  This allows read operations to run optimistically without actually locking.
-  Therefore, read operations are more efficient in read-most scenario, and
-  they do not block other operations.
-  However, more care must be taken to ensure correctness.
-    * You need to validate read operations and handle the failure.
-        * Do not use `ReadGuard::restart()`.
-          Using this correctly requires some extra synchronization
-          (to be covered in lock-free list lecture),
-          which makes `SeqLock` somewhat pointless.
-          The tests assume that `ReadGuard::restart()` is not used.
-    * Since each node can be read and modified to concurrently,
-      you should use atomic operations to avoid data races.
-      Specifically, you will use `crossbeam_epoch`'s `Atomic<T>` type
-      (instead of `std::sync::AtomicPtr<T>`, due to the next issue).
-      For `Ordering`, use `SeqCst` everywhere.
-      (In the later part of this course, you will learn that `Relaxed` is sufficient.
-      But don't use `Relaxed` in this homework, because that would break `cargo_tsan`.)
-    * Since a node can be removed while another thread is reading,
-      reclamation of the node should be deferred.
-      You can handle this semi-automatically with `crossbeam_epoch`.
-
-Fill in the `todo!()`s in `list_set/{fine_grained,optimistic_fine_grained}.rs` (about 40 + 80 lines of code).
+Fill in the `todo!()`s in `list_set/fine_grained.rs` (about 40 lines of code).
 As in the [Linked List homework](./linked_list.md), you will need to use some unsafe operations.
 
 ## Testing
-Tests are defined in `tests/list_set/{fine_grained,optimistic_fine_grained}.rs`.
+Tests are defined in `tests/list_set/fine_grained.rs`.
 Some of them use the common set test functions defined in `src/test/adt/set.rs`.
 
-## Grading (100 points)
+## Grading (45 points)
 Run
 ```
 ./scripts/grade-list_set.sh
 ```
 
-For each module `fine_grained` and `optimistic_fine_grained`,
-the grader runs the tests
+The grader runs the tests
 with `cargo`, `cargo_asan`, and `cargo_tsan` in the following order.
 1. `stress_sequential` (5 points)
 1. `stress_concurrent` (10 points)
@@ -58,12 +33,6 @@ with `cargo`, `cargo_asan`, and `cargo_tsan` in the following order.
 
 For the above tests, if a test fails in a module, then the later tests in the same module will not be run.
 
-For `optimistic_fine_grained`, the grader additionally runs the following tests
-(10 points if all of them passes, otherwise 0).
-* `read_no_block`
-* `iter_invalidate_end`
-* `iter_invalidate_deleted`
-
 ## Submission
 ```sh
 cd cs431/homework
@@ -72,3 +41,44 @@ ls ./target/hw-list_set.zip
 ```
 
 Submit `hw-list_set.zip` to gg.
+
+## Advanced (optional)
+**Note**: This is an *optional* homework, meaning that it will not be graded and not be asked in the exam.
+
+Consider a variant of the homework that uses `SeqLock` instead of `Mutex`.
+This allows read operations to run optimistically without actually locking.
+Therefore, read operations are more efficient in read-most scenario, and
+they do not block other operations.
+However, more care must be taken to ensure correctness.
+  * You need to validate read operations and handle the failure.
+      * Do not use `ReadGuard::restart()`.
+        Using this correctly requires some extra synchronization
+        (to be covered in lock-free list lecture),
+        which makes `SeqLock` somewhat pointless.
+        The tests assume that `ReadGuard::restart()` is not used.
+  * Since each node can be read and modified to concurrently,
+    you should use atomic operations to avoid data races.
+    Specifically, you will use `crossbeam_epoch`'s `Atomic<T>` type
+    (instead of `std::sync::AtomicPtr<T>`, due to the next issue).
+    For `Ordering`, use `SeqCst` everywhere.
+    (In the later part of this course, you will learn that `Relaxed` is sufficient.
+    But don't use `Relaxed` in this homework, because that would break `cargo_tsan`.)
+  * Since a node can be removed while another thread is reading,
+    reclamation of the node should be deferred.
+    You can handle this semi-automatically with `crossbeam_epoch`.
+
+**Instruction**: Fill in the `todo!()`s in `list_set/optimistic_fine_grained.rs` (about 80 lines of code).
+
+**Testing**: Tests are defined in `tests/list_set/optimistic_fine_grained.rs`.
+
+**Self grading**:
+Run
+```
+./scripts/grade-optimistic_list_set.sh
+```
+
+Unlike the main homework, the grader additionally runs the following tests
+(10 points if all of them passes, otherwise 0).
+* `read_no_block`
+* `iter_invalidate_end`
+* `iter_invalidate_deleted`
diff --git a/homework/scripts/grade-list_set.sh b/homework/scripts/grade-list_set.sh
index 0ff29142c1..a07df24f97 100755
--- a/homework/scripts/grade-list_set.sh
+++ b/homework/scripts/grade-list_set.sh
@@ -34,8 +34,6 @@ RUNNER_TIMEOUTS=(
 )
 # the index of the last failed test
 fine_grained_fail=${#COMMON_TESTS[@]}
-optimistic_fine_grained_fail=${#COMMON_TESTS[@]}
-others_failed=false
 
 for r in "${!RUNNERS[@]}"; do
     RUNNER=${RUNNERS[r]}
@@ -53,35 +51,10 @@ for r in "${!RUNNERS[@]}"; do
                 fi
             done
         fi
-        if [ $t -lt $optimistic_fine_grained_fail ]; then
-            echo "Testing optimistic_fine_grained $TEST_NAME with $RUNNER, timeout $TIMEOUT..."
-            TESTS=("--test list_set -- --exact optimistic_fine_grained::$TEST_NAME")
-            for ((i = 0; i < REPS; i++)); do
-                if [ $(run_tests) -ne 0 ]; then
-                    optimistic_fine_grained_fail=$t
-                    break
-                fi
-            done
-        fi
     done
-
-    if [ "$others_failed" == false ]; then
-        echo "Running additional tests for optimistic_fine_grained with $RUNNER, timeout $TIMEOUT..."
-        TESTS=(
-            "--test list_set -- --exact optimistic_fine_grained::read_no_block"
-            "--test list_set -- --exact optimistic_fine_grained::iter_invalidate_end"
-            "--test list_set -- --exact optimistic_fine_grained::iter_invalidate_deleted"
-        )
-        if [ $(run_tests) -ne 0 ]; then
-            others_failed=true
-        fi
-    fi
 done
 
 SCORES=( 0 5 15 30 45 )
-SCORE=$(( SCORES[fine_grained_fail] + SCORES[optimistic_fine_grained_fail] ))
-if [ "$others_failed" == false ]; then
-    SCORE=$(( SCORE + 10 ))
-fi
+SCORE=$(( SCORES[fine_grained_fail] ))
 
-echo "Score: $SCORE / 100"
+echo "Score: $SCORE / 45"
diff --git a/homework/scripts/grade-optimistic_list_set.sh b/homework/scripts/grade-optimistic_list_set.sh
new file mode 100755
index 0000000000..7f58aef126
--- /dev/null
+++ b/homework/scripts/grade-optimistic_list_set.sh
@@ -0,0 +1,76 @@
+#!/usr/bin/env bash
+# set -e
+set -uo pipefail
+IFS=$'\n\t'
+
+# Imports library.
+BASEDIR=$(dirname "$0")
+source $BASEDIR/grade-utils.sh
+
+run_linters || exit 1
+
+export RUST_TEST_THREADS=1
+
+
+REPS=3
+COMMON_TESTS=(
+    "stress_sequential"
+    "stress_concurrent"
+    "log_concurrent"
+    "iter_consistent"
+)
+RUNNERS=(
+    "cargo --release"
+    "cargo_asan"
+    "cargo_asan --release"
+    "cargo_tsan --release"
+)
+# timeout for each RUNNER
+RUNNER_TIMEOUTS=(
+    30s
+    180s
+    180s
+    180s
+)
+# the index of the last failed test
+optimistic_fine_grained_fail=${#COMMON_TESTS[@]}
+others_failed=false
+
+for r in "${!RUNNERS[@]}"; do
+    RUNNER=${RUNNERS[r]}
+    TIMEOUT=${RUNNER_TIMEOUTS[r]}
+    for t in "${!COMMON_TESTS[@]}"; do
+        TEST_NAME=${COMMON_TESTS[t]}
+        # run only if no test has failed yet
+        if [ $t -lt $optimistic_fine_grained_fail ]; then
+            echo "Testing optimistic_fine_grained $TEST_NAME with $RUNNER, timeout $TIMEOUT..."
+            TESTS=("--test list_set -- --exact optimistic_fine_grained::$TEST_NAME")
+            for ((i = 0; i < REPS; i++)); do
+                if [ $(run_tests) -ne 0 ]; then
+                    optimistic_fine_grained_fail=$t
+                    break
+                fi
+            done
+        fi
+    done
+
+    if [ "$others_failed" == false ]; then
+        echo "Running additional tests for optimistic_fine_grained with $RUNNER, timeout $TIMEOUT..."
+        TESTS=(
+            "--test list_set -- --exact optimistic_fine_grained::read_no_block"
+            "--test list_set -- --exact optimistic_fine_grained::iter_invalidate_end"
+            "--test list_set -- --exact optimistic_fine_grained::iter_invalidate_deleted"
+        )
+        if [ $(run_tests) -ne 0 ]; then
+            others_failed=true
+        fi
+    fi
+done
+
+SCORES=( 0 5 15 30 45 )
+SCORE=$(( SCORES[optimistic_fine_grained_fail] ))
+if [ "$others_failed" == false ]; then
+    SCORE=$(( SCORE + 10 ))
+fi
+
+echo "Score: $SCORE / 55"
diff --git a/homework/src/hash_table/growable_array.rs b/homework/src/hash_table/growable_array.rs
index de04310130..473dd4e3e9 100644
--- a/homework/src/hash_table/growable_array.rs
+++ b/homework/src/hash_table/growable_array.rs
@@ -2,7 +2,6 @@
 
 use core::fmt::Debug;
 use core::mem::{self, ManuallyDrop};
-use core::ops::{Deref, DerefMut};
 use core::sync::atomic::Ordering::*;
 use crossbeam_epoch::{Atomic, Guard, Owned, Shared};
 
diff --git a/homework/src/hazard_pointer/hazard.rs b/homework/src/hazard_pointer/hazard.rs
index 4f6df1354b..aea350220d 100644
--- a/homework/src/hazard_pointer/hazard.rs
+++ b/homework/src/hazard_pointer/hazard.rs
@@ -146,6 +146,12 @@ impl HazardBag {
     }
 }
 
+impl Default for HazardBag {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
 impl Drop for HazardBag {
     /// Frees all slots.
     fn drop(&mut self) {
diff --git a/homework/src/linked_list.rs b/homework/src/linked_list.rs
index 39adbaff64..b6b9dd7a1a 100644
--- a/homework/src/linked_list.rs
+++ b/homework/src/linked_list.rs
@@ -1,6 +1,5 @@
 use std::cmp::Ordering;
 use std::fmt;
-use std::iter::FromIterator;
 use std::marker::PhantomData;
 use std::mem;
 use std::ptr;
diff --git a/homework/tests/growable_array.rs b/homework/tests/growable_array.rs
index e1f36c8161..2c01c5a9c9 100644
--- a/homework/tests/growable_array.rs
+++ b/homework/tests/growable_array.rs
@@ -79,12 +79,12 @@ mod stack {
     use crossbeam_epoch::{Atomic, Guard, Owned, Shared};
 
     #[derive(Debug)]
-    pub(crate) struct Stack<T> {
+    pub(super) struct Stack<T> {
         head: Atomic<Node<T>>,
     }
 
     impl<T> Stack<T> {
-        pub(crate) fn new() -> Self {
+        pub(super) fn new() -> Self {
             Self {
                 head: Atomic::null(),
             }
@@ -92,20 +92,20 @@ mod stack {
     }
 
     #[derive(Debug)]
-    pub(crate) struct Node<T> {
+    pub(super) struct Node<T> {
         data: T,
         next: UnsafeCell<*const Node<T>>,
     }
 
     impl<T> Node<T> {
-        pub(crate) fn new(data: T) -> Self {
+        pub(super) fn new(data: T) -> Self {
             Self {
                 data,
                 next: UnsafeCell::new(ptr::null()),
             }
         }
 
-        pub(crate) fn into_inner(self) -> T {
+        pub(super) fn into_inner(self) -> T {
             self.data
         }
     }
@@ -130,7 +130,7 @@ mod stack {
         ///
         /// - A single `n` should only be pushed into the stack once.
         /// - After the push, `n` should not be used again.
-        pub(crate) unsafe fn push_node<'g>(&self, n: Shared<'g, Node<T>>, guard: &'g Guard) {
+        pub(super) unsafe fn push_node<'g>(&self, n: Shared<'g, Node<T>>, guard: &'g Guard) {
             let mut head = self.head.load(Relaxed, guard);
             loop {
                 unsafe { *n.deref().next.get() = head.as_raw() };