flexsds 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. flexsds/__init__.py +4 -0
  2. flexsds/assets/ptit_logo.png +0 -0
  3. flexsds/cli/__init__.py +1 -0
  4. flexsds/cli/banner.py +84 -0
  5. flexsds/cli/main.py +330 -0
  6. flexsds/cli/output.py +69 -0
  7. flexsds/cli/task_runner.py +171 -0
  8. flexsds/cli/wizard.py +360 -0
  9. flexsds/client/__init__.py +4 -0
  10. flexsds/client/chat.py +137 -0
  11. flexsds/client/client.py +107 -0
  12. flexsds/client/monitor.py +95 -0
  13. flexsds/core/__init__.py +44 -0
  14. flexsds/core/compatibility.py +442 -0
  15. flexsds/core/controller.py +502 -0
  16. flexsds/core/cost_model.py +530 -0
  17. flexsds/core/credentials.py +111 -0
  18. flexsds/core/hardware.py +77 -0
  19. flexsds/core/tree_topology.py +296 -0
  20. flexsds/profiles/profile_sm75_t4.json +62 -0
  21. flexsds/profiles/profile_sm89_l4.json +194 -0
  22. flexsds/server/__init__.py +1 -0
  23. flexsds/server/colab_runner.py +62 -0
  24. flexsds/server/local_runner.py +208 -0
  25. flexsds/server/modal_app.py +50 -0
  26. flexsds/server/modal_runner.py +98 -0
  27. flexsds/server/vscode_cli.py +82 -0
  28. flexsds-0.2.0.dist-info/METADATA +204 -0
  29. flexsds-0.2.0.dist-info/RECORD +62 -0
  30. flexsds-0.2.0.dist-info/WHEEL +5 -0
  31. flexsds-0.2.0.dist-info/entry_points.txt +2 -0
  32. flexsds-0.2.0.dist-info/top_level.txt +4 -0
  33. pipeline/entity_eval.py +171 -0
  34. pipeline/examples/node1_extractor_input_m0_04_001.txt +9 -0
  35. pipeline/examples/node1_extractor_output_m0_04_001.json +90 -0
  36. pipeline/extractor.py +502 -0
  37. pipeline/invoice_models.py +163 -0
  38. pipeline/node2_questions.py +30 -0
  39. pipeline/openai_compatible.py +87 -0
  40. pipeline/prompts/README.md +18 -0
  41. pipeline/prompts/node1_extractor_system.md +92 -0
  42. pipeline/prompts/node3_report_user.md +5 -0
  43. pipeline/report_writer.py +143 -0
  44. pipeline/schemas/invoice_extraction.schema.json +469 -0
  45. pipeline/sql_guard.py +140 -0
  46. pipeline/sqlite/schema.sql +219 -0
  47. pipeline/text_to_sql.py +170 -0
  48. pipeline/validation.py +166 -0
  49. pipeline/w1_workload.py +139 -0
  50. proposer/__init__.py +31 -0
  51. proposer/adapter.py +153 -0
  52. proposer/controller.py +460 -0
  53. proposer/cost_model.py +216 -0
  54. proposer/tree_adaptive.py +466 -0
  55. server/__init__.py +3 -0
  56. server/adaptive_scheduler.py +215 -0
  57. server/budget_planner.py +287 -0
  58. server/lineage.py +290 -0
  59. server/lineage_diagnostics.py +38 -0
  60. server/lineage_executor.py +129 -0
  61. server/lineage_startup.py +47 -0
  62. server/t4_rowwise_attention.py +262 -0
