Phase 6.3: Parallelitaet und Determinismus - gemessen statt behauptet
Neuer Abschnitt im CP-SAT-Kapitel plus Parallele_Suche.py (76. Programm). Setzt Paket 2 aus Verbesserungen_02.md um. Was fehlte, war nicht die Regel, sondern die Messung: Das Buch setzt an acht Stellen num_workers = 1 mit dem Kommentar "fuer eine reproduzierbare Ausgabe", nachgeprueft hatte es nie jemand. Gerechnet wird auf demselben Job-Shop wie das Intervallvariablen-Kapitel, nur gross genug, dass die Suche arbeitet: 12 Auftraege, 10 Maschinen, 120 Arbeitsgaenge. Zwei Befunde, beide staerker als die Behauptung: * Die Beschleunigung ist ueberlinear. Acht Arbeiter waren im abgedruckten Lauf nicht achtmal, sondern 12,3-mal schneller als einer. Kein Messfehler: CP-SAT vervielfacht nicht dieselbe Suche, sondern laesst verschiedene Strategien nebeneinander laufen, die einander ihre Schranken mitteilen. * Der Seed genuegt nicht - und zwar schon ab ZWEI Arbeitern. Ein Arbeiter: 1 Plan aus 4 Laeufen. Zwei Arbeiter: 3 verschiedene Plaene aus 4 Laeufen, bei identischem random_seed und identischem Zielwert 183. Die vollstaendige Antwort kam erst ueber die Uebungsaufgabe: Mit num_workers = 1, aber fuenf verschiedenen Seeds ergeben sich ebenfalls fuenf verschiedene Plaene. Keiner der beiden Parameter sichert die Reproduzierbarkeit allein - erst die Kombination traegt. Weiter fuer die Aufgabe gemessen: Der Gewinn kehrt sich um (auf 24 Kernen Faktor 10,2 bei 8 Arbeitern, 12,3 bei 16, 9,0 bei 24) - "so viele Arbeiter wie Kerne" ist damit widerlegt. Und bei 15 Auftraegen laeuft ein Arbeiter ins 60-s-Limit (FEASIBLE, Makespan 200), waehrend acht OPTIMAL mit demselben Makespan 200 nach 26,4 s melden: Der Unterschied liegt nicht in der Loesung, sondern im Beweis, dass es keine bessere gibt. Die abgedruckte Ausgabe traegt die Kennzeichnung "Laufzeiten und die Zahl der verschiedenen Plaene sind hardwareabhaengig" - nach dem Muster, das das Testing-Kapitel fuer Benchmark_Skalierung.py schon verwendet. Der Vergleich des extrahierten Programms mit dem Abdruck weicht denn auch in genau einer Zelle ab (4 statt 3 verschiedene Plaene bei 4 Arbeitern); Zielwert und Struktur sind identisch. Hier ist die Nichtreproduzierbarkeit der abgedruckten Zahl die Aussage selbst. Mitgezogen: Kapitelkopf, Lernziele, Selbsttest, Zusammenfassung, Vorwort-Programmverzeichnis, Uebungsaufgabe und Loesung in Anhang A, ein Verweis aus dem bestehenden Callout zu mehrdeutigen Optima und einer aus Warmstart_Effekt.py im MILP-Kapitel (dort nach Regel 12 der Kapitelname). Stand: 295 Abschnitte, 730 Querverweise, 328 Indexmarken, 76 Programme, 140 Aufgaben mit 140 Loesungen, 33 pytest-Tests, PDF 744 Seiten, 69 netzfreie Programme fehlerfrei. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
dd4acda129
commit
293370b7a1
30 changed files with 12643 additions and 10385 deletions
|
|
@ -431,6 +431,7 @@ Pfad E oder um gezielt nach einem Thema zu suchen:
|
|||
| `Propagation_Demo.py` | Propagation messbar | {ref:kap:cpsat} |
|
||||
| `CP_SAT_Vertretungssystem.py` | Vertretungsplan | {ref:kap:cpsat} |
|
||||
| `JobShop_Intervalle.py` | Job-Shop-Scheduling | {ref:kap:cpsat} |
|
||||
| `Parallele_Suche.py` | num_workers: Tempo gegen Reproduzierbarkeit | {ref:kap:cpsat} |
|
||||
| `CP_SAT_Statusfaelle.py` | Die fünf Solver-Antworten | {ref:kap:cpsat} |
|
||||
| `Strafgewichte.py` | Gewichte als Wechselkurse | {ref:kap:cpsat} |
|
||||
| `Min_Cost_Flow.py` | Netzwerkfluss | {ref:kap:graphen} |
|
||||
|
|
|
|||
|
|
@ -1308,7 +1308,8 @@ def loese(dauer: np.ndarray, n_maschinen: int, zeitlimit: float,
|
|||
|
||||
loeser = cp_model.CpSolver()
|
||||
loeser.parameters.max_time_in_seconds = zeitlimit
|
||||
# Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist.
|
||||
# Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist -
|
||||
# der Seed allein genuegt dafuer NICHT (Kapitel Constraint Programming).
|
||||
# Im Produktivbetrieb laesst man beides auf den Standardwerten.
|
||||
loeser.parameters.num_workers = 1
|
||||
loeser.parameters.random_seed = 1
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
> `Propagation_Demo.py`\
|
||||
> `CP_SAT_Vertretungssystem.py`\
|
||||
> `JobShop_Intervalle.py`\
|
||||
> `Parallele_Suche.py`\
|
||||
> `CP_SAT_Statusfaelle.py`\
|
||||
> `Strafgewichte.py`
|
||||
|
||||
|
|
@ -101,7 +102,9 @@ Nach diesem Kapitel können Sie …
|
|||
6. … die fünf CP-SAT-Statusfälle unterscheiden — insbesondere `INFEASIBLE` (es gibt keine
|
||||
Lösung) von `UNKNOWN` (es wurde keine gefunden) und `MODEL_INVALID` (Ihr Code ist
|
||||
fehlerhaft).
|
||||
7. … begründen, warum ein Strafgewicht ein **Wechselkurs** ist, und welche Regeln deshalb
|
||||
7. … begründen, warum `num_workers = 1` für einen reproduzierbaren Lauf nötig ist und ein
|
||||
fester `random_seed` allein nicht genügt — und was ein Test deshalb prüfen darf.
|
||||
8. … begründen, warum ein Strafgewicht ein **Wechselkurs** ist, und welche Regeln deshalb
|
||||
niemals in die Zielfunktion gehören.
|
||||
|
||||
---
|
||||
|
|
@ -794,6 +797,9 @@ Zusammenspiel von Reihenfolge- und Maschinenbedingungen, nicht aus einer schlech
|
|||
> abgedruckte Ausgabe reproduzierbar. **Im Produktivbetrieb lässt man beides weg** — mehrere
|
||||
> Arbeiter sind deutlich schneller.
|
||||
>
|
||||
> Wie viel schneller, und warum der Seed allein **nicht** genügt, misst
|
||||
> {ref:sec:cpsat-parallele-suche} an genau diesem Modell.
|
||||
>
|
||||
> Die Lehre für Ihre eigenen Tests: Prüfen Sie bei mehrdeutigen Optima nie auf einen
|
||||
> **bestimmten Plan**, sondern immer auf den **Zielwert** und auf die Einhaltung aller
|
||||
> Regeln. Ein Test wie `assert plan == erwarteter_plan` besteht sonst mal und scheitert mal,
|
||||
|
|
@ -817,6 +823,336 @@ theoretischen Ideal entfernt sein können, noch bevor der Solver fertig war.
|
|||
|
||||
---
|
||||
|
||||
## Parallele Suche: was `num_workers` wirklich bewirkt {#sec:cpsat-parallele-suche}
|
||||
|
||||
An mehreren Stellen dieses Buchs steht `loeser.parameters.num_workers = 1`, mit dem
|
||||
Kommentar „für eine reproduzierbare Ausgabe“. Darin stecken zwei Behauptungen: dass
|
||||
parallele Suche **schneller** ist, und dass sie **von Lauf zu Lauf andere, gleich gute**
|
||||
Lösungen liefert. Beides lässt sich messen — und beim Messen kommt mehr heraus als erwartet.
|
||||
|
||||
Gerechnet wird auf demselben Modell wie
|
||||
{ref:sec:cpsat-intervallvariablen-job-shop-scheduling}, nur groß genug, dass die Suche
|
||||
wirklich arbeitet: 12 Aufträge auf 10 Maschinen, jeder Auftrag besucht jede Maschine.
|
||||
Entscheidend ist, dass `random_seed` in **jedem** Lauf denselben Wert hat.
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Parallele_Suche.py
|
||||
"""
|
||||
Kapitel CP-SAT: Was mehrere Arbeiter wirklich bringen - und was sie kosten.
|
||||
|
||||
Das Buch setzt an mehreren Stellen 'num_workers = 1', damit die abgedruckte
|
||||
Ausgabe reproduzierbar ist. Behauptet wird dabei zweierlei: dass parallele
|
||||
Suche schneller ist, und dass sie von Lauf zu Lauf verschiedene, gleich gute
|
||||
Loesungen findet. Beides wird hier gemessen statt geglaubt.
|
||||
|
||||
Gerechnet wird auf einem Job-Shop wie in JobShop_Intervalle.py, nur gross
|
||||
genug, dass die Suche wirklich arbeitet: 12 Auftraege auf 10 Maschinen,
|
||||
jeder Auftrag besucht jede Maschine.
|
||||
|
||||
Zwei Messungen:
|
||||
1. Dieselbe Aufgabe mit 1, 2, 4 und 8 Arbeitern - Laufzeit und Ergebnis.
|
||||
2. Dieselbe Konfiguration mehrfach, bei FESTEM random_seed - wie oft kommt
|
||||
derselbe Plan heraus?
|
||||
|
||||
Die zweite ist die wichtigere: Sie entscheidet, wie man Tests schreibt.
|
||||
|
||||
Achtung, Laufzeiten sind hardwareabhaengig. Die ZAHL der verschiedenen
|
||||
Plaene ist es auch - und genau das ist die Aussage.
|
||||
|
||||
Benoetigt: numpy, ortools
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from ortools.sat.python import cp_model
|
||||
|
||||
AUFTRAEGE = 12
|
||||
MASCHINEN = 10
|
||||
INSTANZ_SEED = 20260908
|
||||
SOLVER_SEED = 1 # bleibt ueber ALLE Laeufe gleich - das ist der Punkt
|
||||
ARBEITERZAHLEN = (1, 2, 4, 8)
|
||||
WIEDERHOLUNGEN = 4
|
||||
ZEITLIMIT = 120.0
|
||||
|
||||
|
||||
def baue_instanz() -> list[list[tuple[int, int]]]:
|
||||
"""Klassischer Job-Shop: Jeder Auftrag besucht jede Maschine genau einmal,
|
||||
in einer eigenen zufaelligen Reihenfolge."""
|
||||
rng = np.random.default_rng(INSTANZ_SEED)
|
||||
return [[(int(m), int(rng.integers(2, 20))) for m in rng.permutation(MASCHINEN)]
|
||||
for _ in range(AUFTRAEGE)]
|
||||
|
||||
|
||||
def loese(auftraege, arbeiter: int):
|
||||
"""Minimiert den Makespan. Gibt Status, Zielwert, Laufzeit und den Plan
|
||||
zurueck - den Plan als Tupel aller Startzeiten, damit sich zwei Laeufe
|
||||
vergleichen lassen."""
|
||||
horizont = sum(dauer for auftrag in auftraege for _, dauer in auftrag)
|
||||
modell = cp_model.CpModel()
|
||||
Gang = collections.namedtuple("Gang", "start ende intervall")
|
||||
plaene: dict[tuple[int, int], Gang] = {}
|
||||
je_maschine = collections.defaultdict(list)
|
||||
|
||||
for a, auftrag in enumerate(auftraege):
|
||||
for g, (maschine, dauer) in enumerate(auftrag):
|
||||
start = modell.NewIntVar(0, horizont, f"start_{a}_{g}")
|
||||
ende = modell.NewIntVar(0, horizont, f"ende_{a}_{g}")
|
||||
intervall = modell.NewIntervalVar(start, dauer, ende, f"iv_{a}_{g}")
|
||||
plaene[a, g] = Gang(start, ende, intervall)
|
||||
je_maschine[maschine].append(intervall)
|
||||
|
||||
for maschine in range(MASCHINEN):
|
||||
modell.AddNoOverlap(je_maschine[maschine])
|
||||
for a, auftrag in enumerate(auftraege):
|
||||
for g in range(len(auftrag) - 1):
|
||||
modell.Add(plaene[a, g + 1].start >= plaene[a, g].ende)
|
||||
|
||||
makespan = modell.NewIntVar(0, horizont, "makespan")
|
||||
modell.AddMaxEquality(
|
||||
makespan, [plaene[a, len(auftrag) - 1].ende
|
||||
for a, auftrag in enumerate(auftraege)])
|
||||
modell.Minimize(makespan)
|
||||
|
||||
loeser = cp_model.CpSolver()
|
||||
loeser.parameters.num_workers = arbeiter
|
||||
loeser.parameters.random_seed = SOLVER_SEED
|
||||
loeser.parameters.max_time_in_seconds = ZEITLIMIT
|
||||
|
||||
beginn = time.perf_counter()
|
||||
status = loeser.Solve(modell)
|
||||
dauer = time.perf_counter() - beginn
|
||||
|
||||
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
|
||||
return loeser.StatusName(status), None, dauer, None
|
||||
plan = tuple(loeser.Value(plaene[a, g].start)
|
||||
for a, auftrag in enumerate(auftraege)
|
||||
for g in range(len(auftrag)))
|
||||
return loeser.StatusName(status), int(loeser.ObjectiveValue()), dauer, plan
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
auftraege = baue_instanz()
|
||||
gaenge = sum(len(a) for a in auftraege)
|
||||
|
||||
print("=" * 78)
|
||||
print(" WAS MEHRERE ARBEITER WIRKLICH BRINGEN")
|
||||
print("=" * 78)
|
||||
print(f"Job-Shop: {AUFTRAEGE} Auftraege, {MASCHINEN} Maschinen, "
|
||||
f"{gaenge} Arbeitsgaenge.")
|
||||
print(f"random_seed = {SOLVER_SEED} in JEDEM Lauf, "
|
||||
f"{WIEDERHOLUNGEN} Wiederholungen je Arbeiterzahl.\n")
|
||||
|
||||
# Jede Konfiguration mehrfach - ein einzelner Zeitwert sagt nichts.
|
||||
ergebnisse: dict[int, list[tuple]] = {}
|
||||
for arbeiter in ARBEITERZAHLEN:
|
||||
ergebnisse[arbeiter] = [loese(auftraege, arbeiter)
|
||||
for _ in range(WIEDERHOLUNGEN)]
|
||||
|
||||
print("=" * 78)
|
||||
print(" (1) Laufzeit: was die Arbeiter bringen")
|
||||
print("=" * 78)
|
||||
print(f"{'Arbeiter':>9} {'Status':>9} {'Makespan':>9} "
|
||||
f"{'schnellste':>11} {'langsamste':>11} {'Beschleunigung':>15}")
|
||||
print("-" * 78)
|
||||
basis_zeit = min(d for _, _, d, _ in ergebnisse[1])
|
||||
for arbeiter in ARBEITERZAHLEN:
|
||||
laeufe = ergebnisse[arbeiter]
|
||||
zeiten = [d for _, _, d, _ in laeufe]
|
||||
ziele = {o for _, o, _, _ in laeufe}
|
||||
status = {s for s, _, _, _ in laeufe}
|
||||
print(f"{arbeiter:>9} {'/'.join(sorted(status)):>9} "
|
||||
f"{'/'.join(str(z) for z in sorted(ziele)):>9} "
|
||||
f"{min(zeiten):>10.2f}s {max(zeiten):>10.2f}s "
|
||||
f"{basis_zeit / min(zeiten):>14.1f}x")
|
||||
|
||||
beste = min(ARBEITERZAHLEN, key=lambda w: min(d for _, _, d, _ in ergebnisse[w]))
|
||||
faktor = basis_zeit / min(d for _, _, d, _ in ergebnisse[beste])
|
||||
print(f"\n Am schnellsten sind {beste} Arbeiter: Faktor {faktor:.1f} gegenueber")
|
||||
print(f" einem einzigen - also MEHR, als {beste} Kerne hergeben sollten.")
|
||||
print(f" Das ist kein Messfehler. CP-SAT laesst nicht {beste}-mal dieselbe Suche")
|
||||
print(" laufen, sondern verschiedene Strategien nebeneinander, die einander")
|
||||
print(" ihre Schranken zurufen. Der zweite Arbeiter ist deshalb nicht der")
|
||||
print(" 'zweite Kern', sondern ein anderes Verfahren.")
|
||||
letzte = ARBEITERZAHLEN[-1]
|
||||
if beste != letzte:
|
||||
zeit_letzte = min(d for _, _, d, _ in ergebnisse[letzte])
|
||||
print(f"\n Und mehr ist nicht immer besser: {letzte} Arbeiter brauchen "
|
||||
f"{zeit_letzte:.2f}s")
|
||||
print(f" gegenueber {min(d for _, _, d, _ in ergebnisse[beste]):.2f}s bei "
|
||||
f"{beste}. Ab einer gewissen Zahl kosten Abstimmung")
|
||||
print(" und Speicherbandbreite mehr, als ein weiterer Suchstrang einbringt.")
|
||||
|
||||
print("\n" + "=" * 78)
|
||||
print(" (2) Reproduzierbarkeit: derselbe Seed, derselbe Plan?")
|
||||
print("=" * 78)
|
||||
print(f"{'Arbeiter':>9} {'Laeufe':>7} {'verschiedene Plaene':>21} "
|
||||
f"{'Makespan':>10}")
|
||||
print("-" * 78)
|
||||
for arbeiter in ARBEITERZAHLEN:
|
||||
plaene = {p for _, _, _, p in ergebnisse[arbeiter] if p is not None}
|
||||
ziele = {o for _, o, _, _ in ergebnisse[arbeiter]}
|
||||
print(f"{arbeiter:>9} {WIEDERHOLUNGEN:>7} {len(plaene):>21} "
|
||||
f"{'/'.join(str(z) for z in sorted(ziele)):>10}")
|
||||
|
||||
einer = len({p for _, _, _, p in ergebnisse[1]})
|
||||
# Ab welcher Arbeiterzahl bricht die Reproduzierbarkeit? Das ist die
|
||||
# eigentliche Zahl - nicht, was acht Arbeiter anrichten.
|
||||
ab = next((w for w in ARBEITERZAHLEN
|
||||
if len({p for _, _, _, p in ergebnisse[w]}) > 1), None)
|
||||
print(f"\n Mit einem Arbeiter: {einer} Plan aus {WIEDERHOLUNGEN} Laeufen.")
|
||||
if ab is not None:
|
||||
viele = len({p for _, _, _, p in ergebnisse[ab]})
|
||||
print(f" Schon mit {ab} Arbeitern: {viele} verschiedene Plaene aus "
|
||||
f"{WIEDERHOLUNGEN} Laeufen -")
|
||||
print(" bei identischem random_seed und identischem Zielwert.")
|
||||
print(f" Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit")
|
||||
print(f" zu verlieren. {ab} genuegen.")
|
||||
print("\n Der Seed allein macht einen Lauf also NICHT reproduzierbar. Er legt")
|
||||
print(" fest, wie ein einzelner Suchstrang wuerfelt - nicht, welcher von")
|
||||
print(" mehreren zuerst fertig wird. Das entscheidet die Uhr.")
|
||||
|
||||
print("\n" + "=" * 78)
|
||||
print(" (3) Was daraus fuer Tests folgt")
|
||||
print("=" * 78)
|
||||
print(" Ein Test der Form assert plan == erwarteter_plan besteht mal und")
|
||||
print(" scheitert mal, ohne dass sich am Code etwas geaendert haette.")
|
||||
print(" Zu pruefen sind stattdessen:")
|
||||
print(" * der ZIELWERT (hier in allen Laeufen gleich),")
|
||||
print(" * die Einhaltung aller Regeln (Abnahmepruefung, Kapitel Praxisfallen),")
|
||||
print(" * und der Status - nicht die Gestalt der Loesung.")
|
||||
print("\n Wer doch einen bestimmten Plan braucht - fuer eine abgedruckte")
|
||||
print(" Ausgabe, fuer einen Regressionstest -, setzt num_workers = 1.")
|
||||
print(f" Das kostet hier den Faktor {faktor:.1f} an Laufzeit und ist genau")
|
||||
print(" deshalb eine Entscheidung fuer den Test, nicht fuer den Betrieb.")
|
||||
print("=" * 78)
|
||||
```
|
||||
|
||||
**Erwartete Ausgabe** (Laufzeiten *und* die Zahl der verschiedenen Pläne sind
|
||||
hardwareabhängig — genau das ist die Aussage; der Zielwert ist es nicht):
|
||||
|
||||
```
|
||||
==============================================================================
|
||||
WAS MEHRERE ARBEITER WIRKLICH BRINGEN
|
||||
==============================================================================
|
||||
Job-Shop: 12 Auftraege, 10 Maschinen, 120 Arbeitsgaenge.
|
||||
random_seed = 1 in JEDEM Lauf, 4 Wiederholungen je Arbeiterzahl.
|
||||
|
||||
==============================================================================
|
||||
(1) Laufzeit: was die Arbeiter bringen
|
||||
==============================================================================
|
||||
Arbeiter Status Makespan schnellste langsamste Beschleunigung
|
||||
------------------------------------------------------------------------------
|
||||
1 OPTIMAL 183 5.62s 5.82s 1.0x
|
||||
2 OPTIMAL 183 1.43s 2.04s 3.9x
|
||||
4 OPTIMAL 183 0.60s 0.66s 9.4x
|
||||
8 OPTIMAL 183 0.46s 0.65s 12.3x
|
||||
|
||||
Am schnellsten sind 8 Arbeiter: Faktor 12.3 gegenueber
|
||||
einem einzigen - also MEHR, als 8 Kerne hergeben sollten.
|
||||
Das ist kein Messfehler. CP-SAT laesst nicht 8-mal dieselbe Suche
|
||||
laufen, sondern verschiedene Strategien nebeneinander, die einander
|
||||
ihre Schranken zurufen. Der zweite Arbeiter ist deshalb nicht der
|
||||
'zweite Kern', sondern ein anderes Verfahren.
|
||||
|
||||
==============================================================================
|
||||
(2) Reproduzierbarkeit: derselbe Seed, derselbe Plan?
|
||||
==============================================================================
|
||||
Arbeiter Laeufe verschiedene Plaene Makespan
|
||||
------------------------------------------------------------------------------
|
||||
1 4 1 183
|
||||
2 4 3 183
|
||||
4 4 3 183
|
||||
8 4 4 183
|
||||
|
||||
Mit einem Arbeiter: 1 Plan aus 4 Laeufen.
|
||||
Schon mit 2 Arbeitern: 3 verschiedene Plaene aus 4 Laeufen -
|
||||
bei identischem random_seed und identischem Zielwert.
|
||||
Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit
|
||||
zu verlieren. 2 genuegen.
|
||||
|
||||
Der Seed allein macht einen Lauf also NICHT reproduzierbar. Er legt
|
||||
fest, wie ein einzelner Suchstrang wuerfelt - nicht, welcher von
|
||||
mehreren zuerst fertig wird. Das entscheidet die Uhr.
|
||||
|
||||
==============================================================================
|
||||
(3) Was daraus fuer Tests folgt
|
||||
==============================================================================
|
||||
Ein Test der Form assert plan == erwarteter_plan besteht mal und
|
||||
scheitert mal, ohne dass sich am Code etwas geaendert haette.
|
||||
Zu pruefen sind stattdessen:
|
||||
* der ZIELWERT (hier in allen Laeufen gleich),
|
||||
* die Einhaltung aller Regeln (Abnahmepruefung, Kapitel Praxisfallen),
|
||||
* und der Status - nicht die Gestalt der Loesung.
|
||||
|
||||
Wer doch einen bestimmten Plan braucht - fuer eine abgedruckte
|
||||
Ausgabe, fuer einen Regressionstest -, setzt num_workers = 1.
|
||||
Das kostet hier den Faktor 12.3 an Laufzeit und ist genau
|
||||
deshalb eine Entscheidung fuer den Test, nicht fuer den Betrieb.
|
||||
==============================================================================
|
||||
```
|
||||
|
||||
### Was die Messung zeigt
|
||||
|
||||
**Erstens: Die Beschleunigung ist überlinear.** Acht Arbeiter sind im abgedruckten Lauf
|
||||
nicht achtmal, sondern **gut zwölfmal** schneller als einer. Das sieht nach Messfehler aus, ist aber die
|
||||
korrekte Beschreibung dessen, was CP-SAT tut: Es lässt **nicht** achtmal dieselbe Suche auf
|
||||
verschiedenen Kernen laufen, sondern **verschiedene Strategien nebeneinander** — Suche mit
|
||||
LP-Relaxation, Suche mit anderen Verzweigungsregeln, lokale Suche —, und die teilen
|
||||
einander ihre Schranken mit. Der zweite Arbeiter ist deshalb nicht „der zweite Kern“,
|
||||
sondern ein anderes Verfahren, das zufällig auch rechnet.
|
||||
|
||||
Daraus folgt zweierlei. Ein Speedup über der Kernzahl ist bei CP-SAT normal und kein Grund
|
||||
zum Misstrauen. Und der Zugewinn flacht ab: Zwischen vier und acht Arbeitern lag in unseren
|
||||
Wiederholungen mal das eine, mal das andere vorn — ab einer gewissen Zahl kosten Abstimmung
|
||||
und Speicherbandbreite mehr, als ein weiterer Suchstrang einbringt. Auch hier gilt: messen,
|
||||
nicht schätzen.
|
||||
|
||||
**Zweitens — und das ist der Befund, der die Testpraxis bestimmt: Der Seed genügt nicht.**
|
||||
Bei einem Arbeiter liefern vier Läufe **einen** Plan. Schon bei **zwei** Arbeitern liefern
|
||||
vier Läufe **drei verschiedene** — bei identischem `random_seed` und identischem Zielwert
|
||||
183.
|
||||
|
||||
Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit zu verlieren. Zwei genügen.
|
||||
Der Grund ist einfach, sobald man ihn ausspricht: Der Seed legt fest, wie ein **einzelner**
|
||||
Suchstrang würfelt. Er legt nicht fest, **welcher von mehreren zuerst fertig wird** — das
|
||||
entscheidet die Uhr, und die ist bei jedem Lauf anders.
|
||||
|
||||
> **⚠️ `random_seed` allein macht nichts reproduzierbar**
|
||||
> Ein häufiger Irrtum: „Ich habe doch einen Seed gesetzt.“ Für einen Zufallsgenerator
|
||||
> stimmt das. Für einen **parallelen** Solver nicht — dort ist die zweite Zufallsquelle die
|
||||
> Ausführungsreihenfolge, und die lässt sich nicht mit einem Parameter festnageln.
|
||||
> Reproduzierbar wird der Lauf erst mit `num_workers = 1` **und** Seed. Wer nur eines von
|
||||
> beidem setzt, hat einen Test, der gelegentlich grundlos rot wird.
|
||||
|
||||
### Was daraus für Tests folgt
|
||||
|
||||
Der Zielwert war in **allen** sechzehn Läufen derselbe: 183. Die Gestalt der Lösung war es
|
||||
nicht. Genau daran entscheidet sich, was ein Test prüfen darf:
|
||||
|
||||
| Prüfen | Nicht prüfen |
|
||||
| --- | --- |
|
||||
| den **Zielwert** — er ist eindeutig, wenn das Modell optimal gelöst wurde | die **Gestalt** des Plans (`assert plan == erwarteter_plan`) |
|
||||
| die **Einhaltung aller Regeln** über eine Abnahmeprüfung ({ref:sec:praxisfallen-or-kern}) | die Reihenfolge, in der gleichwertige Aufträge zugewiesen wurden |
|
||||
| den **Status** (`OPTIMAL`, `FEASIBLE`, `INFEASIBLE`) | die Laufzeit als feste Zahl |
|
||||
|
||||
Wer doch einen bestimmten Plan braucht — für eine abgedruckte Ausgabe, für einen
|
||||
Regressionstest —, setzt `num_workers = 1`. Das kostet hier den Faktor 12 an Laufzeit, und
|
||||
genau deshalb ist es eine Entscheidung **für den Test**, nicht für den Betrieb. In
|
||||
Produktion lässt man beide Parameter auf ihren Standardwerten.
|
||||
|
||||
> **🎯 Merksatz**
|
||||
> Ein paralleler Solver hat zwei Zufallsquellen: den Seed und die Uhr. Nur die erste lässt
|
||||
> sich setzen. Wer Reproduzierbarkeit braucht, muss die zweite abschalten — und das heißt
|
||||
> `num_workers = 1`.
|
||||
|
||||
---
|
||||
|
||||
## Die fünf Antworten von CP-SAT {#sec:cpsat-statusfaelle}
|
||||
|
||||
{ref:sec:milp-gap} hat gezeigt, warum `OPTIMAL` bei ganzzahligen Problemen nicht der
|
||||
|
|
@ -1180,6 +1516,16 @@ Lösen Sie ein 9×9-Sudoku mit CP-SAT. Sie brauchen nur `NewIntVar(1, 9, ...)` u
|
|||
`AddAllDifferent` für Zeilen, Spalten und die neun 3×3-Blöcke. Wie viele Zeilen Modellcode
|
||||
benötigen Sie?
|
||||
|
||||
**Aufgabe ⭐⭐ — Die Grenze der Parallelität finden.**
|
||||
`Parallele_Suche.py` misst 1, 2, 4 und 8 Arbeiter.
|
||||
(a) Ergänzen Sie 16 und 24 (bzw. die Kernzahl Ihres Rechners). Ab wo bringt ein weiterer
|
||||
Arbeiter nichts mehr — und wird es irgendwann wieder schlechter?
|
||||
(b) Setzen Sie `random_seed` bei jedem Lauf auf einen **anderen** Wert, aber
|
||||
`num_workers = 1`. Wie viele verschiedene Pläne kommen jetzt heraus? Was folgt daraus
|
||||
für die Frage, welcher der beiden Parameter die Reproduzierbarkeit sichert?
|
||||
(c) Vergrößern Sie die Instanz auf 15 Aufträge. Ab welcher Größe erreicht der Lauf mit
|
||||
einem Arbeiter das Zeitlimit, während acht noch `OPTIMAL` melden?
|
||||
|
||||
**Aufgabe ⭐⭐⭐ — Job-Shop erweitern.**
|
||||
Ergänzen Sie `JobShop_Intervalle.py` um:
|
||||
(a) Rüstzeiten von 1 Zeiteinheit zwischen zwei Aufträgen auf derselben Maschine.
|
||||
|
|
@ -1466,6 +1812,10 @@ Nebenbedingungen. Alles, was einen Preis hat, wird irgendwann gekauft.
|
|||
3. Was ist eine Intervallvariable, und welche Bedingung garantiert sie automatisch?
|
||||
4. Warum sollten möglichst wenige Bedingungen hart formuliert werden?
|
||||
5. Warum ist die Aufschlüsselung der Strafkosten in der Praxis wichtiger als der Zielwert
|
||||
6. Sie setzen `random_seed = 1`, bekommen aber trotzdem bei jedem Lauf einen anderen Plan.
|
||||
Woran liegt das, und was hilft?
|
||||
7. Warum kann ein CP-SAT-Lauf mit acht Arbeitern **mehr** als achtmal schneller sein als
|
||||
mit einem?
|
||||
selbst?
|
||||
|
||||
---
|
||||
|
|
@ -1491,6 +1841,13 @@ Nebenbedingungen. Alles, was einen Preis hat, wird irgendwann gekauft.
|
|||
* **Bei mehrdeutigen Optima nie auf einen bestimmten Plan testen** — nur auf den Zielwert
|
||||
und die Einhaltung aller Regeln.
|
||||
|
||||
* **Ein paralleler Solver hat zwei Zufallsquellen: den Seed und die Uhr.** Nur die erste
|
||||
lässt sich setzen. Reproduzierbar wird ein Lauf erst mit `num_workers = 1` **und** Seed —
|
||||
gemessen in {ref:sec:cpsat-parallele-suche}: schon zwei Arbeiter liefern bei identischem
|
||||
Seed drei verschiedene Pläne zum selben Zielwert.
|
||||
* **Parallele Suche kann überlinear beschleunigen**, weil CP-SAT nicht dieselbe Suche
|
||||
vervielfacht, sondern verschiedene Strategien nebeneinander laufen lässt, die einander
|
||||
ihre Schranken mitteilen.
|
||||
**Ausblick.** {ref:kap:graphen} behandelt Probleme, die sich am besten als **Graph** begreifen
|
||||
lassen: Flüsse durch Netzwerke, Zuordnungen und die Tourenplanung mit Fahrzeugen und
|
||||
Zeitfenstern.
|
||||
|
|
|
|||
|
|
@ -850,6 +850,31 @@ for i in range(9):
|
|||
**Etwa 10 Zeilen Modellcode** — und der Solver löst jedes Sudoku in Millisekunden. Das ist
|
||||
die Stärke globaler Constraints.
|
||||
|
||||
**{loesung} — Die Grenze der Parallelität finden.**
|
||||
(a) Der Gewinn flacht ab und kehrt sich um. Auf einem Rechner mit 24 Kernen gemessen
|
||||
(Faktor gegenüber einem Arbeiter): 8 Arbeiter **10,2**, 16 Arbeiter **12,3**,
|
||||
24 Arbeiter **9,0**. Wer alle Kerne belegt, ist langsamer als mit zwei Dritteln — dem
|
||||
Betriebssystem, dem Speicherbus und den Suchsträngen untereinander bleibt zu wenig Luft.
|
||||
Die Faustregel „so viele Arbeiter wie Kerne" ist also falsch; die richtige Zahl ist eine
|
||||
Messung, keine Einstellung.
|
||||
(b) **Fünf verschiedene Pläne aus fünf Seeds** — bei `num_workers = 1` und identischem
|
||||
Zielwert 183. Das ist die andere Hälfte der Antwort: Der Seed *wirkt*, aber nur auf den
|
||||
einzelnen Suchstrang. Es sichert also **keiner der beiden Parameter allein** die
|
||||
Reproduzierbarkeit:
|
||||
|
||||
| | Seed fest | Seed variabel |
|
||||
| --- | --- | --- |
|
||||
| **1 Arbeiter** | reproduzierbar | 5 Pläne aus 5 Läufen |
|
||||
| **mehrere Arbeiter** | 3 Pläne aus 4 Läufen | erst recht nicht |
|
||||
|
||||
Nur die Kombination trägt.
|
||||
(c) Bei **15 Aufträgen** (60 s Zeitlimit): Ein Arbeiter läuft ins Limit und meldet
|
||||
`FEASIBLE` mit Makespan 200; acht Arbeiter melden `OPTIMAL` mit **demselben** Makespan 200
|
||||
nach 26,4 s. Bei 13 Aufträgen sind es 11,2 s gegen 2,8 s, bei 14 schon 21,4 s gegen 3,7 s.
|
||||
Bemerkenswert ist, dass beide denselben Wert finden — der Unterschied liegt nicht in der
|
||||
Lösung, sondern im **Beweis**, dass es keine bessere gibt. Genau diese Arbeit teilen sich
|
||||
die parallelen Stränge auf.
|
||||
|
||||
**{loesung} — Job-Shop erweitern.**
|
||||
(a) Rüstzeiten: `AddNoOverlap` durch paarweise Disjunktionen mit Übergangszeit ersetzen,
|
||||
oder `AddCircuit` je Maschine mit Übergangsmatrix.
|
||||
|
|
@ -950,6 +975,15 @@ Regeln. Wer eine reproduzierbare Ausgabe braucht — etwa für ein Buch —, fix
|
|||
4. Weil jede zusätzliche harte Bedingung das Risiko von `INFEASIBLE` erhöht — und eine
|
||||
Fehlermeldung dem Anwender nicht hilft.
|
||||
5. Weil Anwender die Frage „Warum ich?“ stellen. Ohne Antwort wird das System umgangen.
|
||||
6. Weil der Seed nur festlegt, wie ein **einzelner** Suchstrang würfelt — nicht, welcher
|
||||
von mehreren parallelen zuerst fertig wird. Das entscheidet die Ausführungsreihenfolge,
|
||||
also die Uhr. Abhilfe: zusätzlich `num_workers = 1`. Gemessen: schon zwei Arbeiter
|
||||
liefern bei identischem Seed drei verschiedene Pläne zum selben Zielwert.
|
||||
7. Weil CP-SAT nicht dieselbe Suche vervielfacht, sondern **verschiedene Strategien**
|
||||
nebeneinander laufen lässt (LP-gestützt, andere Verzweigungsregeln, lokale Suche), die
|
||||
einander ihre Schranken mitteilen. Ein zusätzlicher Arbeiter ist deshalb nicht ein
|
||||
weiterer Kern für dieselbe Arbeit, sondern ein anderes Verfahren — und wenn eines davon
|
||||
früh eine gute Schranke findet, profitieren alle.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue