Skip to content
VLSI Mentor

Linux & Bash · Part II · Module 30 · Running Work on Shared Engineering Machines

Submitting and Tracking Scheduled Jobs

Submission is one line and everything interesting happens afterwards: the job is refused, or it waits forever, or it starts and is thrown off a machine for using more than it asked for. This chapter produces all four outcomes and reads each one — including a resource cap that turned out to be twice as generous as it was declared.

Advanced22 min readLinuxPart IISchedulersResourcesMonitoring

Part II · Module 30 · Chapter 30.6 · Submitting and Tracking Scheduled Jobs

Chapter 30.5 established what submission changes about your script and how to test it without a scheduler. This chapter is about the submission itself, and about the four things that can happen next.

1. The Submission Line

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# FORMS, from each vendor's documented interface -- see 30.5's note
bsub   -q normal -o run.log -n 4 -R "rusage[mem=8000]" -W 240 ./run_sim.sh
qsub   -q normal -o run.log -pe smp 4 -l h_vmem=8G -l h_rt=4:00:00 ./run_sim.sh
sbatch --partition=normal --output=run.log --cpus-per-task=4 --mem=8G --time=4:00:00 ./run_sim.sh

Four requests in every one of them, and each has a cost if you get it wrong:

requesttoo smalltoo large
queuerejected, or the wrong limitsa long job in a short queue is killed
coresthe tool runs single-threadedthe job waits for cores it will not use
memoryevicted mid-runthe job waits for memory it will not use
wall-clockkilled at the limit, mid-runlower priority, longer wait

The asymmetry is the point. Under-requesting kills a job that was working; over-requesting makes it wait. Which is why the useful habit is to measure what a job actually uses — Chapter 30.3's rss maximum — and request a margin above it, rather than guessing in either direction.

2. A Queue You Can Observe