flexsds/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """FlexSDS: Flexible Speculative Decoding Serving Engine for LLMs."""
2
+
3
+ __version__ = "0.2.0"
4
+ __author__ = "Can Duc Khoi, Bui Duc Dai & Nguyen The Duong"
Binary file
@@ -0,0 +1 @@
1
+ """FlexSDS CLI module."""
flexsds/cli/banner.py ADDED
@@ -0,0 +1,84 @@
1
+ """FlexSDS branding banner module with PTIT logo in ANSI TrueColor."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+ from typing import Optional
6
+ try:
7
+ from flexsds.cli.output import ascii_text, print_output, supports_truecolor, supports_unicode
8
+ from flexsds.core.hardware import HardwareInfo, detect_hardware
9
+ except ModuleNotFoundError:
10
+ from src.flexsds.cli.output import ascii_text, print_output, supports_truecolor, supports_unicode
11
+ from src.flexsds.core.hardware import HardwareInfo, detect_hardware
12
+
13
+ # Pre-compiled ANSI grayscale half-blocks for the official PTIT logo (26 cols x 12 rows)
14
+ # Zero-dependency, sub-millisecond monochrome rendering without PIL at runtime.
15
+ PTIT_LOGO_ANSI = [
16
+ " \033[38;2;181;181;181m▄\033[0m\033[38;2;112;112;112m▄\033[0m\033[38;2;185;185;185;48;2;119;119;119m▀\033[0m\033[38;2;128;128;128;48;2;154;154;154m▀\033[0m\033[38;2;95;95;95;48;2;176;176;176m▀\033[0m\033[38;2;81;81;81;48;2;175;175;175m▀\033[0m\033[38;2;83;83;83;48;2;145;145;145m▀\033[0m\033[38;2;103;103;103;48;2;95;95;95m▀\033[0m\033[38;2;140;140;140;48;2;72;72;72m▀\033[0m\033[38;2;214;214;214;48;2;87;87;87m▀\033[0m\033[38;2;196;196;196m▄\033[0m ",
17
+ " \033[38;2;170;170;170m▄\033[0m\033[38;2;202;202;202;48;2;165;165;165m▀\033[0m\033[38;2;139;139;139;48;2;222;222;222m▀\033[0m\033[38;2;150;150;150m▀\033[0m\033[38;2;221;221;221m▀\033[0m \033[38;2;149;149;149m▀\033[0m\033[38;2;74;74;74;48;2;138;138;138m▀\033[0m\033[38;2;101;101;101;48;2;74;74;74m▀\033[0m\033[38;2;216;216;216m▄\033[0m ",
18
+ " \033[38;2;211;211;211m▄\033[0m\033[38;2;215;215;215;48;2;152;152;152m▀\033[0m\033[38;2;169;169;169m▀\033[0m\033[38;2;215;215;215m▀\033[0m \033[38;2;222;222;222m▀\033[0m\033[38;2;76;76;76;48;2;99;99;99m▀\033[0m\033[38;2;206;206;206;48;2;214;214;214m▀\033[0m ",
19
+ " \033[38;2;221;221;221;48;2;108;108;108m▀\033[0m\033[38;2;125;125;125;48;2;217;217;217m▀\033[0m \033[38;2;120;120;120;48;2;145;145;145m▀\033[0m ",
20
+ " \033[38;2;177;177;177m▄\033[0m\033[38;2;124;124;124;48;2;87;87;87m▀\033[0m\033[38;2;155;155;155m▀\033[0m \033[38;2;231;231;231m▄\033[0m \033[38;2;208;208;208;48;2;187;187;187m▀\033[0m\033[38;2;187;187;187m▀\033[0m ",
21
+ " \033[38;2;160;160;160m▄\033[0m\033[38;2;80;80;80;48;2;71;71;71m▀\033[0m\033[38;2;160;160;160;48;2;227;227;227m▀\033[0m \033[38;2;185;185;185m▄\033[0m\033[38;2;181;181;181m▄\033[0m\033[38;2;178;178;178m▄\033[0m\033[38;2;212;212;212m▄\033[0m \033[38;2;183;183;183m▄\033[0m\033[38;2;185;185;185m▄\033[0m\033[38;2;193;193;193m▄\033[0m\033[38;2;176;176;176m▄\033[0m\033[38;2;210;210;210m▄\033[0m\033[38;2;232;232;232m▄\033[0m\033[38;2;230;230;230m▄\033[0m\033[38;2;218;218;218;48;2;181;181;181m▀\033[0m\033[38;2;219;219;219;48;2;207;207;207m▀\033[0m\033[38;2;229;229;229m▄\033[0m\033[38;2;227;227;227;48;2;170;170;170m▀\033[0m\033[38;2;173;173;173m▄\033[0m\033[38;2;181;181;181m▄\033[0m\033[38;2;166;166;166m▄\033[0m\033[38;2;198;198;198m▄\033[0m",
22
+ " \033[38;2;209;209;209m▄\033[0m\033[38;2;98;98;98;48;2;71;71;71m▀\033[0m\033[38;2;100;100;100;48;2;132;132;132m▀\033[0m \033[38;2;180;180;180;48;2;181;181;181m▀\033[0m\033[38;2;77;77;77;48;2;95;95;95m▀\033[0m\033[38;2;179;179;179;48;2;225;225;225m▀\033[0m\033[38;2;170;170;170;48;2;218;218;218m▀\033[0m\033[38;2;94;94;94;48;2;92;92;92m▀\033[0m\033[38;2;155;155;155;48;2;145;145;145m▀\033[0m\033[38;2;173;173;173m▀\033[0m\033[38;2;129;129;129;48;2;196;196;196m▀\033[0m\033[38;2;90;90;90;48;2;111;111;111m▀\033[0m\033[38;2;161;161;161m▀\033[0m\033[38;2;206;206;206;48;2;246;246;246m▀\033[0m\033[38;2;218;218;218;48;2;210;210;210m▀\033[0m\033[38;2;173;173;173;48;2;185;185;185m▀\033[0m\033[38;2;194;194;194;48;2;205;205;205m▀\033[0m\033[38;2;232;232;232m▀\033[0m \033[38;2;177;177;177m▀\033[0m\033[38;2;137;137;137;48;2;200;200;200m▀\033[0m\033[38;2;86;86;86;48;2;107;107;107m▀\033[0m\033[38;2;172;172;172m▀\033[0m\033[38;2;202;202;202m▀\033[0m",
23
+ " \033[38;2;165;165;165;48;2;138;138;138m▀\033[0m\033[38;2;66;66;66;48;2;68;68;68m▀\033[0m\033[38;2;149;149;149;48;2;149;149;149m▀\033[0m \033[38;2;183;183;183;48;2;173;173;173m▀\033[0m\033[38;2;86;86;86;48;2;86;86;86m▀\033[0m\033[38;2;143;143;143m▀\033[0m\033[38;2;138;138;138m▀\033[0m\033[38;2;168;168;168m▀\033[0m \033[38;2;188;188;188;48;2;184;184;184m▀\033[0m\033[38;2;110;110;110;48;2;95;95;95m▀\033[0m \033[38;2;221;221;221m▄\033[0m\033[38;2;222;222;222;48;2;107;107;107m▀\033[0m\033[38;2;144;144;144;48;2;129;129;129m▀\033[0m\033[38;2;133;133;133;48;2;92;92;92m▀\033[0m \033[38;2;190;190;190;48;2;186;186;186m▀\033[0m\033[38;2;106;106;106;48;2;92;92;92m▀\033[0m ",
24
+ " \033[38;2;126;126;126;48;2;135;135;135m▀\033[0m\033[38;2;72;72;72;48;2;78;78;78m▀\033[0m\033[38;2;131;131;131;48;2;97;97;97m▀\033[0m \033[38;2;217;217;217m▀\033[0m\033[38;2;178;178;178m▀\033[0m \033[38;2;221;221;221m▀\033[0m\033[38;2;182;182;182;48;2;157;157;157m▀\033[0m\033[38;2;198;198;198;48;2;68;68;68m▀\033[0m\033[38;2;81;81;81;48;2;101;101;101m▀\033[0m\033[38;2;106;106;106;48;2;224;224;224m▀\033[0m\033[38;2;197;197;197m▀\033[0m\033[38;2;179;179;179m▀\033[0m \033[38;2;223;223;223m▀\033[0m\033[38;2;181;181;181m▀\033[0m ",
25
+ " \033[38;2;193;193;193m▀\033[0m\033[38;2;74;74;74;48;2;165;165;165m▀\033[0m\033[38;2;77;77;77;48;2;96;96;96m▀\033[0m\033[38;2;145;145;145;48;2;86;86;86m▀\033[0m\033[38;2;152;152;152m▄\033[0m \033[38;2;227;227;227m▄\033[0m\033[38;2;166;166;166m▄\033[0m\033[38;2;93;93;93m▄\033[0m\033[38;2;175;175;175;48;2;69;69;69m▀\033[0m\033[38;2;87;87;87;48;2;121;121;121m▀\033[0m\033[38;2;68;68;68;48;2;205;205;205m▀\033[0m\033[38;2;142;142;142m▀\033[0m ",
26
+ " \033[38;2;195;195;195m▀\033[0m\033[38;2;148;148;148;48;2;209;209;209m▀\033[0m\033[38;2;124;124;124;48;2;179;179;179m▀\033[0m\033[38;2;130;130;130;48;2;160;160;160m▀\033[0m\033[38;2;117;117;117;48;2;164;164;164m▀\033[0m\033[38;2;88;88;88;48;2;193;193;193m▀\033[0m\033[38;2;94;94;94;48;2;226;226;226m▀\033[0m\033[38;2;147;147;147m▀\033[0m\033[38;2;218;218;218m▀\033[0m ",
27
+ "\033[38;2;125;125;125;48;2;141;141;141m▀\033[0m\033[38;2;118;118;118;48;2;136;136;136m▀\033[0m\033[38;2;119;119;119;48;2;137;137;137m▀\033[0m\033[38;2;120;120;120;48;2;138;138;138m▀\033[0m\033[38;2;125;125;125;48;2;137;137;137m▀\033[0m\033[38;2;130;130;130;48;2;136;136;136m▀\033[0m\033[38;2;133;133;133;48;2;136;136;136m▀\033[0m\033[38;2;132;132;132;48;2;137;137;137m▀\033[0m\033[38;2;128;128;128;48;2;138;138;138m▀\033[0m\033[38;2;122;122;122;48;2;140;140;140m▀\033[0m\033[38;2;118;118;118;48;2;141;141;141m▀\033[0m\033[38;2;113;113;113;48;2;136;136;136m▀\033[0m\033[38;2;148;148;148;48;2;141;141;141m▀\033[0m\033[38;2;149;149;149;48;2;144;144;144m▀\033[0m\033[38;2;114;114;114;48;2;134;134;134m▀\033[0m\033[38;2;118;118;118;48;2;143;143;143m▀\033[0m\033[38;2;118;118;118;48;2;140;140;140m▀\033[0m\033[38;2;118;118;118;48;2;140;140;140m▀\033[0m\033[38;2;118;118;118;48;2;140;140;140m▀\033[0m\033[38;2;118;118;118;48;2;139;139;139m▀\033[0m\033[38;2;119;119;119;48;2;139;139;139m▀\033[0m\033[38;2;119;119;119;48;2;138;138;138m▀\033[0m\033[38;2;119;119;119;48;2;138;138;138m▀\033[0m\033[38;2;119;119;119;48;2;137;137;137m▀\033[0m\033[38;2;119;119;119;48;2;137;137;137m▀\033[0m\033[38;2;119;119;119;48;2;137;137;137m▀\033[0m",
28
+ ]
29
+
30
+ def render_banner(hw_info: Optional[HardwareInfo] = None) -> str:
31
+ """Builds and returns the complete side-by-side FlexSDS banner."""
32
+ if hw_info is None:
33
+ hw_info = detect_hardware()
34
+
35
+ gpu_str = hw_info.summary()
36
+
37
+ info_lines = [
38
+ "\033[1;37m ███████╗██╗ ███████╗██╗ ██╗███████╗██████╗ ███████╗\033[0m",
39
+ "\033[1;37m ██╔════╝██║ ██╔════╝╚██╗██╔╝██╔════╝██╔══██╗██╔════╝\033[0m",
40
+ "\033[1;37m █████╗ ██║ █████╗ ╚███╔╝ ███████╗██║ ██║███████╗\033[0m",
41
+ "\033[1;37m ██╔══╝ ██║ ██╔══╝ ██╔██╗ ╚════██║██║ ██║╚════██║\033[0m",
42
+ "\033[1;37m ██║ ███████╗███████╗██╔╝ ██╗███████║██████╔╝███████║\033[0m",
43
+ "",
44
+ " \033[1;37mFlexSDS\033[0m \033[90m· Speculative Decoding Serving Engine · v0.2.0-alpha\033[0m",
45
+ " \033[90mNhóm đồ án D22\033[0m",
46
+ " \033[90mHọc viện Công nghệ Bưu chính Viễn thông (PTIT)\033[0m",
47
+ "",
48
+ f" \033[90m•\033[0m \033[37mHardware\033[0m : \033[90m{gpu_str}\033[0m",
49
+ " \033[90m•\033[0m \033[37mEngine\033[0m : \033[90mHardwareAdaptive v0.2\033[0m",
50
+ " \033[90m•\033[0m \033[37mBackends\033[0m : \033[90mModal / Colab / Local vLLM\033[0m",
51
+ ]
52
+
53
+ max_rows = max(len(PTIT_LOGO_ANSI), len(info_lines))
54
+ col_logo = list(PTIT_LOGO_ANSI) + [" " * 26] * (max_rows - len(PTIT_LOGO_ANSI))
55
+ col_info = list(info_lines) + [""] * (max_rows - len(info_lines))
56
+
57
+ output = []
58
+ for l, r in zip(col_logo, col_info):
59
+ output.append(f" {l} {r}")
60
+ return "\n".join(output)
61
+
62
+
63
+ def render_ascii_banner(hw_info: HardwareInfo) -> str:
64
+ """Compact branding/status fallback; hardware logic remains in HardwareInfo."""
65
+ return ascii_text(
66
+ "FlexSDS | Speculative Decoding Serving Engine | v0.2.0-alpha\n"
67
+ "PTIT | Hoc vien Cong nghe Buu chinh Vien thong | Nhom do an D22\n"
68
+ f"Hardware: {hw_info.summary()}\n"
69
+ "Engine: TreeAdaptive v0.2\n"
70
+ "Backends: Modal / Colab / Local vLLM"
71
+ )
72
+
73
+
74
+ def print_banner(hw_info: Optional[HardwareInfo] = None) -> None:
75
+ """Preserve rich branding on capable terminals; otherwise use plain ASCII."""
76
+ if hw_info is None:
77
+ hw_info = detect_hardware()
78
+ fallback = render_ascii_banner(hw_info)
79
+ rich = supports_unicode(sys.stdout) and supports_truecolor(sys.stdout)
80
+ print_output(render_banner(hw_info) if rich else fallback, fallback=fallback)
81
+
82
+
83
+ if __name__ == "__main__":
84
+ print_banner()
flexsds/cli/main.py ADDED
@@ -0,0 +1,330 @@
1
+ """FlexSDS CLI main dispatcher module."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import sys
6
+ from typing import Optional
7
+
8
+ try:
9
+ from flexsds.cli.output import print_output as print
10
+ from flexsds import __version__
11
+ from flexsds.cli.banner import print_banner
12
+ from flexsds.cli.wizard import run_interactive_wizard
13
+ from flexsds.core.hardware import detect_hardware, HardwareInfo
14
+ from flexsds.core.compatibility import (
15
+ check_compatibility,
16
+ evaluate_serving_preflight,
17
+ ServingBlockedError,
18
+ )
19
+ from flexsds.core.cost_model import (
20
+ FlexCostModel,
21
+ load_profile_for_hardware,
22
+ run_hardware_micro_benchmark,
23
+ )
24
+ from flexsds.core.credentials import CredentialsManager
25
+ from flexsds.client.chat import run_interactive_chat
26
+ from flexsds.client.monitor import run_live_monitor
27
+ from flexsds.cli.task_runner import TaskRunner
28
+ from flexsds.server.local_runner import LocalServingRunner, finish_launch
29
+ from flexsds.server.modal_runner import ModalServingRunner
30
+ except ModuleNotFoundError:
31
+ from src.flexsds.cli.output import print_output as print
32
+ from src.flexsds import __version__
33
+ from src.flexsds.cli.banner import print_banner
34
+ from src.flexsds.cli.wizard import run_interactive_wizard
35
+ from src.flexsds.core.hardware import detect_hardware, HardwareInfo
36
+ from src.flexsds.core.compatibility import (
37
+ check_compatibility,
38
+ evaluate_serving_preflight,
39
+ ServingBlockedError,
40
+ )
41
+ from src.flexsds.core.cost_model import (
42
+ FlexCostModel,
43
+ load_profile_for_hardware,
44
+ run_hardware_micro_benchmark,
45
+ )
46
+ from src.flexsds.core.credentials import CredentialsManager
47
+ from src.flexsds.client.chat import run_interactive_chat
48
+ from src.flexsds.client.monitor import run_live_monitor
49
+ from src.flexsds.cli.task_runner import TaskRunner
50
+ from src.flexsds.server.local_runner import LocalServingRunner, finish_launch
51
+ from src.flexsds.server.modal_runner import ModalServingRunner
52
+
53
+
54
+ def parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
55
+ parser = argparse.ArgumentParser(
56
+ prog="flexsds",
57
+ description="FlexSDS: Flexible Speculative Decoding Serving Engine for LLMs",
58
+ )
59
+ parser.add_argument(
60
+ "-v", "--version", action="version", version=f"flexsds {__version__}"
61
+ )
62
+
63
+ subparsers = parser.add_subparsers(dest="command", help="Command to execute")
64
+
65
+ # Command: detect
66
+ subparsers.add_parser("detect", help="Detect and display hardware / GPU specs")
67
+
68
+ # Command: profile
69
+ profile_parser = subparsers.add_parser("profile", help="Load hardware cost profile for current GPU")
70
+ profile_parser.add_argument(
71
+ "--benchmark",
72
+ action="store_true",
73
+ help="Run micro-benchmark to measure and save profile",
74
+ )
75
+ profile_parser.add_argument(
76
+ "--target",
77
+ type=str,
78
+ default="Qwen/Qwen2.5-7B-Instruct-AWQ",
79
+ help="Target model identifier",
80
+ )
81
+ profile_parser.add_argument(
82
+ "--draft",
83
+ type=str,
84
+ default="duckhoi/qwen2.5-7b-eagle3.1-speculator",
85
+ help="Draft model or speculator head identifier",
86
+ )
87
+ profile_parser.add_argument(
88
+ "--model",
89
+ type=str,
90
+ default=None,
91
+ help="Model identifier override for benchmarking",
92
+ )
93
+
94
+ # Command: stop
95
+ subparsers.add_parser("stop", help="Stop running local FlexSDS server")
96
+
97
+ # Command: auth
98
+ auth_parser = subparsers.add_parser("auth", help="Manage secure credentials outside repository")
99
+ auth_subparsers = auth_parser.add_subparsers(dest="auth_action", help="Auth action to perform")
100
+
101
+ # auth login
102
+ auth_login_parser = auth_subparsers.add_parser("login", help="Save API key / token securely")
103
+ auth_login_parser.add_argument("--provider", choices=["modal", "huggingface", "cloudflare"], default="modal", help="Target service provider")
104
+ auth_login_parser.add_argument("--key", type=str, default=None, help="Specific credential key name")
105
+ auth_login_parser.add_argument("--token", type=str, default=None, help="Token value (prompted securely if omitted)")
106
+
107
+ # auth status
108
+ auth_subparsers.add_parser("status", help="Show masked status of saved credentials")
109
+
110
+ # auth logout
111
+ auth_logout_parser = auth_subparsers.add_parser("logout", help="Delete saved credentials")
112
+ auth_logout_parser.add_argument("--provider", type=str, default=None, help="Target service provider to remove")
113
+
114
+ # Command: serve
115
+ serve_parser = subparsers.add_parser("serve", help="Launch speculative serving server")
116
+ serve_parser.add_argument("--target", type=str, help="Target model identifier")
117
+ serve_parser.add_argument("--draft", type=str, help="Draft model or speculator head identifier")
118
+ serve_parser.add_argument(
119
+ "--backend", choices=["modal", "local", "colab"], default="modal", help="Serving backend platform"
120
+ )
121
+ serve_parser.add_argument("--gpu", choices=["L4", "T4", "A10G"], default="L4", help="Remote GPU memory budget and Modal allocation")
122
+ serve_parser.add_argument(
123
+ "--port", type=int, default=8000, help="API server port"
124
+ )
125
+ serve_parser.add_argument(
126
+ "--background", action="store_true", help="Run local server as background daemon"
127
+ )
128
+ serve_parser.add_argument(
129
+ "--deploy", action="store_true", help="Deploy persistently on Modal Cloud"
130
+ )
131
+ serve_parser.add_argument(
132
+ "--no-adaptive", action="store_true", help="Disable HardwareAdaptive optimization engine"
133
+ )
134
+ serve_parser.add_argument(
135
+ "--allow-fallback", action="store_true", help="Allow running with conservative analytical fallback if exact GPU profile is missing"
136
+ )
137
+ serve_parser.add_argument(
138
+ "--dry-run", action="store_true", help="Print execution command without launching"
139
+ )
140
+
141
+ # Command: chat
142
+ chat_parser = subparsers.add_parser("chat", help="Start interactive streaming chat with served model")
143
+ chat_parser.add_argument("--endpoint", type=str, default="http://localhost:8000/v1", help="API base URL")
144
+ chat_parser.add_argument("--model", type=str, default=None, help="Served model name override")
145
+ chat_parser.add_argument("--temperature", type=float, default=0.0, help="Sampling temperature")
146
+
147
+ # Command: monitor
148
+ monitor_parser = subparsers.add_parser("monitor", help="Live telemetry monitor for speculative decoding traces")
149
+ monitor_parser.add_argument("--trace-file", type=str, default="telemetry/serving_trace.jsonl", help="Path to JSONL telemetry trace")
150
+ monitor_parser.add_argument("--refresh-rate", type=float, default=1.0, help="Telemetry poll interval in seconds")
151
+
152
+ return parser.parse_args(argv)
153
+
154
+
155
+ def get_serving_cost_profile(
156
+ hw_info: HardwareInfo,
157
+ allow_fallback: bool = False,
158
+ custom_profile_path: Optional[str] = None,
159
+ ) -> tuple[Optional[FlexCostModel], str]:
160
+ """Tải hồ sơ chi phí phần cứng phục vụ cho lệnh serve với cơ chế chặn sớm (fail-fast).
161
+
162
+ Nếu allow_fallback=False và không tìm thấy hồ sơ chính xác cho GPU hiện tại,
163
+ trả về (None, "") để kích hoạt xử lý chặn sớm an toàn.
164
+ Nếu allow_fallback=True, cho phép nạp hồ sơ suy diễn dè dặt phân tích an toàn.
165
+ """
166
+ return load_profile_for_hardware(
167
+ hw_info,
168
+ custom_profile_path=custom_profile_path,
169
+ strict=not allow_fallback,
170
+ )
171
+
172
+
173
+ def main(argv: Optional[list[str]] = None) -> int:
174
+ """Main entry point for FlexSDS command-line interface."""
175
+ args = parse_args(argv)
176
+ if args.command == "serve" and not 1 <= args.port <= 65535:
177
+ print("Invalid port: expected 1..65535")
178
+ return 2
179
+ if args.command == "monitor" and args.refresh_rate <= 0:
180
+ print("Refresh rate must be positive")
181
+ return 2
182
+ hw_info = detect_hardware()
183
+
184
+ if args.command == "stop":
185
+ local_runner = LocalServingRunner()
186
+ local_stopped = local_runner.stop()
187
+ if local_stopped:
188
+ print("\033[32m✔ Local vLLM server process stopped successfully.\033[0m")
189
+ else:
190
+ print("\033[90mNo active local server process found.\033[0m")
191
+ return 0
192
+
193
+ if args.command == "auth":
194
+ cred_mgr = CredentialsManager()
195
+ if args.auth_action == "status":
196
+ masked = cred_mgr.get_all_masked()
197
+ print("\n\033[1;37mFlexSDS Secure Credentials (~/.flexsds/credentials):\033[0m")
198
+ if not masked:
199
+ print(" \033[90m(No credentials saved. Run 'flexsds auth login' to add.)\033[0m")
200
+ for prov, keys in masked.items():
201
+ print(f" \033[32m✔ {prov}:\033[0m")
202
+ for k, v in keys.items():
203
+ print(f" {k}: {v}")
204
+ return 0
205
+ elif args.auth_action == "logout":
206
+ target_prov = getattr(args, "provider", None)
207
+ if target_prov:
208
+ deleted = cred_mgr.delete_credential(target_prov)
209
+ if deleted:
210
+ print(f"\033[32m✔ Removed credentials for provider: {target_prov}\033[0m")
211
+ else:
212
+ print(f"\033[90mNo credentials found for provider: {target_prov}\033[0m")
213
+ else:
214
+ for prov in list(cred_mgr.get_all_masked().keys()):
215
+ cred_mgr.delete_credential(prov)
216
+ print("\033[32m✔ Removed all stored credentials.\033[0m")
217
+ return 0
218
+ elif args.auth_action == "login":
219
+ import getpass
220
+ prov = args.provider
221
+ key_name = args.key or ("HF_TOKEN" if prov == "huggingface" else ("MODAL_TOKEN_ID" if prov == "modal" else "TOKEN"))
222
+ val = args.token or getpass.getpass(f"Enter {key_name} for {prov}: ")
223
+ if val:
224
+ cred_mgr.set_credential(prov, key_name, val.strip())
225
+ print(f"\033[32m✔ Securely saved {key_name} for {prov} (POSIX mode 0600; Windows uses filesystem ACLs).\033[0m")
226
+ return 0
227
+ return 0
228
+
229
+ # Display banner on interactive startup or regular commands
230
+ print_banner(hw_info)
231
+
232
+ if args.command == "detect":
233
+ print(f"\n\033[1;32m✔ Hardware: {hw_info.summary()}\033[0m")
234
+ return 0
235
+
236
+ if args.command == "profile":
237
+ if getattr(args, "benchmark", False):
238
+ target_model = args.model or args.target
239
+ draft_model = args.draft
240
+ cost_model, saved_path = run_hardware_micro_benchmark(
241
+ hw_info=hw_info,
242
+ target_model=target_model,
243
+ draft_model=draft_model,
244
+ )
245
+ print(f"\n\033[1;32m✔ Calibrated & saved new hardware profile: {saved_path}\033[0m")
246
+ return 0
247
+
248
+ _, profile_name = load_profile_for_hardware(hw_info)
249
+ print(f"\n\033[1;32m✔ Linked hardware profile: {profile_name}\033[0m")
250
+ return 0
251
+
252
+ if args.command == "chat":
253
+ return run_interactive_chat(
254
+ endpoint=args.endpoint,
255
+ model=args.model,
256
+ temperature=args.temperature,
257
+ )
258
+
259
+ if args.command == "monitor":
260
+ return run_live_monitor(
261
+ trace_file=args.trace_file,
262
+ refresh_rate=args.refresh_rate,
263
+ )
264
+
265
+ if args.command == "serve":
266
+ cost_model, profile_name = get_serving_cost_profile(
267
+ hw_info, allow_fallback=getattr(args, "allow_fallback", False)
268
+ )
269
+ if cost_model is None:
270
+ print(
271
+ f"\n\033[1;31m✖ [Lỗi phần cứng] Chưa có hồ sơ độ trễ cho GPU '{hw_info.device_name} ({hw_info.sm_architecture})'. "
272
+ f"Vui lòng chạy lệnh đo đạc: flexsds profile --benchmark (hoặc thêm cờ '--allow-fallback').\033[0m"
273
+ )
274
+ return 1
275
+
276
+ if profile_name == "conservative_analytical_fallback":
277
+ print(
278
+ f" \033[1;33m⚠ [Cảnh báo phần cứng] Sử dụng hồ sơ suy diễn dè dặt ({profile_name} / Conservative Fallback).\033[0m"
279
+ )
280
+
281
+ if args.target and args.draft:
282
+ target_model = args.target
283
+ draft_model = args.draft
284
+
285
+ preflight = evaluate_serving_preflight(target_model, draft_model, hw_info,
286
+ args.backend, getattr(args, "gpu", "L4"))
287
+ print(preflight.report.summary())
288
+ if not preflight.is_safe:
289
+ print("Launch blocked: " + "; ".join(preflight.reasons))
290
+ if args.backend != "local":
291
+ print(f"Remote {getattr(args, 'gpu', 'L4')} memory budget is estimated, not measured free VRAM.")
292
+ cfg = {"serving": {
293
+ "target_model": target_model, "draft_model": draft_model,
294
+ "draft_method": preflight.report.draft_method,
295
+ "use_heterogeneous_vocab": preflight.report.use_heterogeneous_vocab,
296
+ "port": args.port, "target_gpu": getattr(args, "gpu", "L4"),
297
+ "enable_adaptive": not args.no_adaptive,
298
+ }}
299
+ if args.backend == "colab":
300
+ print("Colab guidance only: no server or endpoint was created; supplied models are not forwarded to the notebook.")
301
+ print("Open notebooks/flexsds_colab_server.ipynb and configure the exact models on that runtime.")
302
+ return 0
303
+ if not args.dry_run and not preflight.is_safe:
304
+ return 2
305
+ if args.dry_run:
306
+ print("Preview only; not a readiness check.")
307
+ if args.backend == "local":
308
+ LocalServingRunner().launch(cfg, port=args.port, background=args.background, dry_run=True)
309
+ else:
310
+ ModalServingRunner().launch(cfg, deploy=args.deploy, dry_run=True)
311
+ return 0
312
+ tasks = TaskRunner()
313
+ task = tasks.add_task(f"Start {args.backend} serving process")
314
+ def launch(t):
315
+ if args.backend == "local":
316
+ return LocalServingRunner().launch(cfg, port=args.port, background=args.background)
317
+ return ModalServingRunner().launch(cfg, deploy=args.deploy)
318
+ proc = tasks.run_task(task, launch)
319
+ return finish_launch(proc, background=args.backend == "local" and args.background)
320
+
321
+ try:
322
+ result = run_interactive_wizard(hw_info)
323
+ return result.get("exit_code", 0) if isinstance(result, dict) else 0
324
+ except ServingBlockedError as exc:
325
+ print(str(exc))
326
+ return 2
327
+
328
+
329
+ if __name__ == "__main__":
330
+ sys.exit(main())
flexsds/cli/output.py ADDED
@@ -0,0 +1,69 @@
1
+ """Encoding-safe presentation without reconfiguring caller-owned streams."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import re
6
+ import sys
7
+
8
+
9
+ _ANSI = re.compile(r"\x1b\[[0-9;]*m")
10
+
11
+
12
+ def supports_unicode(stream) -> bool:
13
+ """Unknown codecs/streams conservatively use ASCII."""
14
+ encoding = getattr(stream, "encoding", None)
15
+ if not encoding:
16
+ return False
17
+ try:
18
+ "\u2584\u2714\u26a0\u00b7".encode(encoding)
19
+ except (UnicodeEncodeError, LookupError):
20
+ return False
21
+ return True
22
+
23
+
24
+ def supports_truecolor(stream) -> bool:
25
+ """Require positive terminal capability evidence, never send color to pipes."""
26
+ if "NO_COLOR" in os.environ or os.environ.get("TERM") == "dumb":
27
+ return False
28
+ try:
29
+ if not getattr(stream, "isatty", lambda: False)():
30
+ return False
31
+ except OSError:
32
+ return False
33
+ return (os.environ.get("COLORTERM", "").lower() in ("truecolor", "24bit")
34
+ or os.environ.get("TERM", "").endswith(("-direct", "-truecolor"))
35
+ or bool(os.environ.get("WT_SESSION")))
36
+
37
+
38
+ def ascii_text(text: str) -> str:
39
+ """Keep status meanings; escape other unencodable content rather than omit it."""
40
+ text = _ANSI.sub("", text)
41
+ for symbol, replacement in (("\u2714", "[OK]"), ("\u26a0", "[WARN]"),
42
+ ("\u00b7", "|"), ("\u2022", "*")):
43
+ text = text.replace(symbol, replacement)
44
+ return text.encode("ascii", errors="backslashreplace").decode("ascii")
45
+
46
+
47
+ def print_output(text: str = "", *, file=None, end: str = "\n", fallback=None) -> None:
48
+ """Write one message; only encoding errors may cause an ASCII retry.
49
+
50
+ I/O errors such as a closed pipe propagate. The stream's encoding is never
51
+ changed. A supplied fallback preserves a compact banner if metadata lies.
52
+ """
53
+ stream = sys.stdout if file is None else file
54
+ if stream is None:
55
+ return
56
+ message = text + end
57
+ if not supports_truecolor(stream):
58
+ message = _ANSI.sub("", message)
59
+ if not supports_unicode(stream):
60
+ message = ascii_text((fallback if fallback is not None else text) + end)
61
+ try:
62
+ stream.write(message)
63
+ except UnicodeEncodeError:
64
+ stream.write(ascii_text((fallback if fallback is not None else text) + end))
65
+
66
+
67
+ def write_output(text: str) -> None:
68
+ """Encoding-safe incremental output for streamed chat content."""
69
+ print_output(text, end="")
@@ -0,0 +1,171 @@
1
+ """Interactive task manager with compact view and Ctrl+O toggled details."""
2
+ from __future__ import annotations
3
+
4
+ import enum
5
+ import os
6
+ import select
7
+ import sys
8
+ import threading
9
+ import time
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Callable, Dict, List, Optional
12
+
13
+ try:
14
+ from flexsds.cli.output import ascii_text, supports_unicode
15
+ except ModuleNotFoundError:
16
+ from src.flexsds.cli.output import ascii_text, supports_unicode
17
+
18
+
19
+ class TaskStatus(enum.Enum):
20
+ PENDING = "pending"
21
+ RUNNING = "running"
22
+ COMPLETED = "completed"
23
+ FAILED = "failed"
24
+
25
+
26
+ @dataclass
27
+ class Task:
28
+ """Represents a discrete setup or serving execution task."""
29
+ name: str
30
+ status: TaskStatus = TaskStatus.PENDING
31
+ logs: List[str] = field(default_factory=list)
32
+ start_time: float = 0.0
33
+ duration: float = 0.0
34
+ error: Optional[str] = None
35
+
36
+ def log(self, message: str) -> None:
37
+ """Appends a log line to task buffer without polluting terminal."""
38
+ clean_msg = message.rstrip("\r\n")
39
+ if clean_msg:
40
+ self.logs.append(clean_msg)
41
+
42
+
43
+ class TaskRunner:
44
+ """Orchestrates tasks, renders interactive status, and handles Ctrl+O log toggle."""
45
+
46
+ SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
47
+
48
+ def __init__(self, interactive: Optional[bool] = None) -> None:
49
+ self.tasks: List[Task] = []
50
+ self.show_details: bool = False
51
+ self.is_interactive = interactive if interactive is not None else sys.stdout.isatty()
52
+ self._current_frame_idx = 0
53
+ self._listener_thread: Optional[threading.Thread] = None
54
+ self._stop_listener = threading.Event()
55
+
56
+ def add_task(self, name: str) -> Task:
57
+ """Enqueues a new task."""
58
+ task = Task(name=name)
59
+ self.tasks.append(task)
60
+ return task
61
+
62
+ def toggle_details(self) -> None:
63
+ """Toggles between compact task list and expanded log details."""
64
+ self.show_details = not self.show_details
65
+
66
+ def handle_key(self, key: str) -> bool:
67
+ """Handles keypress events (Ctrl+O is ASCII 0x0F)."""
68
+ if key == "\x0f" or key == "\x0e": # Ctrl+O (or Ctrl+N alternative)
69
+ self.toggle_details()
70
+ return True
71
+ return False
72
+
73
+ def render_view(self) -> str:
74
+ """Renders current status of all tasks with optional log detail box."""
75
+ lines: List[str] = []
76
+ active_task: Optional[Task] = None
77
+
78
+ for idx, t in enumerate(self.tasks, 1):
79
+ if t.status == TaskStatus.COMPLETED:
80
+ icon = "\033[32m✔\033[0m"
81
+ dur_str = f" \033[90m({t.duration:.1f}s)\033[0m"
82
+ lines.append(f" {icon} [{idx}/{len(self.tasks)}] {t.name}{dur_str}")
83
+ elif t.status == TaskStatus.RUNNING:
84
+ active_task = t
85
+ spinner = self.SPINNER_FRAMES[self._current_frame_idx % len(self.SPINNER_FRAMES)]
86
+ icon = f"\033[36m{spinner}\033[0m"
87
+ elapsed = max(time.perf_counter() - t.start_time, 0.0) if t.start_time > 0 else 0.0
88
+ dur_str = f" \033[33m({elapsed:.1f}s)\033[0m"
89
+ lines.append(f" {icon} [{idx}/{len(self.tasks)}] \033[1m{t.name}\033[0m{dur_str}")
90
+ elif t.status == TaskStatus.FAILED:
91
+ active_task = t
92
+ icon = "\033[31m✖\033[0m"
93
+ lines.append(f" {icon} [{idx}/{len(self.tasks)}] \033[31m{t.name}\033[0m")
94
+ else:
95
+ icon = "\033[90m○\033[0m"
96
+ lines.append(f" {icon} [{idx}/{len(self.tasks)}] \033[90m{t.name}\033[0m")
97
+
98
+ # Footer hướng dẫn phím tắt
99
+ if self.show_details:
100
+ footer = " \033[90m[\033[36mCtrl+O\033[90m: Thu gọn chi tiết log | \033[36mCtrl+C\033[90m: Dừng]\033[0m"
101
+ else:
102
+ footer = " \033[90m[\033[36mCtrl+O\033[90m: Xem chi tiết tác vụ | \033[36mCtrl+C\033[90m: Dừng]\033[0m"
103
+
104
+ lines.append("")
105
+ lines.append(footer)
106
+
107
+ # Nếu chế độ mở rộng đang bật, hiển thị khung log của tác vụ đang chạy hoặc vừa chạy
108
+ if self.show_details and active_task and active_task.logs:
109
+ lines.append("")
110
+ lines.append(f" \033[90m┌─── \033[1;37mNhật ký tác vụ: {active_task.name}\033[0m \033[90m" + "─" * 30 + "┐\033[0m")
111
+ recent_logs = active_task.logs[-8:] # Hiển thị 8 dòng gần nhất
112
+ for log_line in recent_logs:
113
+ lines.append(f" \033[90m│\033[0m \033[90m{log_line[:76]}\033[0m")
114
+ lines.append(" \033[90m└" + "─" * 60 + "┘\033[0m")
115
+
116
+ view = "\n".join(lines)
117
+ return view if supports_unicode(sys.stdout) else ascii_text(view)
118
+
119
+ def _start_key_listener(self) -> None:
120
+ """Starts background non-blocking key listener for Ctrl+O."""
121
+ if not self.is_interactive or not sys.stdin.isatty():
122
+ return
123
+
124
+ def _listen() -> None:
125
+ try:
126
+ import termios
127
+ import tty
128
+
129
+ old_settings = termios.tcgetattr(sys.stdin)
130
+ tty.setcbreak(sys.stdin.fileno())
131
+ try:
132
+ while not self._stop_listener.is_set():
133
+ # Kiểm tra xem có dữ liệu stdin trong 0.1 giây không
134
+ rlist, _, _ = select.select([sys.stdin], [], [], 0.1)
135
+ if rlist:
136
+ ch = sys.stdin.read(1)
137
+ if ch:
138
+ self.handle_key(ch)
139
+ finally:
140
+ termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
141
+ except Exception:
142
+ pass
143
+
144
+ self._stop_listener.clear()
145
+ self._listener_thread = threading.Thread(target=_listen, daemon=True)
146
+ self._listener_thread.start()
147
+
148
+ def _stop_key_listener(self) -> None:
149
+ """Stops background key listener."""
150
+ self._stop_listener.set()
151
+ if self._listener_thread and self._listener_thread.is_alive():
152
+ self._listener_thread.join(timeout=0.3)
153
+
154
+ def run_task(self, task: Task, action: Callable[[Task], Any]) -> Any:
155
+ """Executes a single task with live status updates."""
156
+ task.status = TaskStatus.RUNNING
157
+ task.start_time = time.perf_counter()
158
+ self._start_key_listener()
159
+
160
+ try:
161
+ result = action(task)
162
+ task.duration = max(time.perf_counter() - task.start_time, 0.0)
163
+ task.status = TaskStatus.COMPLETED
164
+ return result
165
+ except Exception as e:
166
+ task.duration = max(time.perf_counter() - task.start_time, 0.0)
167
+ task.status = TaskStatus.FAILED
168
+ task.error = str(e)
169
+ raise
170
+ finally:
171
+ self._stop_key_listener()