There is no scheduler here, so the four outcomes are produced by a stand-in that reproduces the parts affecting your script: the queue wait, the clean environment, a working directory of its own choosing, output to a file, a resource cap, and a job record.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
# qsub-stub — a queue stand-in. There is no scheduler here, so this reproduces
# the parts that affect your script: a clean environment, a directory of the
# queue's choosing, output to a file, a resource cap, and a job record.
# Usage: qsub-stub -q QUEUE [-o OUT] [-f MAXKB] -- CMD...
set -u
queue=normal out="" maxkb=0
while [ $# -gt 0 ]; do
  case "$1" in
    (-q) queue=${2:?-q needs a queue}; shift 2 ;;
    (-o) out=${2:?-o needs a path};    shift 2 ;;
    (-f) maxkb=${2:?-f needs a size};  shift 2 ;;
    (--) shift; break ;;
    (*)  echo "qsub-stub: unknown option '$1'" >&2; exit 2 ;;
  esac
done
[ $# -gt 0 ] || { echo "qsub-stub: no command" >&2; exit 2; }

# a queue the site does not have is a REJECTION, before anything is spent
case "$queue" in
  (normal|long|debug) : ;;
  (*) echo "qsub-stub: job rejected: no such queue '$queue'" >&2; exit 3 ;;
esac

q=${QDIR:?qsub-stub: set QDIR to the queue area}
[ -d "$q" ] || { echo "qsub-stub: no such queue area: $q" >&2; exit 2; }
id=$(( $(cat "$q/next" 2>/dev/null || echo 1000) + 1 ))
printf '%s\n' "$id" > "$q/next"
jd="$q/$id"; mkdir -p "$jd" || exit 2
: "${out:=$jd/job.out}"

{ printf 'id       %s\n' "$id"
  printf 'queue    %s\n' "$queue"
  printf 'user     %s\n' "$(id -un)"
  printf 'host     %s\n' "$(hostname -s)"
  printf 'submitted %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
  printf 'subdir   %s\n' "$PWD"
  printf 'maxkb    %s\n' "$maxkb"
  printf 'cmd     '; printf ' %q' "$@"; printf '\n'
} > "$jd/job.info"
: > "$jd/cmd.args"; for a in "$@"; do printf '%s\n' "$a" >> "$jd/cmd.args"; done
printf 'PEND\n' > "$jd/state"

printf 'Job <%s> is submitted to queue <%s>.\n' "$id" "$queue"

(
  sleep 1                                     # the queue wait
  printf 'RUN\n' > "$jd/state"
  printf 'started  %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >> "$jd/job.info"
  # the execution environment: clean, in a directory of the queue's choosing
  wd="$jd/work"; mkdir -p "$wd"
  args=(); while IFS= read -r a; do args+=("$a"); done < "$jd/cmd.args"
  env -i HOME="$HOME" USER="$(id -un)" PATH=/usr/bin:/bin SHELL=/bin/sh TMPDIR=/tmp \
      bash -c "cd '$wd' && { [ '$maxkb' -gt 0 ] && ulimit -f $maxkb; } ; exec \"\$@\"" \
      bash "${args[@]}" > "$out" 2>&1
  rc=$?
  printf 'exit %d\n' "$rc" > "$jd/status"
  printf 'finished %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >> "$jd/job.info"
  case "$rc" in
    (0)   printf 'DONE\n' > "$jd/state" ;;
    (153) printf 'EVICTED\n' > "$jd/state" ;;   # SIGXFSZ: exceeded the file cap
    (*)   printf 'EXIT\n' > "$jd/state" ;;
  esac
) > /dev/null 2>&1 &
disown 2>/dev/null || true

and something to read it back with:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
# qstat-stub [ID] — what the queue knows about your jobs.
set -u
q=${QDIR:?qstat-stub: set QDIR}
[ -d "$q" ] || { echo "qstat-stub: no such queue area: $q" >&2; exit 2; }
printf '%-7s %-9s %-9s %-7s %s\n' JOBID STATE QUEUE EXIT COMMAND
n=0
for jd in "$q"/*/; do
  [ -d "$jd" ] || continue
  id=$(basename "$jd")
  [ $# -gt 0 ] && [ "$id" != "$1" ] && continue
  n=$((n+1))
  printf '%-7s %-9s %-9s %-7s %s\n' "$id" \
    "$(cat "$jd/state" 2>/dev/null || echo '?')" \
    "$(sed -n 's/^queue *//p' "$jd/job.info" 2>/dev/null)" \
    "$(sed -n 's/^exit //p' "$jd/status" 2>/dev/null || echo '-')" \
    "$(sed -n 's/^cmd  *//p' "$jd/job.info" 2>/dev/null | cut -c1-40)"
done
[ "$n" = 0 ] && { printf 'no jobs found\n'; exit 1; }
exit 0

3. The Four Outcomes

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ QDIR=$PWD/queue qsub-stub -q normal -- "$PWD/bin/run_sim.sh"
Job <1001> is submitted to queue <normal>.
$ QDIR=$PWD/queue qsub-stub -q normal -- /bin/echo hello
Job <1002> is submitted to queue <normal>.
$ QDIR=$PWD/queue qsub-stub -q bogus -- /bin/true
qsub-stub: job rejected: no such queue 'bogus'
$ echo $?
3
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ QDIR=$PWD/queue qstat-stub
JOBID   STATE     QUEUE     EXIT    COMMAND
1001    EXIT      normal    1       /…/m30/bin/run_sim.sh
1002    DONE      normal    0       /bin/echo hello
1003    EXIT      normal    127     vvp /…/m30/logs/dumphang.vvp
1004    EVICTED   normal    153     /tmp/writer.sh

Five states, each with a different meaning and a different response.

REJECTED, before the job exists. A queue that is not configured, a resource request above the queue's maximum, a missing permission. The important property is that nothing has been spent — no wait, no slot, no partial output. A rejection is the cheapest failure available and the only one that is pure good news, because you find out in a second rather than in four hours.

PEND — queued. Observed in the stand-in at t=1s before it moved to RUN. On a real cluster this is where a job can sit indefinitely, and §5 is about telling "waiting its turn" from "will never run".

EXIT with 1 — job 1001. That is Chapter 30.5 §2's measurement arriving through the queue: the clean environment had no PROJ, so the script refused to start. Nothing about the scheduler is wrong; the script was not ready to be submitted.

EXIT with 127 — job 1003. The minimal PATH did not contain the simulator. Same cause, different symptom, and 127 is specifically "command not found" — which is worth recognising on sight because it means the environment, never the design.

EVICTED with 153 — job 1004. The job exceeded a resource it had declared, and §4 is about that number.

4. The Resource Cap, and the Unit That Was Not What It Said

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ QDIR=$PWD/queue qsub-stub -q normal -f 50 -- /tmp/writer.sh
Job <1005> is submitted to queue <normal>.
$ sleep 3; printf 'state=%s status=%s output=%s bytes (cap 50 KB)\n' \
>   "$(cat queue/1005/state)" "$(cat queue/1005/status)" "$(wc -c < queue/1005/job.out | tr -d ' ')"
state=EVICTED status=exit 153 output=51200 bytes (cap 50 KB)

51,200 bytes for a 50 KB cap — exact. And the status decodes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ echo $((153 - 128))
25
$ kill -l 25
XFSZ

153 is 128 + 25, the shell's convention for "terminated by signal 25", and signal 25 here is SIGXFSZ — file size limit exceeded. A job killed by its own declared limit reports it as a signal, not as an error from the tool, which is why the tool's log usually ends mid-sentence with nothing explaining why.

And one limit this platform does not have at all:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ( ulimit -v 65536 )
bash: ulimit: virtual memory: cannot modify limit: Invalid argument

So an address-space cap — the thing a scheduler's mem request usually becomes — cannot be demonstrated or enforced here. A memory limit on this machine is advisory; on a real cluster it is not, and a job exceeding it is killed in a way that looks exactly like job 1004.

5. Queued Forever, or Waiting Its Turn

The state that needs most interpretation is PEND, because it has two completely different causes and they look identical.

Waiting its turn is normal. Other people's jobs are ahead of you, the queue is busy, your priority is what it is.

Never going to run is a request that cannot be satisfied: more cores than any node has, more memory than the queue permits, a combination no machine matches. A scheduler will usually say so if asked, and the asking is the skill:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# FORMS -- the "why is this pending" question, per vendor
bjobs -l 12345          # LSF: the PENDING REASONS block
qstat -j 12345          # SGE/UGE: the "scheduling info" lines
squeue -j 12345 -o '%r' # Slurm: the Reason field
scontrol show job 12345 # Slurm: the full record

What they report falls into three groups, and the group tells you what to do:

the reason sayswhat it meanswhat to do
resources busy, hosts occupied, prioritywaiting its turnwait, or request less
no host matches, resource not availableunsatisfiablefix the request
licence not available, dependency not metblocked on something externalfix the dependency

The second row is the one to recognise quickly, because waiting on it is free of any possibility of success. And the practical move for all three is the same one that applies without any scheduler: ask for what you measured. A job requesting 64 GB because somebody once saw a big number pends behind every job that asked for 8.

6. Tracking a Job to Completion

The loop that follows a job is short, and the care is all in the terminal conditions:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
# follow JOBID — wait for a job to reach a terminal state, then report.
set -u
id=${1:?usage: follow JOBID}
q=${QDIR:?follow: set QDIR}
jd="$q/$id"
[ -d "$jd" ] || { echo "follow: no such job: $id" >&2; exit 2; }

poll=${2:-2}
last=""
while :; do
  st=$(cat "$jd/state" 2>/dev/null || echo UNKNOWN)
  [ "$st" != "$last" ] && { printf '%s  %s\n' "$(date -u '+%H:%M:%SZ')" "$st"; last=$st; }
  case "$st" in
    (DONE|EXIT|EVICTED) break ;;
    (UNKNOWN) echo "follow: job $id has no state -- the queue lost it" >&2; exit 1 ;;
  esac
  sleep "$poll"
done

rc=$(sed -n 's/^exit //p' "$jd/status" 2>/dev/null)
printf '\n-- job %s finished in state %s, exit %s --\n' "$id" "$st" "${rc:-?}"

# a terminal state is not a verdict: Module 29's rule, one more time
out=$(sed -n 's/^cmd  *//p' "$jd/job.info" | head -1)
printf 'command: %s\n' "$out"
if [ ! -s "$jd/job.out" ]; then
  printf 'no output at all -- see Chapter 29.5\n'
elif grep -qI . "$jd/job.out" 2>/dev/null; then
  printf 'last 5 lines of output:\n'; tail -5 "$jd/job.out" | sed 's/^/  /'
else
  # a job's output is not necessarily text, and BSD sed ABORTS on NUL bytes
  printf 'output is %s bytes of binary data -- not shown\n' "$(wc -c < "$jd/job.out" | tr -d ' ')"
fi

case "$st" in
  (DONE)    exit 0 ;;
  (EVICTED) printf 'evicted: it exceeded a resource it declared. Raise the request or reduce the use.\n' >&2; exit 1 ;;
  (*)       exit 1 ;;
esac
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ QDIR=$PWD/queue follow 1005
07:33:45Z  EVICTED

-- job 1005 finished in state EVICTED, exit 153 --
command: /tmp/writer.sh
output is 51200 bytes of binary data -- not shown
evicted: it exceeded a resource it declared. Raise the request or reduce the use.
$ echo $?
1

Following a job from submission shows the transitions:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ QDIR=$PWD/queue qsub-stub -q normal -- /bin/echo hello
Job <1006> is submitted to queue <normal>.
$ QDIR=$PWD/queue follow 1006 1
07:33:03Z  PEND
07:33:04Z  RUN
07:33:05Z  DONE

-- job 1006 finished in state DONE, exit 0 --
command: /bin/echo hello
last 5 lines of output:
  hello
$ echo $?
0

And the two failure modes of the tracker itself:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ QDIR=$PWD/queue follow 9999        # a job directory with no state file
07:33:05Z  UNKNOWN
follow: job 9999 has no state -- the queue lost it
$ echo $?
1
$ QDIR=$PWD/queue follow 7777        # no such job
follow: no such job: 7777
$ echo $?
2

Four decisions.

**It prints a line only when the state changes.**Four decisions.

It prints a line only when the state changes. A poll loop that prints every tick produces a log nobody reads; one that prints transitions produces a timeline.

UNKNOWN is a failure, not a state to wait in. A job directory with no state file means the queue lost track of it, and waiting forever for a state that will never be written is the commonest defect in a tracking script.

The exit status and the output are both reported. Module 29 in one line: a terminal state is not a verdict, and DONE with exit 0 still needs the log read to know whether the simulation passed.

EVICTED gets its own message and its own advice, because it is the one outcome where the fix is a number in the submission line rather than anything in the design.

7. What Module 30 Leaves You

The module began with one job you started yourself and ends with a queue you do not control, and the same three facts held at every step.

The exit status is not the verdict. A failing simulation exits 0 (Module 27), a 256-error compile exits 0 (Chapter 29.2), a run that exhausts its events exits 0 (Chapter 29.5), and a job that reaches DONE has still only told you it finished.

Relative paths move when the process does. Five chapters measured it, the last being a scheduler's choice of directory.

And the tools do not do what the folklore says. nohup did not protect the simulator, wait returned 0 for failed jobs, log growth was not a progress signal, kill -9 cost 88 lines of evidence, a stale artifact reported success, and ulimit -f counted 1024-byte blocks. Every one of those was found by running it rather than by reading about it, which is the only method this module recommends.

What remains is the part no amount of shell can supply: a regression that submits two thousand jobs, decides each verdict, collects the results, and returns a status something else can gate on. Module 32 is that, and Module 31 is the batch mechanics it needs first — test lists as data, seeds, a run matrix, and the bounded parallelism that Chapter 30.2 could not express without wait -n.

Practice

  1. Write out the four resource requests for your own flow, with the cost of getting each one too small and too large.
  2. Measure a job's actual peak memory with Chapter 30.3's watcher and compare it with what you have been requesting.
  3. Submit to a queue that does not exist and confirm the rejection costs nothing.
  4. Observe a job in PEND before it runs.
  5. Submit a script that depends on a profile variable and read the exit status.
  6. Submit one whose tool is not on the minimal PATH and recognise 127 on sight.
  7. Cap a job's output below what it writes and record the state and status.
  8. Decode the status into 128 + signal and name the signal.
  9. Measure what unit ulimit -f counts with three different values.
  10. Work out what a 512-byte assumption would have done to a declared cap.
  11. Try ulimit -v on your machine and on your compute server.
  12. Ask a real scheduler why a job is pending and classify the reason into one of the three groups.
  13. Write follow and confirm it reports transitions rather than every poll.
  14. Delete a job's state file while follow is watching, and confirm it fails rather than waiting.
  15. Follow a job to DONE with exit 0 and then read its log to find out whether the simulation actually passed.

Check Your Understanding

  1. What are the four resource requests, and why is the error asymmetric?
  2. What is the right way to choose a memory request?
  3. Which outcome is the only pure good news, and why?
  4. What are the two causes of PEND, and why do they look the same?
  5. What did exit 1 mean for job 1001, and exit 127 for job 1003?
  6. What does 153 decode to, and which signal is it?
  7. Why does a job killed by its own limit usually have a log that ends mid-sentence?
  8. How many bytes does ulimit -f 100 allow here, and what does that make a block?
  9. What did the 512-byte assumption do to a 50 KB request?
  10. Which direction of a wrong cap is dangerous, and why?
  11. What is the only way to know which unit a limit uses?
  12. What happens with ulimit -v here, and what does that mean for memory limits?
  13. Which of this chapter's claims are measured and which are documentation?
  14. Why does follow treat UNKNOWN as a failure?
  15. Why is DONE with exit 0 still not a verdict?

8. Remember

  • Four requests — queue, cores, memory, wall-clock — and the error is asymmetric. Too small kills a working job; too large only makes it wait.
  • Request what you measured, from a peak rss, with a margin.
  • A rejection costs nothing and is the only outcome that is pure good news.
  • PEND has two causes that look identical: waiting its turn, and unsatisfiable. Ask for the reason.
  • Exit 1 and exit 127 from a submitted job are environment failures, not design failures — 127 specifically means the tool was not on PATH.
  • Eviction reports as 128 + SIGXFSZ = 153, and the tool's log ends mid-sentence because a signal gives it nothing to say.
  • ulimit -f counts 1024-byte blocks here, not 512. The 512 assumption made a 50 KB request into a 100 KB cap.
  • A too-loose cap is the dangerous direction, and exceeding a limit on purpose is the only way to learn its unit.
  • ulimit -v does not work here, so memory limits are advisory on this machine and enforced on a cluster.
  • Report transitions, not polls; treat a missing state as a failure; and read the log even on DONE.
  • A job that fails under the stand-in is yours to fix. One that only fails on the cluster is a question for the cluster team — with the job id, the submission line and the pending reason.

9. What Is Next

Module 30 leaves one job submitted, tracked and understood. Module 31 turns one invocation into many — test lists as data, explicit seeds, a run matrix you can see before launching it, and the bounded parallelism this module could not express — and Module 32 assembles the whole regression on top of it.

Where this fits

Part of the Linux & Bash curriculum.